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.

Attachments