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.
Hi there.
August 26th, 2008 at 5:29 amThanks 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
Thanks for sharing! You saved my day!
August 25th, 2008 at 10:29 pmNo more messing around with model callbacks to do the job!
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…
August 5th, 2009 at 2:41 amWord 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
November 11th, 2009 at 10:44 pmExcelent! Very Usefull!
greetings from Argentina
March 20th, 2010 at 5:46 pmThanks, 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.
May 6th, 2010 at 7:11 pmAwesome and thans
As for Archie,
You have to place that in "new" and "edit" controller
October 18th, 2010 at 8:06 amre: Meme's comment
Actually I think Archie is correct about putting it in "update" action.
I tried putting it in "edit" and all it does is empty out the checkboxes upon reaching the edit view, leading me to think that line is meant to be processed AFTER the edit data is submitted (which makes more sense when I read that line).
I moved it into "update" action and everything is behaving correctly now. I had no need to modify either "create" or "new" actions.
December 15th, 2010 at 11:31 amThank you, you saved me hours of research
February 4th, 2011 at 2:20 pmGreat content.
Thanks!
February 22nd, 2011 at 11:40 amQuick note on the labels. If you want to get the labels to work, set :id => "group_check_#{group.id}" on your checkbox and label_tag "group_check_#{group.id}", group.name for your lables.
April 27th, 2011 at 12:58 pmThanks for this great post; it really helped me in getting this set up. I'm not sure I would have ever figured out how it worked without your description (I would have done a lot of extra manual handling).
May 22nd, 2011 at 5:29 pmYou need put this:
attr_accesible :group_ids
On user.rb to avoid WARNING: Can't mass-assign protected attributes: group_ids
June 4th, 2011 at 12:30 pmThank you for sharing this tip. Nice solution. Saved me a lot of time.
July 20th, 2011 at 11:30 amThanks, I'm much closer…But, what if your user and your group both belonged an overall Union model…say Union has many users and Union has many groups…Right now I'm getting all the checkboxes and groups for the whole app, instead of just one Union?
September 1st, 2011 at 8:16 amGreat tutorial!
September 3rd, 2011 at 7:09 pmHey man, great tip!
Thank you.
January 30th, 2012 at 7:48 amHow do I get the 'update' RSpec to pass using this technique?
June 17th, 2012 at 11:25 amThank you sir, this helped me immensely, even all these years later.
July 20th, 2012 at 12:17 pmMuchas gracias por el aporte! Saludos desde Argentina
July 24th, 2012 at 11:27 amOr you could just do:
params[:user][:group_ids] ||= []
In your controller's update method, before calling update_attributes.
September 6th, 2012 at 11:52 pmAnd how to validate for atleast 1 memership?
I've tried:
validates :memership, :length => { :minimum => 1} #must have atleast 1 record in the HABTM relation
But it does return activerecord validation errors while checking many memerships or none
Nice post!
December 7th, 2012 at 8:52 am