Nested select boxes
A common feature of web-gui-s is the use of select boxes to select a "path" in some hierarchial structure. An example is where one selects a country, another select is populated with the possible states, the third with possible cities etc.
Here is a simple snippet of code that manages such a collection of selects.
var level_selects = new Array();
connect(window, 'onload', function () {
var sel = SELECT();
level_selects.push(sel);
connect(sel, 'onchange', partial(update_levels_below, 0));
appendChildNodes('theselects', sel);
get_level_data(0, []).addCallback(function (data) {
if (data) {
replaceChildNodes(sel, map(function (row) {
return OPTION({"value": row[0]}, row[1]);
}, data));
signal(sel, 'onchange');
}
});
});
function update_levels_below(level_no) {
var path = map(itemgetter('value'), islice(level_selects, level_no+1));
var d = get_level_data(level_no + 1, path);
d.addCallback(function (data) {
if (data) {
var sel = level_selects[level_no+1];
if (sel == undefined) {
/* the next level didn't exist, create it */
sel = SELECT();
level_selects.push(sel);
connect(sel, 'onchange', partial(update_levels_below, level_no+1));
appendChildNodes('theselects', sel);
}
replaceChildNodes(sel, map(function (row) {
return OPTION({"value": row[0]}, row[1]);
}, data));
signal(sel, 'onchange');
} else {
/* There is no next level */
if (level_selects.length > level_no+1) {
/* remove the select and all levels below */
forEach(
islice(level_selects, level_no+1, level_selects.length),
function (sel) {
sel.parentNode.removeChild(sel);
}
);
level_selects.length = level_no+1; /* truncate the array */
}
}
});
}
To use this, one must provide:
- a div with the id 'theselects' (or some other id, change accordingly above)
- a function with the signature get_level_data(level_no, path_above)
The function get_level_data is given the number of a level (level_no) and an array of the currently selected values in the levels above (path_above).
The function must return a deferred that will be called back with the level data. The level data is an array of tuples (two element arrays) containing a value (used for the value attribute in an <option>) and a label (the <option> text).
The function can also return a single "null" to indicate that there is no level with the given level_no.
Ideally, this function just forwards the parameters to a loadJSONDoc which builds the lists on the server side.
Here is an example of get_level_data that returns some nonsense hard-coded test values:
function get_level_data(level_no, path_above) {
/* you could call json here */
var retval = null;
if (path_above.length)
var previous_level_id = path_above.pop();
if (level_no == 0) {
retval = [['web','Web'],['db','Database'],['gui','GUI']];
} else if (level_no == 1) {
if (previous_level_id == 'web') {
retval = [['web-python','Python'], ['web-ruby','Ruby'], ['web-php','PHP']];
} else if (previous_level_id == 'db') {
retval = [['db-sqlite','SQLite'], ['db-mysql','MySQL']];
} else if (previous_level_id == 'gui') {
retval = [['gui-python','Python'], ['gui-cpp','C++']];
} else {
retval = null;
}
} else if (level_no == 2) {
if (previous_level_id == 'web-python') {
retval = [['tg','TurboGears'], ['django','Django']];
} else if (previous_level_id == 'web-ruby') {
retval = [['rails','Ruby on Rails']];
} else if (previous_level_id == 'web-php') {
retval = [['symphony', 'Symphony']];
} else if (previous_level_id == 'gui-python') {
retval = [['wxpython', 'wxPython']];
} else if (previous_level_id == 'gui-cpp') {
retval = [['wxwidgets', 'wxWidgets'], ['mfc', 'MFC']];
} else {
retval = null;
}
} else {
retval = null;
}
return succeed(retval);
}
I would provide a complete working example if I had a good place to put it. Meanwhile you can download the attachment below which is a single HTML file. Beware though that it links to the packed mochikit in svn.
