Unobtrusive dynamic select boxes

June 23, 2011: Internet Explorer 9 introduced a new garbage-collection-related bug that causes this approach not to work anymore. I've created a new HTML5-based example that works around this issue by creating an intermediate object as data-representation of the dependent select box and swapping out entire select elements dynamically (please view source).

A lot of Websites contain forms with dynamic select boxes; a combination of two or more select boxes that are related to each other in such way, that when a user selects something from one select box, another is automatically filled with only those options that apply to this selection.

There are many different ways of creating dynamic select boxes. An often used technique is to simulate them by using page-by-page form submission and a server-side script to return the related options. The disadvantage of this method is that a roundtrip to a server takes time and may impact the user experience. You could use XMLHttpRequest to achieve the same in a more dynamic fashion, however XMLHttpRequest doesn't ensure that you will not experience a lag in requiring the data from the server. And besides this, in both cases you would need a reasonable amount of server-side code to solve a client-side problem.

Back to the client. You could use JavaScript and store all options and their relationships in arrays. However, besides that I am not a big fan of duplicating markup and content in arrays, often these solutions don't degrade very well in browsers that have JavaScript disabled or have no JavaScript support at all. So we need a solution that suffers none of these issues, one that creates real dynamic select boxes, operates at the client-side, is accessible and only defines an option once. Enter unobtrusive scripting and progressive enhancement.

The basic ingredients for any unobtrusive and progressively enhanced solution

  • Start by creating all structural/semantic markup and content needed for a fully functioning page. This foundation will represent the most accessible version of your Web page, one that is meant for the biggest audience possible. Please note that neither behavior nor styles will be attached at this point in time.
  • Add id and class attributes to reference your markup and content, and enhance your markup with the desired presentation and behavior. Only add JavaScript to improve a page's usability, in any other case you should rethink to apply it at all.
  • Keep your structure/content, presentation and behavior separated. Put your JavaScript code in an external JavaScript file and attach it when your page is loaded.
  • Before you apply your behavior, use object/feature detection to test if there is enough JavaScript/DOM support.

Creating the foundation

Let's start with a foundation for our solution. As a testcase we will use a dynamic selectbox for differentiating between different brands and types of PDAs. Our markup would look something like:

<form action="#">
    <select">
        <option value="select">Select PDA brand...</option>
        <option value="dell">Dell</option>
        <option value="hp">HP</option>
        <option value="palmone">PalmOne</option>
    </select>
    <select>
        <option value="select">Select PDA type...</option>
        <option value="aximx30">Axim X30</option>
        <option value="aximx50">Axim X50</option>
        <option value="ipaqhx2750">iPAQ hx2750</option>
        <option value="ipaqrx3715">iPAQ rx3715</option>
        <option value="ipaqrz1710">iPAQ rz1710</option>
        <option value="tungstene2">Tungsten E2</option>
        <option value="tungstent5">Tungsten T5</option>
        <option value="zire72">Zire 72</option>
    </select>
</form>

The next step is to add a series of id and class attributes, so we can reference our select boxes and options with JavaScript. First let's add an id to both our select boxes so we can reference them individually. Next we would like our markup to contain the relationship between the different options of both select boxes. This can very easily be achieved by adding a class attribute to each option of the dynamic select box and using the related value of the option of the main select box as the class name.

Some people may note that classes are CSS related, however in my opinion they are designed to offer a mechanism to group and select markup and content for multiple purposes. Just be careful with adding CSS style rules to classes used by JavaScript. By separating presentation and behavior you will end up with better understandable and maintainable code, with a reduced chance on errors in the long run. To avoid a mix-up you could namespace your class names, e.g. class="js_myclassname", but for now we leave this out of the scope of this article.

Our updated markup would look like:

<form action="#">
    <select id="pda-brand">
        <option value="select">Select PDA brand...</option>
        <option value="dell">Dell</option>
        <option value="hp">HP</option>
        <option value="palmone">PalmOne</option>
    </select>
    <select id="pda-type">
        <option class="select" value="select">Select PDA type...</option>
        <option class="dell" value="aximx30">Axim X30</option>
        <option class="dell" value="aximx50">Axim X50</option>
        <option class="hp" value="ipaqhx2750">iPAQ hx2750</option>
        <option class="hp" value="ipaqrx3715">iPAQ rx3715</option>
        <option class="hp" value="ipaqrz1710">iPAQ rz1710</option>
        <option class="palmone" value="tungstene2">Tungsten E2</option>
        <option class="palmone" value="tungstent5">Tungsten T5</option>
        <option class="palmone" value="zire72">Zire 72</option>
    </select>
</form>

Enhancing your markup with behavior

Now our markup is finalized, we are ready to enhance two static and still unrelated select boxes to a related main and dynamic select box, with the ultimate goal to enhance the usability of our Web page. Let's design how we are going to make our select boxes dynamic.

