VnutZ Domain
Copyright © 1996 - 2017 [Matthew Vea] - All Rights Reserved

2010-08-23
Featured Article

Implementing HABTM Checkboxes

[index] [1,130 page views]

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

More VnutZ.com Content You Might Be Interested In Reading:

It sure is handy having a portable video player ... but getting video you already own onto it without buying it again can be a hassle.

Or try your hand at fate - use the Pattern Analysis of the MegaMillions Lottery or the Pattern Analysis of the PowerBall Lottery page to pick "smarter" numbers. Remember, you don't have to win the jackpot to win money from the lottery!

coinbase