Dynamic Select Boxes – Ruby on Rails
UPDATE: There is a dynamic select boxes for rails 3 tutorial now, so if this isn't working for you, check it out.
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.
106 Responses
to “Dynamic Select Boxes – Ruby on Rails”
Sorry, comments for this entry are closed at this time.
Thank you for your reply Charlie, could you kindly elaborate in clear terms on what changes to make and in which files to do I need to make these changes using your code as guide because I am a rails learner and thus cannot figure out your explanation in the previous post. I will really appreciate if you could start from the controller action or better still provide us a new tutorial as you opined that clearly explains this concept.
This has been giving me real though time for more than a week now and I will really appreciate your help in this regard and I am pretty sure it will be helpful to lots of people out there too.
Thanks.
An updated version of this tutorial would be GREATLY appreciated. With so many of the “examples” I see on the web, I can’t tell what’s “actual” code and what is someone’s pseudo-code.
The updated version is now available, go check it out.