Building a Cascading Drop Down Selection List for Ruby on Rails with jQuery Ajax

A frequent need in building web site application is to have users select one value and then, based on that value, select another value. Real world needs for related values might be : Select a Country and then State or Province; Select a Car Manufacture and then a Car Model. Generically it’s about selecting some Category or Section and then selecting the Sub Category or sub Section, the selection of one field cascades the results in another related field.

Also called Related Drop Down fields or Dependant Drop Down lists or Dynamic Drop Downs or Dependent Drop Downs.

What you want to have happen is something that will look and act like this
If you select Veg’s
Select Veg
and then if you select Meat :
select beef

Also, in the underlying html select tag code, you want values stored and names displayed so you can a)change or correct the names, b) display the names in a different languages but keep the underlying key values.

In the bad old days you might have them select the first value and the go to a new page for the related values, or at best refresh the entire page. Thankfully this is the days of shinny and we haz ajax!! But we still need to get the ducks lined up and quacking in order for this to work. This is my code to do that.

Note that this solution has been tested with RoR 2.3.5 and jQuery 1.4.2 (standard flavours as of early 2010)

you need to have jQuery loaded on the page and the easest way (althought not always the best way) is to pull all the script for m the public/javascrip direcroty of your application with :

1
< %= javascript_include_tag :defaults %>

in the sample I assuming a Section field and a dependent Sub-Section field. From a data model perspective the Section mode has a Id and a Name and has_one :sub_section; and the SubSection has a Id, a Name and a Section Id and belongs_to :section

Note: I added addtion directions in a preamble , which talks about building the Section and SubSection mvc objects in more detail.

in the sub_sections controller

1
2
3
4
5
6
    def for_sectionid
      @subsections = SubSection.find( :all, :conditions => [" section_id = ?", params[:id]]  ).sort_by{ |k| k['name'] }    
      respond_to do |format|
        format.json  { render :json => @subsections }      
      end
     end

this is a pretty straight forward piece of code that uses an id passed in the parameters, returns all the subsection objects for that section_id, sorts that collection of subsections by the subsection.name, and renders that collection as a json object.

As per the notes on the rails wiki for SQL Injection, you need to sanitize the variables being passed as a parameter, and Ruby on Rails has a built in filter for special SQL characters which you need to apply, as above. :conditions => [” section_id = ?”, params[:id]]

Note, you could also use a Dynamic attribute-based finders to get the subsection object :

1
 @subsection = SubSection.find_all_by_section_id( params[:id]).sort_by{ |k| k['name'] }

which is shorter code (less change of a syntax error?) and, as a bonus, automatically applies the sanitize countermeasure.

Further, you might want to create a custom json view for this routine with, only the sub section name and sub section id, if you a) have values want to keep private, or b) the SubSection objection has a lot of data (since we are only interested in 2 fields). That’s the reason its in the SubSections Controller, to keep related stuff together.

in the new or edit view for (in this case) gallery

1
2
3
4
5
6
7
8
9
10
11
12
< % form_for(@gallery) do |f| %>
...
  <p>
    < %= f.label :section_id %><br />      
    < %=  collection_select(:gallery, :section_id, Section.all, :id, :name , options ={:prompt => ""} ) %>
    </p>
  <p>
    < %= f.label :sub_section_id %><br />      
    < %= collection_select(:gallery, :sub_section_id, SubSection.find_all_by_section_id(@gallery.section_id), :id, :name, options ={:prompt => ""}) %>
  </p>
..
< % end %>

Again, a fairly typical piece of Rails ERB code for display a html select tag used to create a select list (or drop-down list).

The collection_select help used for the Section field displays all the valid value Name’s and stores the id’s, notes the selected value (as needed in a edit) and a empty string prompt (as needed in a new).

In the case of the Sub Section field the “extra” stuff is to populate the drop down for an edit of an exiting value.

That one line “< %= collection_select(:gallery, :section_id, Section.all, :id, :name , options ={:prompt => “”} ) %>” replaces all of my 2005 posting Building a Better Drop Down Selection List for Ruby on Rails written for Rails 1.x. That’s progress!!

