A frequently asked question on comp.lang.javascript is how to make linked select elements.
Linked select elements are two or more selects, where choosing a value in one changes the available values in some of the rest.
Example: Selecting a country and town. Selecting the country in one select updates the other select to only contain towns from that country.
This page provides generic code that implements linked select elements.
Linked select elements rely on Javascript. If a browser does not have Javascript available, for technical reasons or by choice, the entire page can be unusable.
As always, when designing a page with Javscript enhancement, one must ensure that it works without Javascript too.
There are two fundmentally different approaches to solving this:
Both solutions have usability issues.
Making all choices available can give a select with 400 towns to choose from. A select element is not the best way to choose from that many different values. Option groups can be used to group the possible values for easier access, but it is still not optimal.
A Wizard-like interface obscures the choices that needs to be made, and requires more round-trips to the server. Also, users are reported not to like them.
A general problem with a select element is that you can only see a small amount of the possible choices at a time. The functionality of a select element can also be achieved with a radio button group. A radio button group makes all choices visible to the user, but uses more space.
As our example, we will select between 25 towns in 5 countries:
Country | Towns | |||||
---|---|---|---|---|---|---|
Denmark | Aalborg | Aarhus | Copenhagen | Esbjerg | Odense | Randers |
England | Bradford | Bristol | Greenwich | Liverpool | London | |
Germany | Berlin | Dortmund | Hamburg | München | Wuppertal | |
France | Avignon | Cannes | Le Havre | Lille | Paris | |
Greece | Athens | Delphi | Olympia | Santorini |
The result we want to send to the server, is a two-letter country code and a three-letter town code, separated by a dash. E.g., the code for Copenhagen in Denmark would be "DK-CPH".
The data sample is deliberatly small to make the examples managable. However, you should try to envision the problems, if there were 400 towns from 20 countries. These are the problems linked select elements are supposed to solve.
If we have no linked select elements, e.g., due to no Javascript support, and we choose to make one large town list, we have one option for each town.
<select name="town">
<option value="DK-AAL">Aalborg</option>
<option value="DK-ARH">Aarhus</option>
<option value="DK-CPH">Copenhagen</option>
...
<option value="GR-SAN">Santorini</option>
</select>
We will use these as the basis of our linked select elements.
Our form needs a an input element with the name "town" that sends the town code. We want to add an extra select element, where one selects the country, and then modify the town select when the country changes.
To support browsers without Javascript, we make the extra select
start out hidden, and the town select starts with all possible towns
in it. We use the CSS property visibility
for
this, and it starts out hidden. If targeting only modern browsers,
one can use the property display:none
instead.
<select id="ctrySel" style="visibility:hidden">
<option value="DK" selected="selected">Denmark</option>
<option value="UK">England</option>
<option value="DE">Germany</option>
<option value="FR">France</option>
<option value="GR">Greece</option>
</select>
<select id="townSel" name="town">
<option value="DK-AAL">Aalborg</option>
...
<option value="GR-SAN">Santorini</option>
</select>
The Javascript code start by removing the options that are from the wrong country. We store references to all the options in an array, for later access. It then makes the country select visible.
function initLinkedSelect(from,to) {
/* Array for storing option text/value pairs */
var = new Array();
/* Make country select visible */
(from.style || from).visibility = "visible";
for (var i=0; i < to.options.length; i++) {
/* Save text and value of original options */
[i] = new Array(to.options[i].text,to.options[i].value);
}
/* When the country selection changes... */
from.onchange = function() {
/* The code for the selected country */
var fromCode = from.options[from.selectedIndex].value;
/* Remove current options */
to.options.length = 0;
/* Run throught all options... */
for (i = 0; i < .length; i++) {
/* If the option starts with the selected country code */
if ([i][1].indexOf(fromCode) == 0) {
/* Add the option to the town select */
to.options[to.options.length] = new Option([i][0],[i][1]);
}
}
/* Select the first of the new options */
to.options[0].selected = true;
}
/* Update the town select now */
from.onchange();
}
All you then need to do, is to call initLinkedSelect
with
the country and town selects as arguments. You can find these elements
either through a form's elements collection, or using document.getElementById
. The initialization function can be called either in the page's onload handler, or in a script tag after the
You can try turning off Javascript and reload to see the difference.
This solution has a problem in Netscape 4. If the page is reloaded, Netscape 4 keeps the contents of the select elements, but reruns the initialization code. That results in all other countries' towns being gone.
The previous solution was based on the data in the select elements at the beginning, and it build an array of data from that. Instead, one can start out with a data array and ignore the options already in the select.
A data array can be a simple array of arrays. For each option, we need the text and value, and we need a way to match it to the selected country. We can have more data than what ends up in the options, so we don't need to have the country keys as the options' values. To demonstrate that, we use a different type of country codes as keys.
The HTML is almost the same as before, except for the different country codes used as keys.
<select style="visibility:hidden">
<option value="DEN" selected="selected">Denmark</option>
<option value="ENG">England</option>
<option value="GER">Germany</option>
<option value="FRA">France</option>
<option value="GRE">Greece</option>
</select>
<select name="town">
<option value="DK-AAL">Aalborg</option>
...
<option value="GR-SAN">Santorini</option>
</select>
The difference is in the Javascript code. Instead of collecting the options from the select element, we have already made an array of option values:
var optionData = new Array(
new Array("DEN","Aalborg","DK-AAL"),
new Array("DEN","Aarhus","DK-AAR"),
...
new Array("GRE","Santorini","GR-SAN")
);
function initLinkedSelect(from,to,) {
(from.style || from).visibility = "visible";
from.onchange = function() {
var fromCode = from.options[from.selectedIndex].value;
to.options.length = 0;
for (i = 0; i < .length; i++) {
if ([i][0] == fromCode) {
to.options[to.options.length] = new Option([i][1],[i][2]);
}
}
to.options[0].selected = true;
}
from.onchange();
}
This method works without problems in Netscape 4, and has the advantage of disconnecting the keys used to select the correct options, from the values of those options. The disadvantage is that you transfer the same data twice: once in the table and once in the initial options of the select element.
The Wizard-like interface starts out with only the country select element and a "Go" (submit) button. If Javascript is available, the button is hidden, and the town select element is made visible. Apart from that, the code is the same.
Instead of having an invisible element on the page from the start,
it can be added using Javascript document.write
. Then it truly isn't there if
Javascript is disabled. For a Wizard-like interface with a data table,
the document.write
can be based on the
data table and avoid transfering the same data twice.