Quick Tip: has_many :through => checkboxes

It’s really easy to create a many-to-many relationship that can be assigned through checkboxes. Check it out!

Let’s say you have Users and Groups. A User can belong to a Group and a Group can have many Users – we call this a Membership, like so (migrations omitted for brevity):

app/models/user.rb

class User < ActiveRecord::Base
  has_many :memberships, :dependent => :destroy
  has_many :groups, :through => :memberships
end

app/models/group.rb

class Group < ActiveRecord::Base
  has_many :memberships, :dependent => :destroy
  has_many :users, :through => :memberships
end

app/models/membership.rb

class Membership < ActiveRecord::Base
  belongs_to :group
  belongs_to :user
end

We can now assign groups to members in a relatively easy manner with no extra work needed in the models. Behold!

app/views/users/edit.html.erb

<h1>User <%= @user.id -%></h1>

<h2>Group Memberships</h2>
<% form_for @user do -%>
  <% Group.all.each do |group| -%>
    <div>
      <%= check_box_tag :group_ids, group.id, @user.groups.include?(group), :name => 'user[group_ids][]' -%>
      <%= label_tag :group_ids, group.id -%>
    </div>
  <% end -%>
  <%= submit_tag -%>
<% end -%>

Errr… something like that. Anyway, the important thing to note is the use of group_ids. The values will get submitted as group_ids, a member of the User. Where did that come from? We don’t have an attribute or method on the model for it, so where’d it come from? Well, seems that it is auto-generated for you to allow something like I just showed.

When this form is submitted, any checked Groups will be associated through Memberships to the User by way of the magic *_ids= method. Should work the other way too with user_ids checkboxes on a group. No extra code needed. Awesome, right?

Bonus: If you uncheck all the checkboxes, then nothing gets posted, doh! So make sure to merge a default value with your parameters like this to ensure the *_ids= method gets called:

app/controllers/users_controller.rb

@user.attributes = {'group_ids' => []}.merge(params[:user] || {})

Super Bonus: When you’re defaulting the group_ids in the controller make sure to use the key as a string, not a symbol. Or if you do use a symbol then make it a Hash with_indifferent_access.

Super Monkey Ball: A monkey encased in a ball who collects bananas.

Posted August 26th, 2008 at 5:29 am in Ruby on Rails | Permalink

This website uses IntenseDebate comments, but they are not currently loaded because either your browser doesn't support JavaScript, or they didn't load fast enough.

6 comments:

  1. Archie:

    Hi there.
    Thanks for the quick tip. As a newbie I have been struggling with above and think your solution to very simple compared to other examples posted elsewhere. Just one question: Where would you actually place the “merge(params[:user]”to make sure the *_ids= method gets called? I was thinking the update method but I am not sure. Perhaps it would be helpful to put inthis into context.
    Much appreciated.
    Archie

  2. Mike:

    Thanks for sharing! You saved my day!
    No more messing around with model callbacks to do the job!

  3. @bpaul:

    Very nice, I love it when you can get Rails to do most of the work! Now if someone updated the check_box_tag to work nicely with arrays it would be even simpler…

  4. rcrogers:

    Word of warning — when you use the collection_ids method, Rails fails to call before_destroy and after_destroy on the join model, don't ask me why. I wasn't the only one to notice: http://dev.rubyonrails.org/ticket/7743

  5. Fer:

    Excelent! Very Usefull!

    greetings from Argentina

  6. nando:

    Thanks, it's been very useful for me too.

    I'd like to point something. In the HTML generated we have diferent checkbox elements with the same id. Also the label's for attribute has the same value for all of them, losing their purpose.

Leave a response: