unobtrusive javascript – PullMonkey Blog http://pullmonkey.com Thu, 16 Aug 2012 23:40:41 +0000 en-US hourly 1 https://wordpress.org/?v=5.6 Dynamic Select Boxes – Ruby on Rails 3 http://pullmonkey.com/2012/08/11/dynamic-select-boxes-ruby-on-rails-3/ http://pullmonkey.com/2012/08/11/dynamic-select-boxes-ruby-on-rails-3/#comments Sat, 11 Aug 2012 01:11:51 +0000 http://pullmonkey.com/?p=57726 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

]]>
http://pullmonkey.com/2012/08/11/dynamic-select-boxes-ruby-on-rails-3/feed/ 12