Implementing HABTM Checkboxes
So I have a relatively simple requirement between two tables with a many-to-many relationship. In Rails parlance, this is a typical Has And Belongs To Many (HABTM) case. Unfortunately, my development environment is physically separate and isolated such that I cannot simply put my example here – but I will transcribe and “sanitize” it in such a way that it is code equivalent. I’m posting what finally worked to see what I should have done. This example was coded under Ruby 1.9.1 with Rails 2.3.8.
The basic premise is that I have a table cars
and a table car_features
that are related via the car_features_cars
join table. In separate cases, I want to be able to populate the cars
table and car_features
table in order to maintain good control of normalization. By that I mean I can add “supercharger” and “turbocharger” to the car_features
table once and then see those options appear as available checkboxes when I’m editing the cars
table.
To begin this venture, I turned to the Internet and found several examples that all failed me but served to get me started.1,2,3 The table definitions were okay:
class CreateCars < ActiveRecord::Migration
def self.up
create_table :cars do |t|
t.string :make
t.string :model
end
end
def self.down
drop_table :cars
end
end
class CreateCarFeatures < ActiveRecord::Migration
def self.up
create_table :car_features do |t|
t.string :feature
end
end
def self.down
drop_table :car_features
end
end
class CreateCarFeaturesCars < ActiveRecord::Migration
def self.up
create_table :car_features_cars, :id => false do |t|
t.integer :car_id, :null => false
t.integer :car_feature_id, :null => false
end
end
def self.down
drop_table :car_features_cars
end
end
I then needed to establish the relationships with two separate models.
class Car < ActiveRecord::Base
has_and_belongs_to_many :car_features
end
class CarFeatures < ActiveRecord::Base
has_and_belongs_to_many :cars
end
From here forward, I had to do things differently than the provided examples. I was going to need a helper function to see if the joined items were already associated or not. The example used a method called include?
which always returned false for me. Instead, I wrote this:
module CarsHelper
def feature_present?(car_feature)
!@car.car_features.find_by_id(car_feature).nil?
end
end
The provided examples never did anything to explicitly add newly checked items. They simply used the update_attributes
method in the controller’s update action and talked about how wonderfully magic Rails was for doing everything for them. Rails didn’t do anything for me. So I added another function to the Cars model.
class Car < ActiveRecord::Base
has_and_belongs_to_many :car_features
def add_features(car_feature)
car_features << car_feature
end
end
Like I mentioned before, the examples just used update_attributes
within their update
action. This didn’t work for me so my update
action (in the cars controller) was written differently. First, I clear out the existing relationships because I found that my add_feature
method would just keep on adding more of the same entries. Second, I loop through the features that were checked and returned in the :car_feature_ids
hash and add the relationship. Finally, I do end up calling the old update_attributes
method for updating any changes to the actual entry in the cars table.
def update
@car = Car.find(params[:id])
@car.car_features.clear;
for feature in params[:car_feature_ids]
@car.add_feature(CarFeature.find_by_id(feature))
end
@car.update_attributes(params[:car])
redirect_to(@car)
end
Lastly, my view differs significantly from the examples. Converting the example’s fields over to my own produced the following which gave me all kinds of errors.
<% @car_features.each do |car_feature| -%>
<%= check_box_tag "car[car_feature_ids][]", car_feature.id, feature_present?(car_feature) -%>
<%=h car_feature.feature -%>
<% end -%>
I ended up having add another element variable to the edit
action in the car
controller to retrieve all the possible car features. Then I was able to use the modified code to create the checkboxes in the edit
view.
def edit
@car = Car.find(params[:id])
@car_features = CarFeatures.find(:all, :order => "feature")
end
<%- @car_features.each do |feature| %>
<%= check_box_tag "[car_feature_ids][]", feature.id, feature_present?(feature.id) -%>
<%=h car_feature.feature -%>
<%- end %>
In the end, this worked correctly for me. When the specific car was shown for editing, all of the possible features would be listed with the appropriate checkboxes pre-populated. When I clicked the UPDATE
button any changes I had made to the car’s fields were updated and the checked boxes were saved in the join table.
None of the examples I’d seen on-line had to do what I did. I assume that my solution is in some way an unholy abomination to the Rails way. Please let me know where this version strays and what should have been done.
1http://www.justinball.com/2008/07/03/checkbox-list-in-ruby-on-rails-using-habtm/
2http://satishonrails.wordpress.com/2007/06/29/multiple-checkboxes-with-habtm/
3http://asciicasts.com/episodes/17-habtm-checkboxes
To address some feedback I've received over time on this article:
- I was told the
include?
instance method should have worked. Yep, I wholeheartedly agree. It should have. But it didn't. Whenever the output was dumped to the console it always returned FALSE. - The
update_attributes
should have simply done the task. Once again Rails gave me the shaft here. No matter how many times it was written the way the examples did it, the checked checkmark values were never appearing in that hash sent to theupdate
action. I only saw them when I changed it which caused me to have a second hash which in turn caused me to have to save them manually.