#17 HABTM Checkboxes (revised)
- Download:
- source codeProject Files in Zip (93.3 KB)
- mp4Full Size H.264 Video (10.8 MB)
- m4vSmaller H.264 Video (6.44 MB)
- webmFull Size VP8 Video (8.22 MB)
- ogvFull Size Theora Video (14.4 MB)
Below is a page from a Rails application that displays a list of products.
A product can belong to a number of categories and there’s a many-to-many association between the Product
and Category
models. The product shown below isn’t in any categories and as yet we don’t have a way to assign categories to a product through the user interface. In this episode we’ll add the ability to assign categories through a list of checkboxes when a user edits the product.
The two models are associated using has_many :through
. Here’s the code for Product
.
class Product < ActiveRecord::Base has_many :categorizations has_many :categories, through: :categorizations end
A product has many categories through a join model called Categorization
.
class Categorization < ActiveRecord::Base belongs_to :category belongs_to :product end
The Category
model has a similar has_many :through
relationship with Product
.
The technique we’ll show you will also work for a has_and_belongs_to_many
relationship but has_many :through
is much more flexible which is why we’ve chosen to use it. One advantage is gives us is a method for accessing the category_ids
for a product. We can demonstrate this in the console by fetching a product and listing its category_ids
.
> p = Product.first > p.category_ids => []
We can also set category_ids through this method.
> p.category_ids = [1,2] => [1, 2]
This assigns the categories and creates the categorization records too and we can now fetch the categories for our product.
> p.categories => [#<Category id: 1, name: "Board Games", created_at: "2011-12-26 21:22:48", updated_at: "2011-12-26 21:22:48">, #<Category id: 2, name: "Clothing", created_at: "2011-12-26 21:22:48", updated_at: "2011-12-26 21:22:48">]
If we look at our product in the browser now we’ll see the categories we’ve assigned to it.
Adding Categories to The Edit Product Form
We need to modify the edit product form so that we can add and remove categories through it. Here’s the form’s template.
<%= form_for(@product) do |f| %> <% if @product.errors.any? %> <div class="error_messages"> <h2><%= pluralize(@product.errors.count, "error") %> prohibited this product from being saved:</h2> <ul> <% @product.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> <div class="field"> <%= f.label :price %><br /> <%= f.text_field :price %> </div> <div class="actions"> <%= f.submit %> </div> <% end %>
The form currently has two fields, one for the product’s name
and one for its price
. We want to add another for the categories and this is how we do it:
<div class="field"> <% Category.all.each do |category| %> <%= check_box_tag "product[category_ids][]", category.id %> <%= category.name %><br/> <% end %> </div>
In the code above we loop through the categories and display a checkbox for each one. If we were rendering a single checkbox we could use f.check_box :category_ids
but this won’t work here as category_ids
is an array and we need multiple attributes for each attribute. Instead we’ve gone for a more manual approach and used check_box_tag
. To make this work with multiple checkboxes we need to end the name with a pair of empty square brackets. This tells Rails to bundle up all of the values submitted into an array in the params
hash. When we visit the edit form for a product now the checkboxes are shown.
If we select some options and submit the form the categories will be added.
When we go back to edit the product again, however, the categories that we selected won’t be checked. We can fix this by adding a third argument to the check_box_tag
that so that the checkbox is checked if the product’s category_id
’s include that category.
<% Category.all.each do |category| %> <%= check_box_tag "product[category_ids][]", category.id, @product.category_ids.include?(category.id) %> <%= category.name %><br/> <% end %>
When we edit a product now the correct checkboxes are checked.
There’s still a bug with this form, though. If we uncheck the selected checkboxes and submit the form the previously selected categories aren’t removed. This is because unchecked checkboxes don’t have their values submitted back to the server. To get around this problem we can go modify to our form to add a hidden field with the same name as the checkboxes and with a value of nil
.
<div class="field"> <%= hidden_field_tag "product[category_ids][]", nil%> <% Category.all.each do |category| %> <%= check_box_tag "product[category_ids][]", category.id, @product.category_ids.include?(category.id) %> <%= category.name %><br/> <% end %> </div>
Now if none of the checkboxes are checked when the form’s submitted the hidden field is included in the fields posted back to the server and the product has its categories removed.
Clickable Checkbox Labels
There’s one more issue in the edit form that we should resolve. Clicking the label next to a checkbox should act as if we’d clicked that checkbox. It’s easy to wrap the name in a label
tag, but we have to associate the label with a checkbox via its id
and currently each checkbox has the same id
. This is bad practice anyway so first we’ll modify the view template to assign each checkbox a unique id.
<% Category.all.each do |category| %> <%= check_box_tag "product[category_ids][]", category.id, @product.category_ids.include?(category.id), id: dom_id(category) %> <%= label_tag dom_id(category), category.name %><br/> <% end %>
To make each checkbox’s id
unique we use the dom_id
method that Rails provides and pass it the category. This will generate an id
of category_n
where n
is the category’s id
. We can then use a label_tag
to display the category’s name and we can use dom_id
here too to associate the label with the checkbox. Now, when we reload the page, we can click the labels to toggle the checkboxes.
The code we have now works well but it’s a little complicated, especially in the view. We could extract this out into a form builder but this is out of the scope of this episode. Alternatively we could use a gem such as SimpleForm or Formtastic. SimpleForm provides a collection_check_boxes method which lets us do what we’ve done here but in a much more concise method. Formtastic has a similar check_boxes method.