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:

# create new rails app
rails new dynamic_select_boxes -m https://raw.github.com/RailsApps/rails3-application-templates/master/rails3-haml-html5-template.rb
# create models:
rails g model genre name:string
rails g model artist name:string genre_id:integer
rails g model song title:string artist_id:integer
rake db:migrate
view raw gistfile1.sh hosted with ❤ by GitHub

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:

3.times do |x|
genre = Genre.find_or_create_by_name(:name => "Genre #{x}")
3.times do |y|
artist = Artist.find_or_create_by_name(:name => "Artist #{x}.#{y}", :genre => genre)
3.times do |z|
Song.find_or_create_by_title(:title => "Song #{x}.#{y}.#{z}", :artist => artist)
end
end
end
view raw gistfile1.rb hosted with ❤ by GitHub

Next, setup your model associations:

# app/models/artist.rb
class Artist < ActiveRecord::Base
belongs_to :genre
has_many :songs
attr_accessible :genre_id, :name, :genre
end
# app/models/genre.rb
class Genre < ActiveRecord::Base
attr_accessible :name
has_many :artists
has_many :songs, :through => :artists
end
# app/models/songs.rb
class Song < ActiveRecord::Base
belongs_to :artist
attr_accessible :artist_id, :title, :artist
end
view raw gistfile1.rb hosted with ❤ by GitHub

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:

# app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
@genres = Genre.all
@artists = Artist.all
@songs = Song.all
end
def update_artists
# updates artists and songs based on genre selected
genre = Genre.find(params[:genre_id])
# map to name and id for use in our options_for_select
@artists = genre.artists.map{|a| [a.name, a.id]}.insert(0, "Select an Artist")
@songs = genre.songs.map{|s| [s.title, s.id]}.insert(0, "Select a Song")
end
def update_songs
# updates songs based on artist selected
artist = Artist.find(params[:artist_id])
@songs = artist.songs.map{|s| [s.title, s.id]}.insert(0, "Select a Song")
end
end
view raw gistfile1.rb hosted with ❤ by GitHub

Now the view just has the 3 select boxes and the unobtrusive javascript (triggered onchange) to make the ajax calls for updating:

# app/views/home/index.html.haml
= collection_select(nil, :genre_id, @genres, :id, :name, {:prompt => "Select a Genre"}, {:id => 'genres_select'})
%br
= collection_select(nil, :artist_id, @artists, :id, :name, {:prompt => "Select an Artist"}, {:id => 'artists_select'})
%br
= collection_select(nil, :song_id, @songs, :id, :title, {:prompt => "Select a Song"}, {:id => 'songs_select'})
:javascript
$(document).ready(function() {
$('#genres_select').change(function() {
$.ajax({
url: "#{update_artists_path}",
data: {
genre_id : $('#genres_select').val()
},
dataType: "script"
});
});
$('#artists_select').change(function() {
$.ajax({
url: "#{update_songs_path}",
data: {
artist_id : $('#artists_select').val()
},
dataType: "script"
});
});
});
view raw gistfile1.rb hosted with ❤ by GitHub

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):

# app/views/home/update_artists.js.haml
$('#artists_select').html("#{escape_javascript(options_for_select(@artists))}");
$('#songs_select').html("#{escape_javascript(options_for_select(@songs))}");
# app/views/home/update_songs.js.haml
$('#songs_select').html("#{escape_javascript(options_for_select(@songs))}");
view raw gistfile1.rb hosted with ❤ by GitHub

Our routes are simple:

# config/routes.rb
DynamicSelectBoxes::Application.routes.draw do
get 'home/update_artists', :as => 'update_artists'
get 'home/update_songs', :as => 'update_songs'
root :to => "home#index"
end
view raw gistfile1.rb hosted with ❤ by GitHub

That's it.

UPDATE: Here's an erb alternative for index.html.

# app/views/home/index.html.haml
<%= collection_select(nil, :genre_id, @genres, :id, :name, {:prompt => "Select a Genre"}, {:id => 'genres_select'}) %>
<br/>
<%= collection_select(nil, :artist_id, @artists, :id, :name, {:prompt => "Select an Artist"}, {:id => 'artists_select'}) %>
<br/>
<%= collection_select(nil, :song_id, @songs, :id, :title, {:prompt => "Select a Song"}, {:id => 'songs_select'}) %>
<script>
$(document).ready(function() {
$('#genres_select').change(function() {
$.ajax({
url: "<%= update_artists_path %>",
data: {
genre_id : $('#genres_select').val()
},
dataType: "script"
});
});
$('#artists_select').change(function() {
$.ajax({
url: "<%= update_songs_path %>",
data: {
artist_id : $('#artists_select').val()
},
dataType: "script"
});
});
});
</script>
view raw gistfile1.rhtml hosted with ❤ by GitHub

