11 Aug
Over 4 years ago, I wrote a tutorial for doing dynamic (cascading) select boxes.
Still getting comments and emails to this day. Mostly asking how to get this working with rails 3, which has moved from prototype to jquery.
So here's a tutorial for getting 3 select boxes to trigger updates for each other.
First set things up:
I just used the html5 haml twitter bootstrap, etc template. Really useful.
If you need data, here's what I used - put this in your db/seeds.rb file:
Next, setup your model associations:
Genres have many artists.
Artists have many songs.
Genres have many songs through artists.
I'm just using a home controller to setup variables for the index page as well as setup variables for use in the dynamic updating:
Now the view just has the 3 select boxes and the unobtrusive javascript (triggered onchange) to make the ajax calls for updating:
We need our rjs files for updating the select boxes, one for the songs (when artist changes) and one for the artists and songs (when genre changes):
Our routes are simple:
That's it.
UPDATE: Here's an erb alternative for index.html.
And the js.haml can be converted to js.erb by taking #{...} and converting to <%= ... %> :
30 Mar
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:
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.