Checkbox list in Ruby on Rails using HABTM

by Justin Ball on July 3rd, 2008

Checkboxes are one of those things that look easy and should be easy, but they aren't always easy. I needed a solution that could create a checkbox list of languages that a user speaks. So I don't forget here's how to do it:

The migrations are important. You have to be sure to exclude the id parameter when you create languages_users or you will get ' Mysql::Error: #23000Duplicate entry' due to the fact that ActiveRecord will try to store a value in the id field that indicates which model created the entry (User.languages << vs Langauges.users). The other option is the create the id parameter so that the direction is maintained but be sure that it is not created as a primary key.

 
class LanguagesUsers < ActiveRecord::Migration
    def self.up
        create_table :languages_users, :id => false, :force => true do |t|
            t.integer :user_id
            t.integer :language_id
            t.timestamps
        end
    end
 
    def self.down
        drop_table :languages_users
    end
end
 
 
class Languages < ActiveRecord::Migration
 
    def self.up
        create_table "languages", :force => true do |t|
            t.string  "name"
            t.string  "english_name"
            t.integer "is_default", :default => 0
        end
    end
 
    def self.down
        drop_table "languages"
        drop_table "users_languages"
    end
end
 
 
class Users < ActiveRecord::Migration
 
    def self.up
        create_table "users", :force => true do |t|
            t.string  "login"
            # other fields excluded for brevity
        end
    end
 
    def self.down
        drop_table "users"
    end
end
 

Here are my models:
user.rb

 
class User < ActiveRecord::Base
    has_and_belongs_to_many :languages
end
 

language.rb:

 
class Language < ActiveRecord::Base
  has_and_belongs_to_many :users
end
 

In my user_controller.rb the create and update methods are simple. This is thanks to the fact that you get a language_ids method on the user object because of the HABTM relationship.

 
    def create
        @user = User.new(params[:user])
        @user.save
    end
 
    def update
        params[:user][:language_ids] ||= []
 
        @user = User.find(current_user)
 
        if @user.update_attributes params[:user]
            flash[:notice] = "Settings have been saved."
            redirect_to edit_user_url(@user)
        else
            flash.now[:error] = @user.errors
            setup_form_values
            respond_to do |format|
                format.html { render :action => :edit}
            end
        end
 
    end
 

On to the view:

 
<ul class="checkbox-list">
  <% @languages.each do |language| -%>
<li><%= check_box_tag "user[language_ids][]", language.id, user_speaks_language?(language) -%> <%= language.english_name -%></li>
 
  <% end -%>
</ul>
 

NOTE: I had an error in my original method. This code:

 
<li><%= f.check_box :language_ids, {:checked => user_speaks_language?(language)}, "#{language.id}", ""  -%> <%= "#{language.english_name}" -%></li>
 

should be this:

 
<li><%= check_box_tag "user[language_ids][]", language.id, user_speaks_language?(language) -%> <%= language.english_name -%></li>
 

And we'll need this helper method:

 
def user_speaks_language?(language)
    if @user && !@user.login.nil? # no sense in testing new users that have no languages
        @user.languages.include?(language)
    else
        false
    end
end
 

The result is that you will get a list of check boxes that update values in the join table that is part of the has_and_belongs_to_many relationship. Rails is very cool

  • Vermin
    It's not working for me. Everytime i try to update only the first checkbox is added. Seems like my category_ids not act as an array even if I have the HABTM relationship.

    If I look at my request, all the values is sent to the server but only the first one is added.

    Any ideas what to do?
  • Check your Rails version. I am on 2.1. Not sure if that makes a difference or not.
  • Frank
    @Vermin - did you discover an answer to your problem? I have exactly the same thing. I am running Rails 2.1.0.
  • Vermin
    I also running 2.1.0 and haven't found a solution yet. Sounds strange that it works for you but not for us.
  • Frank
    I found another implementation that worked like a champ for me: http://media.railscasts.com/videos/017_habtm_ch...

    Unfortunately it is video, but it is short and more importantly, it worked for me. Slightly different approach but nothing radical.
  • Thanks for the link. I haven't had time to look into the issue so I am glad that you found a technique that works.
  • There was an error in my code. Thanks to Frank for pointing out the link with the proper method.
  • Great article, but what if you wanted to take it a step further?

    What if you wanted to record something like how well each user speaks each language?

    I believe you would need another model that joins languages with users via their ids and then has another field called how_well (for example).

    But then what? Do you use a :through to instead of the HABTM? How could you make your example work so that when a user is added you select all the languages they speak and then select how well they speak each language?

    thanks!

    josh
  • You, my friend, are a life saver. Here's how to add clickable labels:

    <li>
    <% field_id = "language_#{language.id}" %>
    <%= check_box_tag "user[language_ids][]", language.id, user_speaks_language?(language), {:id => field_id} -%>
    <%= label_tag field_id, language.english_name -%>
    </li>
  • jbasdf
    Glad I could help and thanks for the clickable labels code.
  • Your blog post got me to thinking. That controller code looks a little too easy. Does this mean that any time two models are connected and you use update_attributes, someone else can control the relationships by messing with the form? I just found out, the answer is yes. This is a *huge* vulnerability. Fortunately, someone pointed me at a blog explaining the issue: http://railspikes.com/2008/9/22/is-your-rails-a....
  • jbasdf
    Thanks for the link. I haven't looked at this code in a while and having another pair of eyes find a potentially huge problem is very helpful.
  • mkrx
    Well... I see the code, I put it into my code, I run it, it works... freakin' magic! I don't see how it creates and deletes the join records. Once again... amazing!

    How would you set a 'third field' in the join record? We are doing sign-ups for races, and I've taken your example and created a race_tshirts join table. I want to add a 'price_adder' field in the join table for larger or special tshirts. Any suggestions would be very helpful.

    Mark
  • jbasdf
    Be sure to checkout the link above (http://railspikes.com/2008/9/22/is-your-rails-a...) so that you don't have security problems. I'm not sure that there is an easy way to add items to the join table using this method. You might take a look at this http://guides.rubyonrails.org/2_3_release_notes... to see if this will help. It would require a bit of architecture change, but using nested attributes might give you more control.
  • Yes, you *can* add rows to the join table using the vulnerability you linked to. I talked about it at length, including the solution I settled on, here: http://jjinux.blogspot.com/2009/07/rails-config...
  • mkrx
    JJ, thanks for that. You've opened up a new world on the security front. I'll have to study it before I can implement it.

    Still, I need a way with the great code that you and Justin have supplied to update a 'third' field in the join record. What I have is a join record that does a HATBM with 'races' and 'tshirts'. Our clients want to be able to assign different tshirt sizes from a pick list (which I've put into the DB) and possibly have a 'price adder' for large or special shirts.

    With your code above is there an easy way to pass in an array of 'price adder' fields (most of which will be zero) and then pass them into the join record in the controller on create or update?

    Would it be something like...

    params[:race][:tshirt_ids].merge(:price_addr => params[:price_addr][]) ||= []

    ...? I'm assuming I would create an array somehow in the view of price_addr[], similar to tshirt_ids (over my head here). The code above won't work, because I'm mixing type String with Array in the merge(), but I'm guessing if it can be done its a short bit of code.

    Thanks,
    Mark
  • I think what you're looking for is has_many :through.
  • akellakarthik
    i am new to ROR. i am trying to create a checkbox list by values which i retrive from database table. How can i acheive this?
  • Grab the records. In your template, use a loop ;)
  • arun
    Thanks for the awesome post
blog comments powered by Disqus