Doing Checkbox Adds with HABTM

For some of you this may be a “what?” kind of post. But it’s something that other people playing with the Ruby on Rails framework might be able to use.

I’ve spent a while developing a production pipeline for Meticulous and I am thankful that I am not trying to build this beast in PHP. But Rails is relatively young (and rapidly changing) and that limits the amount of existing info for the newbies in the crowd. So doing an insert to a join table isn’t exactly something they run through in Agile Development with Rails.

So let’s say you’ve been making an app and you have a has_and_belongs_to_many relationship between two models. For the sake of this exercize, we’re going to use the Manuals model that we’re creating in the Koji Production Pipeline. Manuals would be user manuals. Various manuals generally belong to one department, however, there’s a lot of hardware and software that is shared between departments. We want to be able to show, on the department page, any manuals that belong to it. Simple enough, right?

Your schema/migration would look something like this:

  create_table "departments_manuals", :id => false do |t|
    t.column "manual_id", :integer, :limit => 10, :default => 0, :null => false
    t.column "department_id", :integer, :limit => 10, :default => 0, :null => false

and then your models…

class Manual < ActiveRecord::Base
has_and_belongs_to_many :departments

class Department < ActiveRecord::Base
has_and_belongs_to_many :manuals

Once this is setup, it’s pretty much view hacking to get this to play nice. In your form partial for manuals (which is used in new and edit for scaffolded code), you basically get something setup that will return a list of departments and an applicable check box.

<p>What Departments should this appear in?</p>
<% Department.find(:all, :order => "name ASC").each do |d|  %>
   <li><%= check_box_tag 'manual[department_ids][]',, @manual.departments.include?(d) %>
<%= %></li>
<% end %>

The check_box_tag is a sexy little piece of code (full details in the api) that basically creates a check box with all the trappings. You can call a :checked => 1 is you want in the form tag helper and it will auto check the box, but then you’d be assuming that all the boxes in the array were checked.

<li><input id="manual[department_ids][]" name="manual[department_ids][]" type="checkbox" value="5" />Animation</li>

etc. etc.

So basiclaly, the code finds all the departments, orders them by the :order conditional statement (in this case by name in ascending order) and then loops through each one, echoing a list item with a checkbox followed by the department name.

Simple. Sexy. Took forever to figure out the first time (and a lot of questions in the #ror channel). Anyways. Hopefully that can help someone else out when they run into a similar issue.

Back to the code!

  1. Paul Goscicki says:

    This has been very helpful!

  2. Randy says:

    I'm working with Rights/roles for authentication. I loop through all rights, printing out name/controller/action. Before the loop ends, I do another loop using your code to grab the checkbox status for 7 roles. I put the _form.rhtml in my admin folder since I'm currently in the admin index method (didn't know how to call a partial for another controller). It works great! I now get about 20 rows of rights with 7 columns on the right that show the correct state of roles assigned to each right.

    I have one little weirdness though. On that same layout page, I can create a new right, but it takes the last right's row of role checkboxes as input, setting the exact roles that the last right had! Also, In the browser source code, every checkbox has the same name, same id, but dif values from 1~7? If I somehow add checkboxes to the 'new' form, will that take care of it defaulting to what the last row's roles were?

    Lastly, (given that I am in the admin controller#index looping through rights, then roles...) how would I go about updating a checkbox's status in the database? ( the join table) I want to uncheck/check a box and have an "onChange" event fire off a record update in the database. This would match the conenience of the rest of the rights form that uses in_place_editing_field to update any column in any row without leaving the overall form.

  3. John Athayde says:

    Randy -

    I know this code might be getting stale with newer versions of rails, but I haven't yet updated the pipeline code that runs this. Refactoring time for me it seems :)

    In regards to your loop, so if the first six are a value of 0 and the last one is a value of 1, on submit makes them all 1 in the boolean? It seems there's some weirdness and bugginess in my code in regards to how it's playing with ActiveRecord.

    There is a more extensive post now on the WIKI": that seems to have been updated quite recently. It looks like Stephen Caudill also has posted a helper for checkbox habtm that he created in August 06.

  4. Ben says:

    It appears necessary to also have an input type="hidden" name="manual[department_ids][]" value="" tag somewhere if you want to be able to allow the user to select nothing in your list. Otherwise when updating the object, if they uncheck every previously selected item, nothing in the habtm is updated because no manual[department_ids][] values are in params[].

  5. John Athayde says:

    Thanks Ben. Textile broke up your code, if you surrond it with @ it renders it.

    @input type="hidden" name="manual[department_ids][]" value=""@

    is what I think you had submitted.

  6. GustafB says:

    Thank you jathayde and Ben.

    Without the hidden input it's not possible to select "nothing". However, when I add it I get the error

    Couldn't find all Departents with IDs ('1','')

    ActiveRecord tries to find the Department with id ''.

  7. Myguell says:

    vrai et faux, car il y a deux possibilite9s pour ge9ne9rer une check-box en Rails :- check_box a le cmmoortepent indique9 pour pouvoir eatre facilement inte9gre9 dans le mode8le Active Record ;- check_box_tag qui se comporte comme la spe9cification HTML = seule la valeur coche9e est renvoye9e.

Post a comment

Name or OpenID (required)

(lesstile enabled - surround code blocks with ---)