Monday, January 22, 2007

Cascading Drop-downs in Rails

We were looking for a solid example of cascading dynamic drop-down select lists to use in our rails application, and found the web sorely lacking in solid examples. We found a very good start at http://www.railsweenie.com/forums/2/topics/767 but it wasn't complete enough, or didn't work entirely. So my very good buddy and co-worker Sheri and I figured this out, and got it working for our app, and wanted to document it here in the hopes that it helps someone else. Here's the nutshell version:

It's probably already there, but make sure this line is in your standard_layout.rhtml:

<%= javascript_include_tag :defaults %>

Add this function def to application_helper.rb:

def update_select_box( target_dom_id, collection, options={} )
# Set the default options
options[:text] ||= 'name'
options[:value] ||= 'id'
options[:include_blank] ||= true
options[:clear] ||= []
pre = options[:include_blank] ? [['','']] : []
out = "update_select_options( $('" << onclick="BLOG_clickHandler(this)" class="blsp-spelling-error" id="SPELLING_ERROR_5">dom_id.to_s << "'),"
out << "#{(pre + collection.collect{ |c| [c.send(options[:text]), c.send(options[:value])]}).to_json}" << ","
out << "#{options[:clear].to_json} )"
end

This calls update_select_options which needs to go into application.js:

function update_select_options( target, opts_array, clear_select_list ) {

if( $(target).type.match("select" ) ){ // Confirm the target is a select box

// Remove existing options from the target and the clear_select_list
clear_select_list[clear_select_list.length] = target // Include the target in the clear list

for( k=0;k <>
obj = $(clear_select_list[k]);
if( obj.type.match("select") ){
len = obj.childNodes.length;
for( var i=0;i <>
}
}

// Populate the new options
for(i=0;i <>
o = document.createElement( "option" );
o.appendChild( document.createTextNode( opts_array[i][0] ) );
o.setAttribute( "value", opts_array[i][1] );
obj.appendChild(o);
}
}
}

Add something like this to the form.rhtml (changing the name of the observable field as appropriate):

<%= observe_field 'item[facility_id]', :frequency => 0.5,
:update => 'location_id', :url =>
{ :controller => 'item', :action=> 'refreshLocation' },
:with => "'facility_id=' + escape(value)" %>

Add something like this to the controller:

def refreshLocation
@facilities = Facility.find(:all)
@facility = Facility.find(params[:facility_id])
@locations = Location.find_all_by_facility_id(params[:facility_id])
render :update do |page|
page << text =""> :description} )
end
end


This tidbit in the form.rhtml is the ultimate target of all this work (this is the drop down we want to refresh)






<%= select_tag "item[location_id]", options_from_collection_for_select(@locations,:id,:description) %>



If I missed any code attributions from the various sources we pieced this together from, I'm sorry. Write me and I'll make good and give attribution where appropriate.

If you have any questions about this, post 'em and I'll take my best shot at answering.

And finally, thanks Sheri! Couldn't have done it without you!