Blog
How to handle many to many associations in nested forms using checkboxes !
As everybody know thanks to one of the first awesome Ryan Bates’ railscasts (see http://railscasts.com/episodes/17-habtm-checkboxes or the revised one http://railscasts.com/episodes/17-habtm-checkboxes-revised ), you can easily deal with many to many relations using check_box_tags.
For instance, if we have books and categories, here is the code you could obtain!
The classes:
book.rb
class Book < ActiveRecord::Base
has_many :classifications, :dependent => :destroy, :autosave => true , :inverse_of => :book
accepts_nested_attributes_for :classifications, :allow_destroy => true, :reject_if => :all_blank
has_many :categories, :through => :classifications
end
category.rb
class Category < ActiveRecord::Base
has_many :classifications, :dependent => :destroy, :autosave => true , :inverse_of => :category
accepts_nested_attributes_for :classifications, :allow_destroy => true, :reject_if => :all_blank
has_many :books, :through => :classifications
end
classification.rb
class Classification < ActiveRecord::Base
belongs_to :category, :inverse_of => :classifications
belongs_to :book, :inverse_of => :classifications
end
And the form:
new.html.erb
<%= form_for @book do |f| %>
<p>
<%= f.label :name %>
<%= f.text_field :name %>
</p>
<p>Categories</p>
<ul>
<% @categories.each do |cat| %>
<%= hidden_field_tag "book_category_ids_none", nil, {:name => "book[category_ids][]"}%>
<li>
<%= check_box_tag "book_category_ids_#{cat.id}", cat.id, (f.object.categories.present? && f.object.categories.include?(cat.id)), {:name => "book[category_ids][]"} %>
<%= label_tag "book_category_ids_#{cat.id}", cat.name %>
</li>
<% end %>
</ul>
<%= f.submit %>
<% end %>
But now, how to handle this if we introduce the new concept of library. And furthermore, if you need to had books with all these linked categories directly from the library creation or edition form.
You’ll need to use a unique reference to identify these nested objects, a convenient way exists to solve this problem.
First, install the cocoon gem (All detailed information are available here https://github.com/nathanvda/cocoon)
Then, here is the new library class and the partial form used with cocoon
classification.rb
class Library < ActiveRecord::Base
has_many :books, :dependent => :destroy, :autosave => true , :inverse_of => :library
accepts_nested_attributes_for :books, :allow_destroy => true, :reject_if => :all_blank
end
_book_fields.html.erb
<div>
<h3>Book</h3>
<p>
<%= f.label :name %>
<%= f.text_field :name %>
<p>
<p>Categories</p>
<ul>
<% @categories.each do |cat| %>
<%= hidden_field_tag "#{f.object_name}_category_ids_none", nil, {:name => "#{f.object_name}[category_ids][]"}%>
<li>
<%= check_box_tag "#{f.object_name}_category_ids_#{cat.id}", cat.id, (f.object.categories.present? && f.object.categories.include?(cat.id)), {:name => "#{f.object_name}[category_ids][]"} %>
<%= label_tag "#{f.object_name}_category_ids_#{cat.id}", cat.name %>
</li>
<% end %>
</ul>
<%= link_to_remove_association "Remove book", f %>
</div>
As you can see the method object_name give you the complete name of the newly instanciated object requested by the form. The same way, the method object_id could give you only the id of this object.