Friday 20 January 2012

Rails 3.1 Linked Dropdown / Cascading Select

I'm currently using Rails 3.1, and I've found that although Rails has helper methods for a huge number of things, it does not have anything to help me create linked dropdowns (I've seen these referred to as cascading dropdowns and linked selects also), despite this being something I use in a lot of projects.

I created a solution previously that was very flexible, which involved using the jQuery Flexbox plugin and making AJAX calls when a selection changes, which returned JSON objects to populate child menus.  While this would have worked in my current project, it would be overkill, so I set about looking for a solution that is a little simpler, and came up with something that I will certainly want to use again.

The solution I have implemented here involves the jQuery Chained plugin, and a little work in my controller and views to get this working in Rails 3.

I first copied the plugin to my assets/javascripts directory, to include it in the project (no need to include it specifically in Rails 3.1, the application.js manifest will do that for us).

I should point out here that my current project is a home automation controller, so the simplest way to demonstrate this is with my Room and Device models (a Room has_many devices, and a Device belongs_to a Room). As such, I want to first have the user select a room, and then have the second dropdown only display devices that are in that room.

In my view, I created the room and device select boxes:

<%= select("room", "name", options_for_select(@rooms_for_dropdown), {:prompt => 'Select Room'}) %>

<%= select("device", "name", options_for_select(@devices_for_dropdown), {:prompt => 'Select Device'}) %>

In my controller, i created some of the variables I used above.

@rooms = Room.all
@rooms_for_dropdown = []
@rooms.each do |i|
  @rooms_for_dropdown = @rooms_for_dropdown << [i.name,i.id]
end
    
@devices = Device.all
@devices_for_dropdown = []
@devices.each do |i|
  @devices_for_dropdown = @devices_for_dropdown << [i.name,i.id,{:class => i.room.id}]
end

You will note that I've used :class in the array of devices that I'm returning - this is essential for using the jQuery chained plugin. What is happening is that we are actually returning an array of all devices available to the user, but each of the options has a class according to what room it belongs to. This allows the plugin to dynamically hide the options that are not relevant, without making an AJAX call (it should be noted that if Javascript happens to be turned off, all options will be available - while this wouldn't be ideal, it means that the application won't be completely broken without JS).

The last thing I need to do is tell the plugin what select boxes are chained, which I can do in a file called jquery.chained_dropdowns.js (again, no need to specifically include this file in Rails 3.1, but be careful of the naming - we need to ensure that it is included after jQuery and the jQuery chained plugin, and Rails 3.1 includes the files alphabetically, so calling it chained_setup.js, for example, would result in an error):

$(document).ready(function(){
 $('select#device_name').chainedTo('select#room_name');
});

That's it! Easy, wasn't it? One thing to note here is that we are including all of the available options in the html - if you have an enormous number of options (or if you don't want users to be able to pick up your complete option list), this might cause an issue - I'll come to this in a later post.

3 comments:

  1. hello.. where should i locate this script:
    $(document).ready(function(){
    $('select#select1_name').chainedTo('select#select2_name');
    });

    ReplyDelete
  2. Thank you for shared this example. I have many hours try this, and you painted really simple.

    ReplyDelete