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