17: HABTM Checkboxes 

(view original Railscast)

Other translations: It Es Fr

The edit form with the checkboxes added.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) %>
      <%= check_box_tag "product[category_ids][]", category.id, @product.categories.include?(category) %>
      <%= category.name %>
<% 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.

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 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.