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

 <%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js", "rails.jquery.js", "application.js" %>
  <%= 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:

resources :profiles

  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.

<% semantic_form_for @profile, :remote=>true, do |form| %>
    <% 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:

jQuery(function($) {
  // 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

def update_state_select
    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.

#This is _states.html.erb stage 1 - without the jQuery code
<%= 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

<script type="text/javascript">
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:

def update_city_select
    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:

<%= semantic_form_for "profile", do |form| %>
  <%= 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

jQuery files from Github

Formtastic plugin

Beware: Use the proper branch for Rails 3 gem ‘formtastic’, :git => “http://github.com/justinfrench/formtastic.git”, :branch => “rails3″

Simone Carletti’s Blog

Ryan Bates’ excellent video cast series

A couple of people asked me to show more info on the actual table design and models used in this example. You can read more on this site by following this link Rails 3, jQuery and multiselect dependencies part 2.

Comments, questions and suggestions welcome

32 Responses to “Rails 3, jQuery and multi-select dependencies”

  1. Aurimas Navickas @ August 18th, 2010

    Hey,

    super nice tutorial, I’m grateful :D
    Just there is one thing, when i’m putting rendered partial with select in it, and then posting the form, i’m getting null value of rendered select box…. :D i don;t get why, because all id’s and controllers name are correct..

  2. peter @ August 18th, 2010

    Hi Aurimas, What I haven’t detailed in this article is the database structure that’s needed to supply information to the fields. My intent was to focus on the presentation side of things rather than the data. Could it be you haven’t got an appropriate data structure in place to respond to your queries? For example, I have tables with names like ‘countries’, ‘regions’ ‘cities’ each with the appropriate foreign keys to link them all together.

  3. John Gadbois @ September 3rd, 2010

    I think you can leave the js in application.js if you use .live(‘change’, function() {}); vs .change(function() { }).

    The selects are being loaded into the DOM so you need live to attach the events to dynamically created elements.

  4. tidds @ September 3rd, 2010

    The second ajax fire isn’t happening when the code is in app.js because jquery isn’t aware of the elements in the dom since you ajax loaded them from the previous select.

    Read up on ajax’s live and delegate functions.

  5. André @ September 30th, 2010

    Hello,
    can you please explain me better about the data modeling of this project??I don’t know how to implement the models…

  6. peter @ October 6th, 2010

    AndrĂ©, just for you ;) I’ve created another article explaining the data table structure and the model files I’ve used to create this project.
    You can access it from this link rails-3-jquery-and-multiselect-dependencies-part-2. Any problems, let me know.

  7. Andrey Peresleguine @ October 13th, 2010

    Hi Peter, it was useful, you’ve saved my time, thanks!

  8. jolmes @ October 15th, 2010

    Thanks Peter, very usefull.

    I run into a strange issue. I’m doing a Province>City select process. I’m able to get the cities loaded once a province is selected. The strange thing is that in the HTML source view the cites list are not showing, it simply shows and empty select tag(what is rendered by default), even though it’s on the HTML page. The problem is that when I post the form it treats the city field like an empty field. Did you run into this? Sorry if it’s a noob question, I’m a bit of a noob to be assured.

  9. jolmes @ October 15th, 2010

    Thanks Peter, very useful.

    I run into a strange issue. I’m doing a Province>City select process. I’m able to get the cities loaded once a province is selected. The strange thing is that in the HTML source view the cites list are not showing, it simply shows and empty select tag(what is rendered by default), even though it’s on the HTML page. The problem is that when I post the form it treats the city field like an empty field. Did you run into this? Sorry if it’s a noob question, I’m a bit of a noob to be assured.

  10. jolmes @ October 15th, 2010

    Nevermind it worked! Thanks :)

  11. billis @ November 6th, 2010

    very nice article, I was wondering about how to do this.

    One question though. Is the :remote => true necessary in the partials? After all, jQuery.get() is used to implement the ajax bit.

  12. pnina @ November 25th, 2010

    Great it worked for me!! I managed to replace my observe_field behavior from rails 2.35 to 3.0.
    But when i tried to append an loading animation on it it didn’t worked.
    (I added the ‘profile_country_id’ to my jquery loading function
    like this:
    var toggleLoading = function() { $(“#loading”).toggle() };
    $(“#profile_country_id”)
    .bind(“ajax:loading”, toggleLoading)
    .bind(“ajax:complete”, toggleLoading)
    .bind(‘ajax:success’, function(evt, data, status, xhr) {
    });
    But nothing happened
    It will be very nice if you could add an example to how implement an loading div to this

  13. Ed Jones @ December 18th, 2010

    Marvelous article..but…

    I just get an error when doing all of this. Is it compatible with mongoid?

    Thanks.

  14. Mikael Henriksson @ January 25th, 2011

    Awesome! I use a completely different setup with fields for, group_collection_select and collection_select but your posts saved my bacon!

    Thanks a bunch for taking the time

  15. Shaun @ January 25th, 2011

    In this line of code:
    true, do |form| %>

    What does the :remote=>true do? Is it necessary even though you’re doing the ajaxy stuff in application.js?

    Thanks! And thanks for the write-up, exactly what I was looking for.

  16. Shaun @ January 25th, 2011

    Err, looks like that^ line of code didn’t display correctly. It’s the :remote=>true in the semantic_form_for tag.

  17. hexxen @ February 4th, 2011

    thanks for this guide, i ve been looking for this for a while.
    showing the details of the models in another post was a good thing, i was confused as well.
    @shaun: i think the :remote=>true is what lets rails know that it should use ajax.

  18. chip @ March 16th, 2011

    Trying to create a User form, which has a state field, selection from which drives a city selection. Both City and State models have an id and name field, the City having a state_id field as well. Not sure if this is te same problem as Aurimas’, but everything seems to be working for me except the save — I can see the first POST when the state changes, and it is passing the correct parameter then, and I get the correct list of cities from which to select, but when I go to create the user (profile), it is sending a NULL. Your response spoke of foreign keys and data relationships, but can you explain why that would prevent the saving of the city_id (very new to rails). An aside, I’m not saving the state_id on the user model, figuring it could be derived from the city_id if needed. The tutorial has been very helpful & I appreciate all the time and work you and others contribute to teaching people through articles such as this one.

  19. chip @ March 16th, 2011

    I am having a similar issue as Aurimas. The drop-downs all behave as expected, but the city_id isn’t saving. My user model only has the city_id (I figured that I didn’t need the state_id as it could be derived from city if needed). Your response spoke of the foreign keys / data relationships. I don’t have keys specified in the migrations themselves, but State has many cities, City belongs to state, city has many users, user belongs to city, state has many users through cities. What I don’t understand though, is the drop-down for cities is just selecting an integer for the field city_id – why would the data relationships (correct or otherwise) impact the saving? I can see the initial POST when the state is selected, and the cities do populate in the select… If I change the city_id field to a regular input box, I can type it in & save.
    BTW – fantastic tutorial; I appreciate all the time & work you & others like you put into these.

  20. Wojek @ March 17th, 2011

    I just looking for something like this :)
    But i have problem, if i submit my form region_id or other fields ids from partials donts save value only NIL

  21. chip @ March 17th, 2011

    Having a similar problem as Aurimas; dropdowns populating/behaving as expected, but not saving. Your reply to Aurimas suggested a bad data/model relationship might be the problem. Can you explain? The dropdowns are simply inserting an integer, right? I wouldn’t expect it to depend on foreign keys. Which, by the way, I don’t have specified, but I have relationships set in my models – user belongs to city, city belongs to state, city has many users, state has many cities — is this not enough? All of the other form data is being saved, so I am wondering if there is something in my controller that is amiss? Do I have to pass any parameters from the partial?
    Thank you by the way for the excellent article.

  22. Giovanni Sakti @ March 23rd, 2011

    Exactly what im looking for,
    thanks Peter!

  23. Giovanni Sakti @ March 23rd, 2011

    Much useful Peter,
    Thanks!

  24. Martin Harrigan @ May 31st, 2011

    I think you should be using “semantic_fields_for” instead of “semantic_form_for” in _states.html.erb to suppress the nested form tags.

    Martin.

  25. chip @ September 29th, 2011

    Fantastic tutorial – question though: if you make city (for example) required, are you able to get the validation to work completely? I can get the error message, but having a heck of a time getting it to highlight the field. For some reason, it’s not getting the field__with_error wrapper…

  26. Suryakant maurya @ November 24th, 2011

    Thanks a lot..Its so useful for me.

  27. dave @ January 11th, 2012

    Thanks much.

    I had to rename the javascript passed parameter in the route and the controller to something other than :id.

    The parameter was not being passed through the route if there was not an :id within the route that had the same number. If applied to this example I was not able to get the state(:id) data unless there was a profile(:id) of the same number.

  28. Nathan @ April 16th, 2012

    Hi Peter,

    Thank you for the wright up. This is exactly what I’ve been looking for. The only exception I have is that my address is nested in a “account” model that’s accepting nested attributes for addresses.

    I keep getting a 404 error when specifying the jQuery.get(‘/account/update_city_select/’

    Any ideas?

    Thanks,
    Nathan

  29. ivan calderon @ May 5th, 2012

    Thanks a lot! :D, been trying to make this work for over a week!

  30. Bardach @ August 23rd, 2012

    Very useful tutorial. one of the few tutorial on the subject.
    Question please: Can I use this tuto with Simple_form? a idea?

  31. murty @ September 24th, 2012

    Because I am a noob, I was hesitant to try ajax/jquery. But this article gave me the confidence. I tried this using simple form everything works except city_id from the partial does not get passed to the create action. Chip seem to be having the same problem. I am wondering if anything else is required in .js

  32. James Dunn @ October 5th, 2012

    I found your post to be extremely helpful. Thanks!

Leave a Reply



*