In my first attempt to solve this problem, I created a script that would dynamically show the related options and hide the unrelated ones on page load or when the selection in the main select box would change. To achieve this I would iterate through all options of the dynamic select box and test whether each option's class name would equal the value of the selected option of the main select box. To enhance the usability of the select boxes I would make sure that a descriptive option with value="select" is always shown first.

However after I built and tested my script I figured that it only worked in Firefox/Mozilla browsers. For a to me still unknown reason only Mozilla based browsers support style.display on options. So I had to look for a different solution. Lucky for me that a while ago I stumbled upon a solution that can easily replace style.display constructs.

The attack of the clones

When it comes to real life, most people probably think that cloning is not an aesthetic solution to any problem. However when using the W3C DOM it is a very useful and common tool to recreate existing markup and content. Instead of showing and hiding the options of the dynamic select box directly, we have to make two changes to make things succeed.

First, when the page is loaded we clone the dynamic select box and store it in memory as a backstage resource pool. Second, our earlier designed onload and onchange events would, instead of hiding and showing an option, delete all options of the dynamic selectbox and repopulate it by cloning new options from the cloned select box in memory. In this way we recreate our dynamic select box on the fly.

Still with me? Actually if you are not interested in diving into the DOM and JavaScript, you don't really have to know all of this. One of the advantages of unobtrusive scripting is that it is easily applicable. It's like an ABC: Make sure your markup looks like A, include this JavaScript file B, and as a result you get a page that behaves like our finished example C.

A look under the hood

This is all the JavaScript code needed for our example:

function dynamicSelect(id1, id2) {
	if (document.getElementById && document.getElementsByTagName) {
		var sel1 = document.getElementById(id1);
		var sel2 = document.getElementById(id2);
		var clone = sel2.cloneNode(true);
		var clonedOptions = clone.getElementsByTagName("option");
		refreshDynamicSelectOptions(sel1, sel2, clonedOptions);
		sel1.onchange = function() {
			refreshDynamicSelectOptions(sel1, sel2, clonedOptions);
		};
	}
}
function refreshDynamicSelectOptions(sel1, sel2, clonedOptions) {
	while (sel2.options.length) {
		sel2.remove(0);
	}
	var pattern1 = /( |^)(select)( |$)/;
	var pattern2 = new RegExp("( |^)(" + sel1.options[sel1.selectedIndex].value + ")( |$)");
	for (var i = 0; i < clonedOptions.length; i++) {
		if (clonedOptions[i].className.match(pattern1) || clonedOptions[i].className.match(pattern2)) {
			sel2.appendChild(clonedOptions[i].cloneNode(true));
		}
	}
}
window.onload = function() {
	dynamicSelect("pda-brand", "pda-type");
}

In line 2 we run two feature tests to check if there is enough W3C DOM support to execute the contents of our script. In lines 3 and 4 we obtain references to the main and dynamic select boxes. In line 5 we clone the dynamic select box and store it in memory. In line 6 we obtain the references to all cloned options. In lines 7 to 10 we make two calls to our reusable function called refreshDynamicSelectOptions, one on page load for initialization and one in the onchange event of the main select box.

In lines 14 to 16 we delete all options of the dynamic select box. In line 17 and 18 we create two regular expression objects that we will use later, when we test if our class name either equals "select" or the value of the selected option of the main select box. In the remainder of the function we iterate through all cloned options and perform the tests for each one of them. In case of a match, we clone the option and append it to the dynamic select box.

In lines 25 to 27 we attach our previously defined behavior for our two specific select boxes on page load. You can scale the code in multiple ways. If you would like to reuse the functions over multiple pages, you may want to move the window.onload event handler to the head of your (X)HTML file, like in this example. Be careful not to overwrite existing onload handlers. You can avoid this by using one of the following solutions: 1, 2, 3. Just by adding extra markup and an additional dynamicSelect function call within the window.onload event handler, you can add multiple dynamic select boxes to your page. And finally you can create multiple chained dynamic select boxes.

A note about browser support

The examples don't work in Internet Explorer 5.x/Mac due to its buggy DOM implementation. Now, I personally didn't need to support this browser in the past year and I don't expect that I will ever have to support it in the future. However, if you do need to support it, it is best to prevent that for Internet Explorer 5.x/Mac the contents of our script gets executed. Unfortunately the only way to achieve this is by using a last resort means as browser detection:

function dynamicSelect(id1, id2) {
	var agt = navigator.userAgent.toLowerCase();
	var is_ie = ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1));
	var is_mac = (agt.indexOf("mac") != -1);
	if (!(is_ie && is_mac) && document.getElementById &&document.getElementsByTagName) {
		...

Our updated example and the accompanying JavaScript file.

Special thanks

To Mark Wubben for reviewing this article and helping me to get it in top-notch condition.