A simple and fast solution to add has_many associations via a select box and checkboxes.
Have you ever wanted to add associations to a record by selecting associated items with a select box and collecting the associations into a set of checkboxes?
The problem
It is described in Ryan Bates’ Complex Forms screencast and a good solution is given there. However Ryan’s solution is a little bit complicated by:
- using Add / Remove buttons for each association
- the CRUD code is full with hacks (especially the Update section)
The Solution
The selectbox / checkboxes approach in my opinion:
- is more natural for the end user:
- you select an association and add to a list
- if you don’t need the association you just uncheck it. The checkbox is a very common and simple way to accept something or not.
- is much easier to implement for you:
- you’ll have to create only once, only one action: the “Add”
- handling associations as a collection of checkboxes has almost a natural support in rails.
And the good news are the “Add” action will not use server calls via AJAX (link_to_remote), rendering the checkbox is done by client-side javascript (link_to_function).
The Value Added
It took me one hard day to figure out Rails is not able to pass parameters to link_to_function as passing with :with to link_to_remote.
In other words, when using client-side Javascript to handle a request you’ll have to gather the parameters needed for the response also via Javascript, Rails is not offering you any help. (Correct me please if I’m wrong)
The Code
The explanation is missing, but if you watch Ryan’s screencast you’ll understand the logic and the technique used in these snippets too.
Anyway comments are open, feel free to ask me questions
The Model
The user.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class User < ActiveRecord::Base has_many :structures, :dependent => :nullify # A virtual attribute for the selectbox on the view when adding structures to a user def virtual_orgchart_selector=(virtual_orgchart_selector) # empty, it is only for the view end # A virtual attribute for adding structures to user # - this will add the structures checked on the view to the database # - if all structures are removed on the view, on update, this method won't be called. # this issue is managed in the controller's update method def virtual_orgcharts=(virtual_orgcharts) orgcharts = virtual_orgcharts.values orgs = orgcharts.map {|o| Structure.find_by_id o[:id] } self.structures.replace orgs end end |
In structure.rb
1 | belongs_to :user |
The View and the Helper
The views/users/new.html.erb (exactly the same like the edit.html.erb)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <h1>New user</h1> <% form_for(@user) do |f| %> <%= f.error_messages %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <% fields_for "user[virtual_orgchart_selector]", @user.structures do |org| %> <p> <%= org.label "Organizational roles" %><br /> <%= org.select :id, to_select(Structure.orgcharts) %> <%= link_to_add_checkbox "Add", "user[virtual_orgcharts]" %> </p> <% end %> <div id="orgcharts_container"> <%= render :partial => "orgchart", :collection => @user.structures %> </div> <p> <%= f.submit "Create" %> </p> <% end %> <%= link_to 'Back', users_path %> |
The _orgchart.html.erb partial”
1 2 | <%= to_checkbox_tag_from_record "user[virtual_orgcharts]", orgchart, 1 %> <br/ > |
The Application.js
1 2 3 4 5 6 | function insert_checkbox(dom) { id = $('user_virtual_orgchart_selector_id').value; name = $('user_virtual_orgchart_selector_id').select('[value=' + id + ']').first().innerHTML; dom_id = dom + '[' + id + '][id]'; $('orgcharts_container').insert('<input type="checkbox" checked="checked" value='+id+' id='+dom_id+' name='+dom_id+' />' + name); } |
The Controller
The users_controller update method is extended to handle the scenario when all structures/orgcharts are removed for a given user
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # PUT /users/1 # PUT /users/1.xml def update @user = User.find(params[:id]) # removing all orgcharts when all checkboxes are cleared # - if the checkboxes are all cleared, there is no 'virtual_orgcharts' param and the 'virtual_method=' is not executed in the model @user.structures.clear if params[:virtual_orgcharts].nil? respond_to do |format| if @user.update_attributes(params[:user]) flash[:notice] = 'User was successfully updated.' format.html { redirect_to(@user) } format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @user.errors, :status => :unprocessable_entity } end end end |
The application helper (for HTML generators)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # HTML generators, formatters # ---------------------------- # Returns a check_box_tag + label # Usage: to_check_box_tag("user[structures]", "Director General", 1) def to_check_box_tag(dom_id, name, value, checked=true) "#{check_box_tag dom_id, value, checked} #{name}" end # Returns a check_box_tag + label # - the input is a database record with id and name # - the dom_id is transformed to suit AR associations def to_checkbox_tag_from_record(dom, record, checked=true) return "" if record.nil? id = record.id name = record.name dom_id = "#{dom}[#{id.to_s}][id]" to_check_box_tag dom_id, name, id end |
And finally the User spec
1 2 3 4 5 6 7 8 9 10 11 | User - should be valid - should have many structures - should offer for the Controller a virtual attribute to display a select box on create/update - should offer for the Controller a virtual attribute to manage the Orgchart has_many associations User with Orgcharts - should clear the associated structures when the user is removed (PENDING: Not Yet Implemented) - should allow to remove all orgcharts of a user (PENDING: Not Yet Implemented) - should allow to select any existing Orgchart when adding or updating the user (PENDING: Not Yet Implemented) - should clear automatically the existing association if the association is assigned to another user (PENDING: Not Yet Implemented) |





You’re missing some of the code required, specifically the “to_select” method, and the “link_to_add_checkbox” method… you may want to look this over.