There’s the magic sauce for all of this, assuming that you are including the jQuery javascript library (tested with version jQuery 1.4.2) on the page. I have this in a partial used in the new and edit pages for the gallery.

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
<script type="text/javascript">
    $(document).ready(function(){
        $("select#gallery_section_id").change(function(){
            var id_value_string = $(this).val();
            if (id_value_string == "") {
                // if the id is empty remove all the sub_selection options from being selectable and do not do any ajax
                $("select#gallery_sub_section_id option").remove();
                var row = "<option value=\"" + "" + "\">" + "" + "</option>";
                $(row).appendTo("select#gallery_sub_section_id");
            }
            else {
                // Send the request and update sub category dropdown
                $.ajax({
                    dataType: "json",
                    cache: false,
                    url: '/sub_sections/for_sectionid/' + id_value_string,
                    timeout: 2000,
                    error: function(XMLHttpRequest, errorTextStatus, error){
                        alert("Failed to submit : "+ errorTextStatus+" ;"+error);
                    },
                    success: function(data){                    
                        // Clear all options from sub category select
                        $("select#gallery_sub_section_id option").remove();
                        //put in a empty default line
                        var row = "<option value=\"" + "" + "\">" + "" + "</option>";
                        $(row).appendTo("select#gallery_sub_section_id");                        
                        // Fill sub category select
                        $.each(data, function(i, j){
                            row = "<option value=\"" + j.sub_section.id + "\">" + j.sub_section.name + "</option>";  
                            $(row).appendTo("select#gallery_sub_section_id");                    
                        });            
                     }
                });
            };
                });
    });
</script>

The Code is wrapped up in standard jQuery code for doing unobtrusive javascript, waiting for the DOM to finish load and then watching for changes on the select tag with the gallery_section_id tag id.

The first section of code (lines 5-9) just reacts if the Section field is set to my default prompt of blank, in which case it blanks out the sub section values and does nothing else.

The next piece (line 14-16) is the meat of the $.ajax request: setting the dataType to “json” (which ensures the controller routine renders as expected), doing “cache: false” is best for development but is someting to look at in production if your supporting data is very very static. setting the url is Key! in this case ” url: ‘/sub_sections/for_sectionid/’ + id_value_string ” so that it calls the “sub_sections” controller and the “for_sectionid” routine and passes the id string.

If the ajax returns successfully, then the code removes the exiting option’s (important! it works on the option part of the DOM) from the sub_section select tag, adds my default blank line, and than append the id and name values from the returned json object.

Using Firefox firebug extension will make your life much easier in debug this, in particular confirming the ajax request fires off the way (and to the url) you expected, and returns what you expect.

Update : thanks to Tom Meinlschmidt‘s feedback I’ve added “.sort_by{ |k| k[‘name’] }” to the function in the sub_sections controller to return the results in order by the sub section name, as it should have been. (and I fixed up a typo!)

