Pneumatic Cylinders

Matt Jankowski

Let’s assume a basic has_many :through situation like this…

class Paper < ActiveRecord::Base
  has_many :categorizations, :dependent => :destroy
  has_many :categories, :through => :categorizations
end
class Category < ActiveRecord::Base
  has_many :categorizations, :dependent => :destroy
  has_many :papers, :through => :categorizations
end
class Categorization < ActiveRecord::Base
  belongs_to :category
  belongs_to :paper
end

…ever since rails 2.something there’s been a setter method that you get “for free” when you declare relationships like this (it used to only work on a normal HABTM, but now also works on HMT associations). In this case, you will get a #category_ids= method on Paper, which takes an array of id values for Category records, and updates the categorizations between the two to reflect what’s sent in. Works like this (I’ve stripped out some of the SQL related to validations) …

>> Paper.first.categories.size
# SELECT count(*) AS count_all
# FROM `categories`
# INNER JOIN categorizations ON categories.id = categorizations.category_id
# WHERE ((`categorizations`.paper_id = 4))
=> 0
>> Paper.first.category_ids = [Category.first.id, Category.last.id]
# INSERT INTO `categorizations` (`category_id`, `paper_id`) VALUES(1, 4)
# INSERT INTO `categorizations` (`category_id`, `paper_id`) VALUES(79, 4)
=> [1, 79]
>> Paper.first.categories.size
# SELECT count(*) AS count_all
# FROM `categories`
# INNER JOIN categorizations ON categories.id = categorizations.category_id
# WHERE ((`categorizations`.paper_id = 4))
=> 2

This is great for a scenario where you have a view that lists a collection of checkboxes in a form for a user to select which categories should be associated with a paper, you might have markup like this…

<% @categories.each do |category| -%>

<%= check_box_tag 'paper[category_ids][]',
    category.id,
    paper.categories.include?(category),
    :id => "category_#{category.id}" %>

<label for="category_<%= category.id %>"><%=h category.name %></label>
<% end -%>

''

…in this case, when a user selects categories from the resulting set of checkboxes in the view, there will be a params[:category_ids] array present, which will in turn call the #category_ids= setter on the Paper record when we call #update_attributes and #save in the #update action in the controller.

Now, what happens if a user unchecks ALL of the checkboxes, but the Paper record previously had categories associated with it? The user is clearly indicating that all categorization records connecting the Paper in question to it’s current Category associations should be destroyed. However, because of the way HTML works and browsers submit forms, there will NOT be any value sent in for params[:category_ids], thus the #category_ids= setter will never be called, and so the previous associations will stick around even though they should have been deleted.

One solution to this is to update your view to include one more line…

<%= hidden_field_tag 'paper[category_ids][]', nil %>
<% @categories.each do |category| -%>

<%= check_box_tag 'paper[category_ids][]',
    category.id,
    paper.categories.include?(category),
    :id => "category_#{category.id}" %>

<label for="category_<%= category.id %>"><%=h category.name %></label>
<% end -%>

This will ensure that there is always a category_ids Array in your params, and will have the effect of setting the association to nil (empty collection in this case) if all the boxes are unchecked.

You could probably argue that this should be in a beforefilter which massages the params on the #update action, and I wouldn’t stop you from doing that—but I’m comfortable with this being in the view. I look at the params that result from the view as the most direct method which captures the intent of the user and communicates it to the controller in the HTTP request – and it’s also in line with what Rails already does with normal checkbox tags. From that perspective, it’s fine to have the view generate an empty categoryids array by default, when the user does not want to save any categories with a Paper, and this technique accomplishes that.

Also, check out this GAS POWERED blender