Jan 12, 2010

Cascading Select Boxes using jQuery

Today, we came across a need for a set of cascading select boxes to filter through a list of Organizations, Territories and Location. When I've come across this need in the past I tended to use ajax calls to load a new list into the select box based on the selection. While the ajax option is good for very large sets of data to keep the initial load time of the page down, it creates a few minor issues for smaller data sets.

  1. Returning a short list of options via ajax can be slow due to having to make a separate call to the server.
  2. Additional requests being sent to what may already to a taxed server
  3. There can be a good amount of server-side code that needs to be written to handle and respond to the ajax requests.

For our smaller data set we felt it would be more beneficial to start by loading all available options during page load and use jQuery to quickly filter and show only the options we need from the options already loaded into the browser. I came up with this function to do just this.

function cascadeSelect(parent, child){
 var childOptions = child.find('option:not(.static)');
 child.data('options',childOptions);
 
 parent.change(function(){
  childOptions.remove();
  child
   .append(child.data('options').filter('.sub_' + this.value))
   .change();
 })
 
 childOptions.not('.static, .sub_' + parent.val()).remove();
}

I was shocked at the ridiculously small amount of code needed to provide this functionality. After pre-building may page with a form containing the select boxes and added a class to each option in the child select to define which option from the parent it was associated with ("sub_1" where parents value is "1"), simply call the function with the parent and child defined as jquery objects:

cascadeForm = $('.cascadeTest');
orgSelect = cascadeForm.find('.orgSelect');
terrSelect = cascadeForm.find('.terrSelect');
locSelect = cascadeForm.find('.locSelect');
 
cascadeSelect(orgSelect, terrSelect);
cascadeSelect(terrSelect, locSelect);
And voila:
Organization:
Territory:
Location:

I've posted a full sample html at Snipplr. Feel free to comment and give suggestions there. As with all my code it's licensed as Creative Commons Attribution-Share Alike 3.0 Unported License. Feel free to consume and improve accordingly.

Update: ::face-palm:: In my joy of completing a task that seemed to good to be true, I posted this before realizing it was to good to be true. At this time I've only had a chance to test this code in Firefox. After an attempt to show it off, it appears to not work properly in WebKit browsers and maybe others. I will run it through the full gauntlet soon and do by best to ensure cross-browser compatibility for all browsers officially support by jQuery.

Update 2: After a little work and a better understanding of what doesn't work to "hide" options in browsers other than Firefox, I now have code that does just as I explained above but works cross-browser. I have updated the code above as well as the full example on Snipplr. Again, please feel free to give any feedback you might have.

Confirmed Working:
  • Firefox (3.6 RC1, 3.5, 3.0, 2.0, 1.5, 1.0)
  • IE (8, 7, 6 SP2, 6)
  • Safari 4
  • Opera (10, 9)
  • Chrome 4

19 comments :

Matt Doar said...

Nice work this.

Anonymous said...

Just what I was after, thanks for sharing

Kris said...

Thanks, super helpful. Is there an easy way to change the code to act more like a filter than a menu, i.e. instead of hiding by default all options are shown and then once a parent is select the children are filtered accordingly?

Jeremy Satterfield said...

Haven't tested this extensively, but this sounds like the direction you want to go. Should get you started anyway.function cascadeSelect(parent, child){
var childOptions = child.find('option:not(.static)');
child.data('options',childOptions);

parent.change(function(){
childOptions.remove();
if(this.value > 0){
newOptions = child.data('options').filter('.sub_' + this.value);
}else{
newOptions = child.data('options');
}
child
.append(newOptions)
.change();
})

//childOptions.not('.static, .sub_' + parent.val()).remove();
}

Callaway said...

Does anyone know how to make this code with with Mobile Safari's "next" button when in form mode? When a selection is made, the next field is not updated unless you close the chooser. In other words, the "next" button breaks the script.

Ryan Ward said...

This won't work with spaces, any suggestions?

Jeremy Satterfield said...

Ryan,

I'm not quite sure what you mean by "This won't work with spaces."

Class names cannot contain spaces. Having a class attribute with spaces in it gives that element one separate class for each word.

For the value attribute of the select, I tend to use primary keys from a database and believe that generally, but maybe not always, primary keys shouldn't contain spaces either.

Maybe you have something else in mind for your use case, but I hope this will clears it up a bit.

Anonymous said...

Just what I needed. Thanks very much.

Anonymous said...

Thanks a lot, nice and neat.

Anonymous said...

This little piece of wonder works (Oct 19, 2011) in Firefox 7, Opera 11, Chrome 14 and IE4Linux. Thanks for making other people's life easier!

Best,
Raul

Erik said...

Perfect piece of code, works as advertised!

Jeremy Satterfield said...

"Perfect" is a bit generous, but glad it works you.

Nicole said...

Hi there -- this code is working great, but I'm no Javascript expert. Is there an easy way to have the last item selected show/hide a div? Thanks!

Anonymous said...

Thanks ............
you really saves me. And the best part is I can makes it work with option selected for editing needs.
once again, thanks

Anonymous said...

I was trying to put 1 more level in the cascading boxes, but not sure what exact code is needed.

Jeremy Satterfield said...

I would think adding something similar to thise should be sufficiant.

officeSelect = cascadeForm.find('.officeSelect');

cascadeSelect(locSelect, officeSelect);

lee hosun said...

Thank you. if registed value is setting as ex. 'terrSelect' is not change. How to solve this problem.

ex) $('.orgSelect option:eq(2)').attr("selected","selected");

Jeremy Satterfield said...

Lee,

Since we are binding the functionality to the select using the .change() method, we can simply use the .change() method to trigger it after the correct option has been selected.

ie,
$('.orgSelect option:eq(2)').attr("selected","selected");
$('.orgSelect').change();

Bessy said...

Thanks. I just had Organization and Territory select boxes and would like to change some code.There is "Organization 4" in Organization dropdown list, and no corresponding items in Territory select box, the Territory need to display "No result" . How can I do this? Thanks a lot.