17: HABTM Checkboxes
(view original Railscast)
In our example application for this episode we have two models, Product
and Category
, each with a has_and_belongs_to_many
relationship with the other. What we would like to be able to do is to edit the categories that a product is in by using a list of checkboxes on the edit form for that product.
Adding the checkboxes
The form for editing our product currently has fields for the product’s name and price, but no way of editing the categories the product is in. There are two methods for adding a checkbox to a form, check_box
and check_box_tag
. We’re going to use check_box_tag
as is gives us the control we need over the name of the checkbox. The code we’ll add to the edit form (products/edit.html.erb
) is shown below.
<% for category in Category.find(:all) %> <div> <%= check_box_tag "product[category_ids][]", category.id, @product.categories.include?(category) %> <%= category.name %> </div> <% end %>
check_box_tag
takes three parameters: the name of the checkbox, the value of the checkbox and a boolean value that determines whether the checkbox should be checked. The name we’ve used looks a little strange, but you’ll see why it is named this way shortly. We determine if each category checkbox should be checked or not by seeing if the product is in that category by using @product.categories.include?(category)
.
Seeing if it works
The edit form with the checkboxes added.
If we refresh the form and test it, we see that it works and the product’s categories are updated when we check some of the checkboxes and click ‘submit’. How does Rails know how to update the product correctly? The development log has the answer.
Processing ProductsController#update (for 127.0.0.1 at 2009-01-15 20:57:56) [PUT] Parameters: {"commit"=>"Edit", "authenticity_token"=>"31b711f2c24ae7cea5abf3f758eef46b472eebf3", "product"=>{"price"=>"99.0", "name"=>"Television Stand", "category_ids"=>["2", "4"]}, "id"=>"1"}
When it’s submitted, the edit form passes the category parameters in the product hash as an array. It does this because of the name we gave the checkboxes, product[category_ids][]
. The first part of the name tells Rails to pass the category_ids as part of the product hash, while the empty square brackets tell it to pass the values as an array. But where does the product’s category_ids
method, which is called when the parameters for the product are updated, come from? The answer is that its generated by the has_and_belongs_to_many
method in the Product
model. We can test this by opening script/console
and updating a product manually.
>> p = Product.first => #<Product id: 1, name: "Television Stand", price: 99.0, created_at: "2009-01-11 21:32:12", updated_at: "2009-01-11 21:32:12"> >> p.category_ids => [2, 3] >> p.category_ids = [1,4] => [1, 4] >>
When we do this Rails generates the following SQL to update the product’s categories:
DELETE FROM "categories_products" WHERE product_id = 1 AND category_id IN (2,3) INSERT INTO "categories_products" ("product_id", "id", "category_id") VALUES (1, 1, 1) INSERT INTO "categories_products" ("product_id", "id", "category_id") VALUES (1, 4, 4)
One Small Gotcha
There is still one small problem with our update method. If we uncheck all of the checkboxes to remove a product from all categories then the update fails to remove any categories the product was in. This is because a checkbox on an HTML form will not have its value sent back if it is unchecked and therefore no category_ids
will appear in the product’s parameters hash, meaning that the category_ids
are not updated.
To fix this we have to modify our products controller to set the category_ids
parameter to be an empty array if it is not passed to the update action. We can do this using Ruby’s ||=
operator and add the following like at the top of the update action.
params[:product][:category_ids] ||= []
This will ensure that if none of the checkboxes are checked then the product is updated correctly so that it is in no categories.