Dynamic Select Boxes - Ruby on Rails

March 30th, 2008 by charlie

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:
1
2

rake db:migrate
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.

38 Responses to “Dynamic Select Boxes - Ruby on Rails”

  1. Adriano Says:
    Nice tutorial, I will be doing it, soon.
    Just a nice consideration does the render_html works in ie ?
  2. charlie Says:

    @Adriano - do you mean the replace_html rjs line? IAE, it should work in IE, let me know if you run into any problems.
  3. Jonathan Says:
    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 ....
  4. Adriano Says:
    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
  5. charlie Says:

    @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.
  6. bonhoffer Says:
    this is truly great. thanks for your simple, clean tutorial.
  7. cworth Says:
    I can't get this to work. Using the debugger, execution never gets to the update_artists function. Any advice?
    Thanks
  8. cworth Says:
    Got it to work. Forgot a line of code.
    Works great!
  9. cworth Says:
    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
  10. charlie Says:

    @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?
  11. maige Says:
    This is a clear and straight forward example. Thanks!
  12. enduddyangedy Says:
    select(personal) locality. So to out!
  13. ganez Says:
    I can't get this to work. Execution never gets to the update_artists function. ???
  14. Andy Says:
    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.
  15. ganez Says:
    i cant able to select artist values as well as song value in dropdown....any suggestions pls..
  16. charlie Says:
    @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.

  17. Geoff Says:
    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!
  18. charlie Says:

    @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
  19. Harish Kumar Says:
    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

  20. charlie Says:

    @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.
  21. boblu Says:
    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?
  22. charlie Says:

    @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
  23. Drew Says:
    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>



    <th>Category</th>


    <%= f.text_field ( :title, :size => 40, :style => "font-size: 1.0em;") %>
    <%= collection_select(nil, :section_id, @sections, :id, :name,
    {:prompt => "Select a section"},
    {:onchange => "#{remote_function(:url => {:action => "update_categories"},
    :with => "'section_id='+value")}"}) %>
    <%= render :partial => 'categories_list', :object => @categories %>


    < /filter:code>

    _categories_list partial
    <filter:code>
    <%= collection_select(nil, :category_id, @categories, :id, :name,
    {:prompt => "Select a category"}) %>
    < /filter:code>

  24. charlie Says:

    @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.
  25. Vinay Says:
    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?
  26. charlie Says:

    @Vinay - Thanks. I can't really tell what you are asking for without an example. Are you talking about this line:


    1
    2
    3
    4
    <%= collection_select(nil, :genre_id,  @genres,  :id, :name,
    {:prompt => "Select a Genre"},
    {:onchange => "#{remote_function(:url => {:action => "update_artists"},
    :with => "'genre_id='+value")}
    "
    }) %>

    And you want to use a nested named route and get the session[:user_id] passed in as user_id, try this:


    1
    2
    {:onchange => "#{remote_function(:url  => update_contacts_company_path,
    :with => "'genre_id='+value&user_id=#{session[:user_id]}}")}
    "
    }) %>

    Let me know.
  27. charlie Says:
    I messed up the quotes on the :with string:



    "#{remote_function(:url  => update_contacts_company_path,                                                       :with => "'genre_id='+value+'&user_id=#{session[:user_id]}'")}"}) %>

  28. Vinay Says:
    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!
  29. charlie Says:

    @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=<...>&..."
  30. Vinay Says:
    @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?
  31. Mayo Says:
    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?
  32. charlie Says:
    @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

  33. Ernest Says:
    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:




    1
    2
    3
    4
    5
    6
    7
    8
    9


    <% fields_for "person[address_attributes]", address do |f| %>

    <%= f.collection_select(:city_id, cities, :id, :nombre,
    {:prompt =
    >
    "City"}) %>

    <%end%
    >





    Each person has_one address. I hope you can shed some light here. :-)
  34. charlie Says:

    @Ernest - Glad you like this tutorial.

    It wouldn't surprise me if you had to do something like this:



    <% fields_for "person[address_attributes][]", address do |f| %>

    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.
  35. Marcus Says:
    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



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    <p>
    <label for="gallery">Gallery:</label>
    <%= collection_select(nil, :gallery_id, @galleries, :id, :title,
    {:prompt => "Select a Gallery"},
    {:onchange => "#{remote_function(:url => {:action => "update_subcats"},
    :with => "'gallery_id='+value")}
    "
    }) %>
    <div id="subcats"
    >

    <label for="subgallery">Sub Gallery:</label>
    <%= render :partial => 'subcats', :object => @subcats %>
    </div
    >

    </p>



    images_controller.rb



    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

    def new
    @image = Image.new
    @galleries = Gallery.find(:all)
    @subcats = Subcat.find(:all)
    respond_to do |format|
    format.html # new.html.erb
    format.xml { render :xml => @image }
    end
    end

    # GET /images/1/edit
    def edit
    @image = Image.find(params[:id])
    @galleries = Gallery.find(:all)
    @subcats = Subcat.find(:all)
    end

    def update_subcats
    # updates artists and songs based on genre selected
    gallery = Gallery.find(params[:gallery_id])
    subcats = genre.subcats

    render :update do |page|
    page.replace_html 'subcats', :partial => 'subcats', :object => subcats
    end
    end



    _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???
  36. charlie Says:

    @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.
  37. Andrew Says:
    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)?
  38. charlie Says:
    @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) -



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
      def update_artists
    # updates artists and songs based on genre selected
    # since we can have an invalid genre_id lets rescue it
    genre = Genre.find(params[:genre_id]) rescue nil
    # check the genre_id, if it is 0 then the user selected the prompt and we need to redisplay all artists and songs
    artists = params[:genre_id] == "0" ? Artist.all : genre.artists
    songs = params[:genre_id] == "0" ? Song.all : genre.songs

    # this part stays the same
    render :update do |page|
    page.replace_html 'artists', :partial => 'artists', :object => artists
    page.replace_html 'songs', :partial => 'songs', :object => songs
    end
    end



    ** Warning -- not tested, so let me know how it goes

Leave a Reply

Check here to see how to submit syntax highlighted code to the comments.