I have seen this asked a lot in the forums, so I thought I would write up a little tutorial.
For this tutorial I am going to have three select boxes. The first select box will be a super category of the next two select boxes and the second select box will be a super category of the third select box. I hope that makes sense. To demonstrate, I thought I would use Genre -> Artist -> Song. So let's get started:
Create your models and build your migrations:
1
2
3
4
|
ruby script/generate model genre name:string
ruby script/generate model artist name:string genre_id:integer
ruby script/generate model song title:string artist_id:integer
|
Populate your genres, artists and songs through a migration:
1
2
|
ruby script/generate migration create_hierarchy
|
Contents of migration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
class CreateHierarchy < ActiveRecord::Migration
def self.up
g1 = Genre.create(:name => "Genre 1")
g2 = Genre.create(:name => "Genre 2")
g3 = Genre.create(:name => "Genre 3")
a1 = Artist.create(:name => "Artist 1", :genre_id => g1.id)
a2 = Artist.create(:name => "Artist 2", :genre_id => g1.id)
a3 = Artist.create(:name => "Artist 3", :genre_id => g2.id)
a4 = Artist.create(:name => "Artist 4", :genre_id => g2.id)
a5 = Artist.create(:name => "Artist 5", :genre_id => g3.id)
a6 = Artist.create(:name => "Artist 6", :genre_id => g3.id)
Song.create(:title => "Song 1", :artist_id => a1.id)
Song.create(:title => "Song 2", :artist_id => a1.id)
Song.create(:title => "Song 3", :artist_id => a2.id)
Song.create(:title => "Song 4", :artist_id => a2.id)
Song.create(:title => "Song 5", :artist_id => a3.id)
Song.create(:title => "Song 6", :artist_id => a3.id)
Song.create(:title => "Song 7", :artist_id => a4.id)
Song.create(:title => "Song 8", :artist_id => a4.id)
Song.create(:title => "Song 9", :artist_id => a5.id)
Song.create(:title => "Song 10", :artist_id => a5.id)
Song.create(:title => "Song 11", :artist_id => a6.id)
Song.create(:title => "Song 12", :artist_id => a6.id)
end
def self.down
# you can fill this in if you want.
end
end
|
Yes, I know it is generic data, sorry. So anyway, as you can see there are 3 genres, each with 2 artists, for a total of 6 artists each with 2 songs for a total of 12 songs. Each genre has 4 songs through its artists. Ok, so now we need to populate the database:
Now we need to modify our models to set up the associations.
1
2
3
4
5
6
7
8
|
class Genre < ActiveRecord::Base
has_many :artists
has_many :songs, :through => :artists
end
class Artist < ActiveRecord::Base
has_many :songs
end
|
That should be it, now let's go to the console and see that all this works:
1
2
3
4
5
6
7
8
9
10
|
ruby script/console
Loading development environment (Rails 2.0.2)
>> g = Genre.find(:first)
=> #<Genre id: 1, name: "Genre 1", created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">
>> g.artists
=> [#<Artist id: 1, name: "Artist 1", genre_id: 1, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">, #<Artist id: 2, name: "Artist 2", genre_id: 1, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">]
>> g.songs
=> [#<Song id: 1, title: "Song 1", artist_id: 1, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">, #<Song id: 2, title: "Song 2", artist_id: 1, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">, #<Song id: 3, title: "Song 3", artist_id: 2, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">, #<Song id: 4, title: "Song 4", artist_id: 2, created_at: "2008-03-30 11:52:25", updated_at: "2008-03-30 11:52:25">]
>>
|
Looks like it works :) Ok, now on to the controller. Our controller needs to have some action for the view, I just used index, and two other actions, for remote function calls, I called these update_artists and update_songs. update_artists() is called when a genre is changed and it updates the list of artists and the list of songs based on the genre. update_songs() only updates the songs based on the artist. So let's look at this code:
# the controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
class TestItController < ApplicationController
def index
@genres = Genre.find(:all)
@artists = Artist.find(:all)
@songs = Song.find(:all)
end
def update_artists
# updates artists and songs based on genre selected
genre = Genre.find(params[:genre_id])
artists = genre.artists
songs = genre.songs
render :update do |page|
page.replace_html 'artists', :partial => 'artists', :object => artists
page.replace_html 'songs', :partial => 'songs', :object => songs
end
end
def update_songs
# updates songs based on artist selected
artist = Artist.find(params[:artist_id])
songs = artist.songs
render :update do |page|
page.replace_html 'songs', :partial => 'songs', :object => songs
end
end
end
|
Now as far as views go we have one view (index.html.erb) and two partials (_songs and _artists). Let's take a look at those:
# the _songs partial (_songs.html.erb):
1
2
3
|
<%= collection_select(nil, :song_id, songs, :id, :title,
{:prompt => "Select a Song"}) %>
|
# the _artists partial (_artists.html.erb):
1
2
3
4
5
6
|
<%= collection_select(nil, :artist_id, artists, :id, :name,
{:prompt => "Select an Artist"},
{:onchange => "#{remote_function(:url => {:action => "update_songs"},
:with => "'artist_id='+value")}"}) %>
<br/>
|
# and last, but not least, the index view (index.html.erb):
1
2
3
4
5
6
7
8
9
|
<%= javascript_include_tag :defaults %>
<%= collection_select(nil, :genre_id, @genres, :id, :name,
{:prompt => "Select a Genre"},
{:onchange => "#{remote_function(:url => {:action => "update_artists"},
:with => "'genre_id='+value")}"}) %>
<br/>
<div id="artists"><%= render :partial => 'artists', :object => @artists %></div>
<div id="songs"><%= render :partial => 'songs', :object => @songs %></div>
|
Ok, this probably takes some explanation. I will save that for part II, where I will also improve upon what we have so far and include a demo.
For now, if you have any questions, just ask me in the comments.
March 31st, 2008 at 06:02 PM Nice tutorial, I will be doing it, soon.
Just a nice consideration does the render_html works in ie ?
March 31st, 2008 at 09:02 PM
@Adriano - do you mean the replace_html rjs line? IAE, it should work in IE, let me know if you run into any problems.
April 1st, 2008 at 05:39 AM I'm kind a newbie in rails, shouldnt the controller be the app controller ?
I created a folder named test in it i added files index.rhtml and the 2 partials in this folder and modified the app controller, am I doing something wrong ?
I used to just use script generate/scaffold so I dont know which error it is. Thanks in advance ....
April 1st, 2008 at 06:00 AM Thanks for the tutorial! all the previous that I saw when using IE it would not work with replace html. This is the first fully working, fully explained tutorial about cascading select I ever saw (and i searched over 40). Congrats, and thanks for making me unstuck in this problem =D
April 1st, 2008 at 08:26 AM
@Adriano - glad it worked for you and thanks for the nice comments
@Jonathan - sure you could use the app controller, if you mean app/controllers/app_controller.rb, and not app/controllers/application.rb. I just like using test_it for my controller for some reason.
Here is a very basic guide:
(assuming you did the database and models)
1) ruby script/generate controller <whatever>
2) copy the code (excluding the class (top) and end(bottom)) from what I have in my controller code to your app/controllers/<whatever_you_called_it>.rb
3) copy the code from index.html.erb to app/views/<whatever>/index.html.erb
4) do step 3 for both partials
5) start your server and go to http(s)://<your>/<whatever>
That ought to get you close at least.
April 10th, 2008 at 06:12 PM this is truly great. thanks for your simple, clean tutorial.
April 23rd, 2008 at 10:02 AM I can't get this to work. Using the debugger, execution never gets to the update_artists function. Any advice?
Thanks
April 23rd, 2008 at 10:29 AM Got it to work. Forgot a line of code.
Works great!
April 23rd, 2008 at 12:02 PM Any idea on how to get this to work with new or edit forms? I get null values saved in the genre_id and artist_id fields whenever I try to create a new song or edit a song. Thanks
April 23rd, 2008 at 01:24 PM
@cworth -
When you create a new song, you only have to associate the artist_id. When you submit a new song, what params do you have as a result?
April 25th, 2008 at 04:12 AM This is a clear and straight forward example. Thanks!
July 2nd, 2008 at 10:16 PM select(personal) locality. So to out!
July 15th, 2008 at 12:35 AM I can't get this to work. Execution never gets to the update_artists function. ???
July 15th, 2008 at 01:12 AM Thanks for the article, it was really clear and easy to follow.
Is there any way that the artist drop-down is only populated once a genre has been selected, and the song drop-down is only populated once the artist has been selected? Ideally the unpopulated drop-downs would be disabled until they're populated.
July 15th, 2008 at 02:50 AM i cant able to select artist values as well as song value in dropdown....any suggestions pls..
July 16th, 2008 at 08:40 AM @Andy - Just remove this line from update_artists:
page.replace_html 'songs', :partial => 'songs', :object => songs
You can start with the drop-downs disabled then in the render :update, you can use rjs to enable the appropriate one
@ganez - Did you populate your database? I will probably need more information.
August 4th, 2008 at 02:13 PM Cool tutorial. How would u change this so that an Artist could be in two different Genre's. Say you wanted to be able to find the one entry for Shakira in both the "Spanish" Genre and the "Pop" Genre. Thanks!
August 4th, 2008 at 11:13 PM
@Geoff - you would need a join table between genres and artists, such that artist.genres is possible. Then a few other changes ... I just created an example for you - http://pullmonkey.com/2008/8/5/dynamic-select-boxes-many-to-many-ruby-on-rails
August 13th, 2008 at 12:54 AM Hi
its is very nice example u have explain here and i implemneted also its working fine. I modified the application little like i added one button and when i click on this button i need the value selected like the genre id, artist Name and song name and display this value in controller action for other operation but i unable to get i tried a lot
can u help ...........
Thanks in Advance
Harish
August 13th, 2008 at 07:24 AM
@Harish - You can just wrap the whole thing in a form and add a submit button. Then you use params inside your action to get your values.
August 21st, 2008 at 10:34 PM Thanks for this great tutorial.
I implement your method to my application.
Everything works all right in Firefox and Opera.
However, I cannot get it working in IE7.
The only difference between mine and yours is the the render target tag. You used . And I used
But I do not think this is the reason why it cannot work in IE7.
Do you have some ideas?
August 22nd, 2008 at 08:19 AM
@boblu - seems that some of your comment did not register. Use this information to post html/ruby code in your comment - http://pullmonkey.com/2008/7/23/open-flash-chart-ii-plugin-for-ruby-on-rails-ofc2#comment-46303
September 10th, 2008 at 10:53 PM I am having a problem translating this box into a create / edit form_for. I've simplified it and adapted to my application. So I have sections, each section has many categories. When I create an article, I use one form_for that renders a partial for the form. Within that partial, I render another partial for the categories depending on which section is chosen (like the genre / artist relationship). When I submit the form, there is not a parameter for the category_id in the param list.
My form code is below
new.html.erb
<filter:code>
<% form_for :post, :url => {:action => 'create'}, :html => { :multipart => true } do |f| %>
<%= render :partial => 'form', :locals => { :f => f } %>
<%= submit_tag ("Create", :action => 'create', :style => "margin: 1.5em 0 0 100px;") %>
<% end %>
< /filter:code>
_form partial
<filter:code>
<th>Title</th>
<th>Section</th>
{:prompt => "Select a section"},
{:onchange => "#{remote_function(:url => {:action => "update_categories"},
:with => "'section_id='+value")}"}) %>
<th>Category</th>
< /filter:code>
_categories_list partial
<filter:code>
<%= collection_select(nil, :category_id, @categories, :id, :name,
{:prompt => "Select a category"}) %>
< /filter:code>
September 11th, 2008 at 11:20 AM
@Drew - you left the space in there < /filter:code> :)
Anyway, post the html page source, so I can look at what this code is generating.
September 19th, 2008 at 05:25 AM Thanks for an awesome tutorial! :) Simple and effective. I am trying to do this for a nested resource Company which is nested under Users. Once the Company is selected, a Contacts drop down is to be populated. And my update_contacts method is in the Companies Controller. The generated path for this is update_contacts_company_path and i need to pass the session[:user_id] along with this. I dont know how to construct the URL for this. The company_id is getting sent in the :with param whereas the url can be generated only if it sent alongwith the path like this - update_contacts_company_path(session[:user_id], company_id). Can someone help me out with fixing this?
September 19th, 2008 at 09:41 AM
@Vinay - Thanks. I can't really tell what you are asking for without an example. Are you talking about this line:
And you want to use a nested named route and get the session[:user_id] passed in as user_id, try this:
{:onchange => "#{remote_function(:url => update_contacts_company_path,:with => "'genre_id='+value&user_id=#{session[:user_id]}}")}"}) %>
Let me know.
September 19th, 2008 at 09:45 AM I messed up the quotes on the :with string:
September 19th, 2008 at 10:09 AM Thanks Charlie! I cant try this until tonight though. So will let you know asap :). I was trying to pass the session[:user_id] in the :with hash(is it a hash?) and was not sure of the correct syntax. Will try this and let you know soon. Thanks again!
September 19th, 2008 at 11:23 AM
@Vinay - :with is a string. it is used like you would construct a GET url when passing variables.
So, http://pullmonkey.com?var1=<somevalue>&var2=<...>&... is the same as :url => "pullmonkey.com", :with => "var1=<somevalue>&var2=<...>&..."
September 20th, 2008 at 01:06 AM @Charlie - Thanks for that gyaan(explanation) :). I changed it the way you have showed. The Ajax call does not seem to be running though. I have the Javacript files included. Also, this line is causing an error in the contacts partial.
<%= collection_select(nil, :contact_id, contacts, :id, :name,{:prompt => "Select Contact"}) %>
Here, 'contacts' is a nil object and so Rails tries to do nil.map which does not exist. How did you work around this?
September 27th, 2008 at 10:18 AM Hi there,
trying this I never get above the first steps with migratin the joined table. As much as I tried to find any mistake I allways get:
== 20080927160238 CreateHierarchy: migrating ==================================
rake aborted!
undefined method `artist_id=' for #<song:0x31e77f8>
Any idea?
September 27th, 2008 at 09:26 PM @Mayo - Make sure you typed this line correctly:
ruby script/generate model song title:string artist_id:integer
Also, post your RAILS_ROOT/db/migrate/...songs.rb migration file
November 17th, 2008 at 01:51 PM Thanks. Your tutorial is clear and effective. I'm trying to use this technique to have one select for states and other for cities for a person database. However, when I change the state, I get an error:
`@person[address_attributes]' is not allowed as an instance variable name
The partial for cities is:
Each person has_one address. I hope you can shed some light here. :-)
November 18th, 2008 at 10:04 AM
@Ernest - Glad you like this tutorial.
It wouldn't surprise me if you had to do something like this:
This way the hash of attributes can be populated ... as in an address has a street which would be at person[address_attributes][street] and city at person[address_attributes][city].
Give it a shot. Let me know.
December 1st, 2008 at 04:24 PM Hey Charlie,
This is a great tutorial. I was able to make it work on the song/artist/genre scenario but when I applied to my image gallery scenario it did not work
basicaly I have:
images
galleries
subcats
an the relationships are:
images belong_to subcats
images belong_to galleries
subcats belong_to galleries
subcats has_many images
galleries has_many images, through :subcats
so on image edit and new views I'm trying to create a dynamic select for galleries which will populate subcats.
Image table has gallery_id and subcat_id.
and here is how I used your tutorial:
edit.html.erb
images_controller.rb
_subcats.html.erb - is the only partial I created since I'm using gallery populate subcat.
<filter:code>
<%= collection_select(nil, :subcat_id, subcats, :id, :title,
{:prompt => "Select an Sub Gallery"}
) %>
< /filter:code>
both dropdowns are showing and the second one does no respond to the first one.
Prototype is there.
Am I missing anything???
December 3rd, 2008 at 09:54 PM
@Marcus - You have one tiny typo in your update_subcats action.
subcats = genre.subcats
should be
subcats = gallery.subcats # using gallery.
Your update_subcats() action is only called in a remote_function, so you won't see the error or even maybe know there is an error unless you look in the log (development.log or production.log depending on the env). I am sure your log must have an undefined method subcasts for nil error or whatever.
Let me know, if you still have problems.
December 7th, 2008 at 11:18 PM Great stuff. Really awesome write-up!
Is there a way to reset the select the sub-selections if the user resets the main selection? For example, a user initially selects a genre (and the genre's entries pop up), but then decides to reset the genre selection by clicking on "Select a Genre". Can we reset the song selection list to display all songs now (or even disable it until the user selects a genre)?
December 8th, 2008 at 11:38 PM @Andrew -
So if the user select "Select a Genre" again, we can display ALL Songs and ALL artists like this:
1) either check the params[:genre_id] or check the resulting genre
2) if the params[:genre_id] is 0 then we can return all the artists and songs
if the genre is nil, we can return all the artists or songs
Something like this might work (probably best to check the params[:genre_id] ... should be 0) -
** Warning -- not tested, so let me know how it goes