19 thoughts on “Building a Cascading Drop Down Selection List for Ruby on Rails with jQuery Ajax

  1. Pingback: Building a Better Drop Down Selection List for Ruby on Rails | False Positives

  2. hmm, using @subsections = SubSection.find( :all, :conditions => “section_id = #{(params[:id]}” ) is a bit obsolete, better to use named_scopes or as you’re using later than, find_all_by_section_id …

    btw – valid :condition code is “:conditions => {:section_id => params[:id]}

    and names_scope in model could be:
    named_scope :by_section, lambda {|id| {:conditions => {:section_id => id}}
    and optionally add some sorting support
    named_scope :sort_by_name, :order => :name

    and use as SubSection.by_section(params[:id]).sort_by_name

  3. Excellent! Thank you very much, this give me a complete idea of the integration between jQuery and Rails 3. It’s hard to find up to date tutorials!

  4. Great tutorial!

    Sadly, I am getting “RoutingErrors”.

    Started GET “/sub_sections/for_sectionid/4?_=1295344413000″ for 127.0.0.1 at 2011-01-18 10:53:33 +0100

    ActionController::RoutingError (No route matches “/sub_sections/for_sectionid/4″)

    Do I have to make any additional settings in the route.rb ?
    How did you configure the routes for your example ?

    • Hey Jay!

      I just recreated the example without any problems.

      The route.rb was that created by running the scaffold generator for the various objects (section, subsection, gallery )

      map.resources :galleries
      map.resources :sub_sections
      map.resources :sections
      map.connect ‘:controller/:action/:id’
      map.connect ‘:controller/:action/:id.:format’

      and the details I’m seeing in the console look similar to yours “http://127.0.0.1/sub_sections/for_sectionid/4?_=1295389834815″

      possible typo’s in the route.rb or the SubSectionsController ? Also, always restart the server if you have made changes to the routes.rb.

      glad it was useful and I hope you get your error resolved.

  5. Hi Ian!

    Thanks for checking. I figured it out on my own. I just made a new regular route and it worked:

    match ‘/sub_sections/for_sectionid/:id’ => ‘sub_sections#for_sectionid’

    Now it’s working like a charm! :-)

    Thank you very much for this great tutorial.

  6. I get the following when I select a category, however, the subcategory drop down does not populate. What might I be doing wrong?

    Started GET “/subcategory/for_categoryid/1?_=1310287713956″ for 127.0.0.1 at Sun Jul 10 01:48:33 -0700 2011
    Processing by SubcategoriesController#for_categoryid as JSON
    Parameters: {“id”=>”1″, “_”=>”1310287713956″}
    Subcategory Load (0.2ms) SELECT “subcategories”.* FROM “subcategories” WHERE ( category_id = ‘1’)
    Completed 200 OK in 19ms (Views: 3.8ms | ActiveRecord: 1.1ms)

    Thank you for your help in advance!

  7. Pls my subsection is not populated too, what could be wrong and @ jon i dont understand what u mean by ‘i missed a name change’

    i used ‘ “– Select a Category –” %>’

    • Dayo :

      I think that @jon is saying that he corrected for the typo I fixed.

      So things to check if your subsection is not populated :
      a) is the ajax request to the ‘/sub_sections/for_sectionid’ routine getting fired?(look in the logs as per @jon’s first post) and are there any results (put a debugging message to show the number of rows being returned).

      b) If it’s not even getting that far then you will ned to put in some javascript alerts to see how far it is getting. ( using “Select a Category” shouldn’t be a problem if you trap for it a the top of the ” $(“select#gallery_section_id”).change(function() …”

      c) if you are getting results from the ajax but is not populating then it may not be of the structure it’s expecting.

      hope this helps

  8. I have got all of this working fine except that the sub_sections do not populate when I select a section. However, I have validates_presence_of on my model so the form cannot submit without certain things being filled in (including sub_sections).

    If I hit submit before I fill the form correctly I get an error but also the sub-categories select drop down also becomes populated!!

    Any ideas why the sub categories are not automatically populating?

  9. I should add that I get the ajax request as per Jon’s comment…

    Started GET “/sub_sections/for_sectionid/6?_=1332352204848″ for 127.0.0.1 at Wed Mar 21 17:50:04 +0000 2012
    Processing by SubSectionsController#for_sectionid as JSON
    Parameters: {“id”=>”6″, “_”=>”1332352204848″}
    SubSection Load (0.3ms) SELECT `sub_sections`.* FROM `sub_sections` WHERE ( section_id = ‘6’)
    Completed 200 OK in 2ms (Views: 0.2ms | ActiveRecord: 0.3ms)

    • Hi Graham,

      I have the similar problem as yours, eventually after I change both j.sub_section.id and j.sub_section.name, to j.id and j.name, seems to work.

      Hope this useful for you.

  10. Hi Kahfei

    Yes, I discovered the same solution. I tried to post that yesterday but the site was down/having problems.

    I have since been trying to find a way to add a 3rd level (sub_sub_section). Cant get it working. Any ideas?

  11. Good night,

    I’m using ruby 1.8.7 and 2.3.4 rail, make the example inetnte Building a Cascading Drop Down Selection List for Ruby on Rails with Ajax and jQuery to select a value from the first list, fill the second with the word “undefined” however when I look at the console I get the following:

    GET Started “/ subcategory/for_categoryid/1? _ = 1310287713956″ for 127.0.0.1
    Processing by SubcategoriesController # for_categoryid as JSON
    Parameters: {“id” => “1”, “_” => “1310287713956”}
    Subcategory Load (0.2ms) SELECT “subcategories”. * FROM “subcategories” WHERE (category_id = ‘1 ‘)

    if I am only the procedure “for_categoryid” from the url shows me the array of values ​​but not because the combo returns me undefined.

    Please if I can help, thanks.

  12. Hi,

    I have one problem, when I try to edit some register I get this error

    “PG::Error: ERROR: operator does not exist: character varying = integer
    LINE 1: … FROM “departments” WHERE “departments”.”directions_id” = 1″

    I change the sections => directions
    and sub_section => departments

    any idea to solve this?

Leave a Reply