And the js.haml can be converted to js.erb by taking #{...} and converting to <%= ... %> :

# app/views/home/update_artists.js.haml
$('#artists_select').html("<%= escape_javascript(options_for_select(@artists)) %>");
$('#songs_select').html("<%= escape_javascript(options_for_select(@songs)) %>");
# app/views/home/update_songs.js.haml
$('#songs_select').html("<%= escape_javascript(options_for_select(@songs)) %>");
view raw gistfile1.rhtml hosted with ❤ by GitHub



12 Responses to “Dynamic Select Boxes – Ruby on Rails 3”

  1. By Bob Hazlewood on Aug 14, 2012 | Reply

    Thanks for updating this. I love the clear examples that don’t “assume” you know what something does, or why it’s required.

  2. By Foluso on Aug 15, 2012 | Reply

    Thank you for this but I am working on ‘erb’ and this example uses ‘haml’. Please how do I make it work on my work environment? Sorry if I sound naive

  3. By charlie on Aug 16, 2012 | Reply

    Foluso – the only file you’ll want to convert to use erb is going to be the index.html.haml.
    The others, you can just rename to .js.erb.

    For the index.html.erb, just wrap the lines starting with “=” with <% and %>
    And instead of :javascript, just wrap with .

    I’ve updated the post to have the erb example for that file.

  4. By Foluso on Aug 16, 2012 | Reply

    Thank you very much Charlie for your help. I eventually got it to work though I had to adjust the following files too

    # app/views/home/update_artists.js.erb
    $(‘#artists_select’).html(“”);
    $(‘#songs_select’).html(“”);

    # app/views/home/update_songs.js.erb
    $(‘#songs_select’).html(“”);

  5. By charlie on Aug 16, 2012 | Reply

    Thanks Foluso, updated post with the converted erb – at the bottom.

  6. By Bob Hazlewood on Aug 20, 2012 | Reply

    I know this is beyond the scope of your tutorial, but — let’s say I want to take the results of all three selects, and pass those as parameters to a controller? On the final select, render a partial with the params ?

  7. By Bob Hazlewood on Aug 21, 2012 | Reply

    I figured it out.

    When I select the third element, it executes a render partial with a link_to , :controller => ‘controller_name’, :action => ‘action_name’, :parameter1 => params[:select_name1], :parameter2 => params[:select_name2]

    Thanks again for a great learning tool.

  8. By Bob Hazlewood on Aug 29, 2012 | Reply

    hahaha back again with another question.

    In your example, the elements come back in the order in which they are in the database (song1, then song2, then song3). Of course, in the “real world” the data would be more like “Good Vibrations”, “Surfer Girl”, and “I Get Around”. What if you want the select list to be in alphabetical order? Normally I would pull the data like @songs = Song.find(:all, :order => ‘title ASC’) – how would you do that with the .map?

  9. By charlie on Sep 5, 2012 | Reply

    You can do this many ways.
    1) In the models: You can set the default order of all the models. Plus for the has_many associations, you could add an order clause: has_many :songs, :order => :title
    2) In the controller: Instead of Genre.all, you would do Genre.order(:name), or instead of artist.songs, you would do artist.songs.order(:title)

    Then the map just acts on the ordered collection.

  10. By Bardach on Jan 31, 2013 | Reply

    Hi Charlie,
    Thanks for this useful tutorial.
    How can used it with simple_form?
    Bardach5

  11. By Bardach on Jan 31, 2013 | Reply

    Hi Charlie,
    Thanks for this useful tutorial.
    How can use it with simple_form?
    Bardach

  12. By `Drew on Feb 5, 2013 | Reply

    First of all, thanks for this. It has been an excellent teaching tool for the collection_select dropdown box method for me. That being said, I’m stuck. My dropdown menus aren’t populated with any data! I followed the tutorial exactly the first two times through. THEN I copied and pasted your code to make sure it wasn’t my mistyping that had screwed things up. No luck still. Any suggestions for where my hangup might be?

Sorry, comments for this entry are closed at this time.