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:
<%= 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})%>
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_tag
is 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 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.
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.
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!
Great little article! Helped me out, thanks!
Hi folks!
I got the following error:
You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.[]
app/controllers/restaurants_controller.rb:44:in `create'
Parameters:
{"restaurant"=>{"name"=>"Restaurant do Leandro"},
"commit"=>"Create",
"authenticity_token"=>"bdad0a7ac7b929c1200fc2e48f997f5e86eb87a1",
"category_ids"=>["1"]}
someone could help me?
Nice article. There is another way for multiselect (for has_many and habtm relationships) though – showing two select boxes, one for all available options and one for the selected options, and the opportunity to move options from one select box to the other. I call this SwapSelect and is described (incl. download) here: http://trendwork.kmf.de/175
I know, it’s a 1y old post. Anyway i’m sure a lot of visitors drop in this article so…
@animeword
In your create and update method change “@params” in “params” (no @ is needed)
Hi, you can change @restaurant.categories.collect { |cat| cat.id}) into @restaurant.category_ids. Also, I think it’s better to assign this values to variables in controller. :)
Anyway thanks for this post.
I just wanted to take a moment and let you know that I’ve been savouring reading your posts over the last few weeks. I have a website of my own, and would enjoy to switch links with you. If you’re interested just leave me a comment on my page or send me an e-mail with your details.
Works like charm! Thanks a lot … !
Hi,
You have:
@restaurant.categories = Category.find(@params[:category_ids]) if @params[:category_ids]
Instead I think it should be:
@restaurant.category_ids = Category.find(@params[:category_ids]) if @params[:category_ids]
Above, I have changed @restaurant.categories to @restaurant.category_ids
I am a newbie to rails and ruby both, so please pardon me if i am making an obvious mistake.
thx
Thanks Cory,
Great post , even the above technique worked for me after some small changes on rails 3.1.
Very nice to share your ideas and knowledge to others :)
Well done Cory :)
Thank you very much for this, I searched for this kind of form very long and was really desperate because nothing worked! :)
I want to say thanks, you ended a 4 hour search with your one line solution :)