Rails 3, jQuery and multi-select dependencies
I’ve just upgraded to Rails 3 and hit a real blocker that’s taken me two days to work out. I hope this post can save someone else even a few hours of stuffing around with Rails 3, jQuery, unobtrusive javascript and other assorted gems.
The problem
I have a user profile form with dropdown selects for country, region and city. To start out, only the country select is populated. When the user selects a country, the region select populates with regions from that country, when a region (aka state/territory) is selected, the city list is populated. I know there are countries that don’t have regions, but that’s another day’s thinking.
My setup is fairly simple. I’m using jQuery, NOT prototype, so there’s no observe_field. Try to use such will result in an error ‘observer_field undefined method’. I’m using formtastic as a UI helper because I’ve been impressed by it’s DRYness and I’m tired of coding html forms. This solution shows the formtastic layouts, but can be applied to standard rails forms very easilyy.
jQuery setup
I’m using the jQuery 1.4.2 minimized version of the file from Google and a jquery/ujs wrapper/helper file I downloaded from github at http://github.com/rails/jquery-ujs/blob/master/src/rails.js.
I renamed this file as rails.jquery.js and copied it to the folder public/javascripts/ under the root of my application folder.
Next step is to update the javascript files that are included in the header of your application. So, open up app/views/layouts/application.html.erb
<%= csrf_meta_tag %>
The javascript_include tag lists specifically those files I need. If you use the “defaults” option you’ll end up with the prototype.js included in the mix. This is something I’m trying to avoid given it’s not UJS.
The csrf_meta_tag reference is a Rails 3 enhancement. It seeks to avoid cross site scripting issues by ensuring the token created by the authenticated visitor is maintained throughout the site, thereby ensuring that a button click or post back has actually come from a genuine session.
Update routes.rb
When a change of a select list is triggerd, an Ajax post will be made to my profiles controller (remember we’re dealing with the ‘profile’ model here). I’m using two actions, update_state_select and update_city_select.
To add these calls into the routes.rb file, update it with the following:
match 'profiles/update_state_select/:id', :controller=>'profiles', :action => 'update_state_select'
match 'profiles/update_city_select/:id', :controller=>'profiles', :action => 'update_city_select'
The main form
The form where users can enter their new profile details is called new.html.erb and resides under the app/views/profiles folder. It’s a formtastic form, but once you’ve seen the code, you can do the mental shift to whatever other UI component you fancy.
<% form.inputs do %>
<%= form.input :firstname %>
<%= form.input :middlename %>
<%= form.input :lastname %>
<%= form.input :image, :as=> :file %>
<%= form.input :timezone_adjustment, :as=> :time_zone %>
<%= form.input :gender, :as => :radio, :label => "Gender", :collection => [["Male", "M"], ["Female", "F"]] %>
<%= form.input :birthday, :as=> :date, :start_year =>1930 %>
<%= form.input :facebook_profile_url %>
<%= form.input :linkedin_profile_url %>
<%= form.input :country_id, :collection=>Country.find(:all, :order=>:name).collect{ |c| [c.name,c.id]}, :required=>true %>
<div id="addressStates">
<%= render :partial => 'states' %>
</div>
<div id="addressCities">
<%= render :partial => 'cities' %>
</div>
<%= form.buttons :commit %>
<% end %>
<% end %>
Handle the change events
We have to add in the javascript now that will get fired when the first select (the country list) is changed. This is a piece of javascript code I’ve decided to put into the application.js file under the public/javascripts file.
The code looks like this:
// when the #country field changes
$("#profile_country_id").change(function() {
// make a POST call and replace the content
var country = $('select#profile_country_id :selected').val();
if(country == "") country="0";
jQuery.get('/profiles/update_state_select/' + country, function(data){
$("#addressStates").html(data);
})
return false;
});
})
What’s happening here is the jQuery code is waiting for a change to be triggered by a control called ‘profile_country_id’. When a change is detected, it’s taking the value of the selected value and passing it as a parameter to the get call on profiles/update_state_select. The results of the call will end up being rendered as html in the addressStates div.
If you have firebug installed (and you should!!) you can see the javascript get being triggered in the console window. If you haven’t implemented the update_state_select method in your controller, you’ll probably get a 404 error.
The controller
This is the simple bit apart from a few minor tweaks to accommodate Rails 3 ActiveRecord queries
states = Region.where(:country_id=>params[:id]).order(:name) unless params[:id].blank?
render :partial => "states", :locals => { :states => states }
end
Note the use of the ‘where’ and the ‘order’ methods applied directly to the entity name. I think this is one of the neatest changes in the Rails 3 ActiveRecord domain. I’m going to have to build an automation utility to update all my old code.
Rendering the partial
In the simplest case, the partial that displays the states should just be a select populated with the results of the update_state_select method call. There’s a ‘but…’ coming up so pay attention.
<%= semantic_form_for "profile", :remote=>true, do |form| %>
<%= form.inputs do %>
<% if !states.blank? %>
<%= form.input :region_id, :collection=>states.collect{ |s| [s.name,s.id]} %>
<% else %>
<%= form.input :region_id, :collection=>[] %>
<% end %>
<% end %>
<% end %>
So far it’s looking good, when a user selects the country drop-down the jQuery event is triggered, that does a Get to the controller. The controller builds a collection and passes it to the _states partial. However, (this is the ‘but’ I mentioned) I couldn’t get the change event of the state drop-down unless I put the jQuery code in as part of the partial. If anyone knows what/how to add this code to the application.js and still get it to fire, please let me know.
To get the change event of the state_id select to trigger, add the following code to the top of the _state.html.erb file. Note how similar it is to the code in the application.js file
jQuery(function($) {
// when the #region_id field changes
$("#profile_region_id").change(function() {
// make a POST call and replace the content
var state = $('select#profile_region_id :selected').val();
if(state == "") state="0";
jQuery.get('/profiles/update_city_select/' + state, function(data){
$("#addressCities").html(data);
})
return false;
});
})
</script>
Now we have a functioning form with an Ajax enabled country and state select lists. All that’s left to do is to do a copy and paste of code in the profiles controller to get the update_city_select method working and add a new _cities.html.erb partial to display the results of the controller method call.
Here’s all the relevant code for the controllers/profiles_controller.rb file:
cities = City.where(:region_id=>params[:id]).order(:name) unless params[:id].blank?
render :partial => "cities", :locals => { :cities => cities }
end
And the code for the app/views/profiles/_cities.html.erb partial:
<%= form.inputs do %>
<% if !cities.blank? %>
<%= form.input :city_id, :collection=>cities.collect{ |c| [c.name,c.id]} %>
<% else %>
<%= form.input :city_id, :collection=>[] %>
<% end %>
<% end %>
<% end %>
Summary
I hit the first wall when I added an observe_field and couldn’t explain why I was getting method undefined errors. That started me down a long and winding road of frustration. I think I’ve overcome most of the frustrations and I’m starting to get the hang of the Rails 3 changes.
There is a plugin that will allow you to keep your old Rails 2 ways alive but every time I saw mention of it, I also saw comments about how bad an idea it is. I mean, you could probably also run some COBOL code as a plugin, but why live in the past?
References
Beware: Use the proper branch for Rails 3 gem ‘formtastic’, :git => “http://github.com/justinfrench/formtastic.git”, :branch => “rails3″
Ryan Bates’ excellent video cast series
Comments, questions and suggestions welcome

