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