PullMonkey Blog


11 Aug

Dynamic Select Boxes - Ruby on Rails 3


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 <%= ... %> :


05 Aug

Dynamic Select Boxes (many to many) - Ruby on Rails


I just got comment asking how one would go about doing a many to many relation in this dynamic select box example. For example, what if an artist belongs to multiple genres. Here we go:

The original tutorial.

Create your models and build your migrations:

1
2
3
4
5

ruby script/generate model genre name:string
ruby script/generate model artist name:string   # no genre_id here, moved to association table
ruby script/generate model song title:string artist_id:integer
ruby script/generate model artist_association artist_id:integer genre_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
33
34
35
36
37
38
39
40
41
42
43
44
45
46

class CreateHierarchy < ActiveRecord::Migration
  def self.up
    # same genres as before
    g1 = Genre.create(:name => "Genre 1")
    g2 = Genre.create(:name => "Genre 2")
    g3 = Genre.create(:name => "Genre 3")

    # same artists as before, but without a genre_id
    a1 = Artist.create(:name => "Artist 1")
    a2 = Artist.create(:name => "Artist 2")
    a3 = Artist.create(:name => "Artist 3")
    a4 = Artist.create(:name => "Artist 4")
    a5 = Artist.create(:name => "Artist 5")
    a6 = Artist.create(:name => "Artist 6")

    # now set which artists belong to which genres
    # Artist 1 belongs to all three genres
    ArtistAssociation.create(:genre_id => g1.id, :artist_id => a1.id)
    ArtistAssociation.create(:genre_id => g2.id, :artist_id => a1.id)
    ArtistAssociation.create(:genre_id => g3.id, :artist_id => a1.id)
   # the rest of the artists only belong to one association
    ArtistAssociation.create(:genre_id => g1.id, :artist_id => a2.id)
    ArtistAssociation.create(:genre_id => g2.id, :artist_id => a3.id)
    ArtistAssociation.create(:genre_id => g2.id, :artist_id => a4.id)
    ArtistAssociation.create(:genre_id => g3.id, :artist_id => a5.id)
    ArtistAssociation.create(:genre_id => g3.id, :artist_id => a6.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

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
9
10
11
12
13
14
15
16
17
18
19
20
21

class Genre < ActiveRecord::Base
  has_many :artist_associations
  has_many :artists, :through => :artist_associations
  # CAN"T NEST HMTs .....  has_many :songs, :through => :artists
  # do it by hand ... argh
  def songs
    artists.map{|a| a.songs}.flatten
  end
end

class Artist < ActiveRecord::Base
  has_many :artist_associations
  has_many :genres, :through => :artist_associations
  has_many :songs
end

class ArtistAssociation < ActiveRecord::Base
  belongs_to :artist
  belongs_to :genre
end

That should be it for the many to many relationship.



Everything else is the same as in the last tutorial.

# 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>