Multiselect control in Rails with a Many to Many relationship
This evening I had the chance to dive back into some Rails stuff. I came across a problem of which there was surprisingly no straightforward documentation from start to finish, so I wanted to put it here.
Let's say you have 3 tables - restaurants (id, name), categories (id, name), and restaurants_categories (restaurant_id, category_id). You want to let people who are entering or editing a restaurant select multiple categories the restaurant will belong to. Straightforward, right?
Actually, somewhat. There are a couple of good articles which get us close. The key is setting up the relationships, and combining select_tag with options_for_select.
First, after you've generated your models and controllers for restaurant and category, open up your model for Restaurant and add has_and_belongs_to_many :categories. This is the magic to let Rails know about the relationship.
Next, go into your restaurants_controller and in your create and update methods, just after you new up the object, add @restaurant.categories = Category.find(@params[:category_ids]) if @params[:category_ids]. category_ids is what our multiselect options box will put the selected items into.
Up till now, all of this is in the jrhicks.net blog. However, to implement the categories, he spits out HTML. There's a better way - using select_tag with options_for_select. Open up your restaurants\_form.rhtml file, and add the following:
<p><label for="restaurant_categories">Categories</label><br/>
<%= select_tag("category_ids[]", options_for_select(Category.find(:all).collect { |cat| [cat.name, cat.id] }, @restaurant.categories.collect { |cat| cat.id}), {:multiple=>true, :size=>6})%></p>
Whew! Let's break this down:
label- This is just the title of the select list.select_tag- this follows the format (object, options, html_options)- "category_ids[]" - the variable that we'll populate when the form is submitted. It has to end with "[]" to let Rails know we are populating into an array
options_for_select- This is a helper which spits out the optionsselect_tagis looking for. It has two parameters - an array of objects to use for the options list, and an array of item ids that should be selected{:multiple=>true,:size=>6}- This sets our select list to allow multiple selections, and show 6 entries at a time
Going back to the options_for_select - Notice we do two things:
Category.find(:all).collect { |cat| [cat.name, cat.id] }
This loops through all of the categories and builds an array with their names and ids. This will be turned into <option value="#{cat.id}">#{cat.name}</option> basically.
Second, we do:
@restaurant.categories.collect { |cat| cat.id }
which loops through all of the categories we have set in the restaurant, passing an array of their ids as the second parameter.
So effectively, this displays the multiselect list, and automatically selects the appropriate options if any have been selected.




3 Comments:
Cory,
It's usually considered bad practice to data access calls from the view. It's much, much harder to document the data access through TDD specifications since some of the data access is in the view, and testing views requires a different kind of testing.
By
Scott Bellware, at 1:08 AM
Thanks Scott. The way to get around that would simply be to populate an @categories variable and an @selected_categories variable in the control that the view would use.
Thanks for the reminder!
By
Cory Foy, at 8:53 AM
Great little article! Helped me out, thanks!
By
Eric, at 9:56 AM
Post a Comment
Links to this post:
Create a Link
<< Home