Post Meta

Bookmarks

  • Delicious
  • Digg
  • Reddit
  • Magnolia
  • Newsvine
  • Furl
  • Facebook
  • Technorati

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?

has_many with select box and 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)

Ryan’s solution

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 :D

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) %>
 
      &nbsp;&nbsp;  
      <%= 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)


Related Post

  • No Related Post

  1. Gravatar

    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.

    11 / 13 / 09:22

Leave A Comment

+ -