Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

A Dynamic Select Component for Javascript


Jan00: A Dynamic Select Component for Javascript

Steve is a developer at Digital Paper, a company specializing in large-format document distribution over networks. He can be reached at [email protected].


Javascript Compatibility


Web developers are expected to create browser-based interfaces that behave similarly to native-GUI applications. Sometimes, the HTML-supplied components aren't up to the task. ActiveX and Java-based widgets can provide a way around this problem, but often at the cost of compatibility, performance, or security. Client-side scripting languages can be a good compromise between the bareness of static HTML and flexibility of downloadable applets. Javascript is my language of choice for these tasks because it provides a solid, object-based framework for programming, and runs consistently on both Microsoft's Internet Explorer and Netscape's Communicator.

For instance, our company recently provided a custom solution for a client who wanted dynamic select objects -- HTML select lists that would change content depending on the chosen values in other HTML select lists. Users could choose a car manufacturer from one list, then be presented with related car models in the second list, followed by the appropriate options for that car in the third list. The effect is a cascading update of related information on an HTML form.

Designing a GUI component in Javascript is a little different than developing a custom widget in more traditional object-oriented environments. In a language like Java, you select an appropriate base class, extend its functionality with an inherited class, instantiate an object of the new class, and add it to the GUI panel. This approach fails in Javascript for two reasons:

  • The Document Object Model (DOM) Select object is not a Javascript object. You cannot inherit from it in a traditional sense.
  • There is no provision for adding a custom Javascript object as a GUI component on an HTML page.

Fortunately, Javascript offers some powerful mechanisms to get the job done.

Javascript Overview

The core Javascript language specification doesn't mention anything about browsers, HTML, or the Internet. The flavor of Javascript found in browsers is client-side Javascript, which allows interaction with the DOM. Javascript is not a strongly typed language. While types can be attributed to variables at declaration, an untyped variable can hold any Javascript primitive or object value.

Arrays in Javascript are similarly flexible. Arrays are sparse and need not be dimensioned at declaration. A single array can hold heterogeneous types of data, and can be resized at run time. Array values are referenced using a nonnegative integer in square brackets following the array name. Unfortunately, Javascript does not support multidimensional arrays, but these can be approximated with arrays of arrays.

The same array can also contain values with associative indexing (much like the hash data type in Perl). The notation for referencing associative arrays can take two forms:

  • Array notation, which uses an index string (called a "property") in place of a numeric index.
  • Object notation, which uses the dot operator to separate the array name and property string.

Dot operators can be used to chain properties and access deeper levels of the object structure. Any unique string can be used as a property. This becomes especially important during program execution, when property names in array notation can be evaluated at run time. Creating a unique property name from the n-array subscripts can simulate multidimensional arrays. While this technique is painless for assigning and retrieving individual values, cycling through a given row or column of such an array requires string manipulation.

Javascript Objects

The Javascript language specification defines an object as "a container that associates names and indexes with data of arbitrary type." Because arrays and objects are type equivalent, creating object attributes is no more complicated than assigning values to an array. While property names are traditionally used as attribute identifiers, there's nothing stopping you from using numeric indexing as well. An attribute can be any value, including another object. As luck would have it, Javascript functions are objects as well. Javascript functions can be used three ways. First, they can be called like traditional functions or subroutines, and do not need to return a value. Second, they are full Javascript objects, which means they can be assigned properties. Finally, the function name without trailing parentheses is a function reference, which can be assigned as an attribute of any object -- including other functions. Function references assigned to an object serve as methods of that object.

Objects are prototype based, so there is no idea of a class or instance of an object. Instead of creating an immutable class definition and instantiating it, the object is built through a function called a "prototype." The prototype can be any valid Javascript function, and does not need to define all properties that the object may have over its lifetime. Objects are constructed using the new operator followed by the prototype function. The newly created object contains a reference to its prototype as a hidden attribute. Once constructed, the object can be modified directly -- attributes and methods can be added and removed at run time.

One obstacle still remains -- the DOM Select is not a Javascript object. How do you modify its behavior? Client-side Javascript has built-in objects corresponding to the DOM, which includes GUI objects such as the DOM Select. These built-in Javascript objects are created when the browser loads a page containing DOM objects. With the exception of certain read-only properties and custom behaviors, these Javascript objects can be modified like any other.

Component Design

Listing One is the Javascript source of the example component presented here. The objective in building this component was not just to implement a dynamic select object (DSO), but to create a flexible, customizable component that you can use in your web pages. Ideally, customizing the DSO should involve no Javascript programming at all. To that end, the component really has two interfaces -- one for developers (embedded in the Javascript itself) and another for the user interface (embedded in the HTML page).

Figure 1 is a class diagram of this Javascript component. The DSO's dependency on the Javascript Select object is analogous to an inheritance relationship. The ActionManager contains the action strings associated with its parent DSO. The DSOs are contained by the DSManager, which manages the group relationships. The OptionList object wraps an array of Javascript Option objects, which are peers of the DOM Option object. Each DSO contains a reference to a single OptionList, but this reference changes as the actions dictate. The OLManager class provides a simple container for the OptionLists.

The component lets DSOs be related to others on the same HTML form, other forms, or even themselves. Each item selected on a DSO can be assigned separate -- or multiple -- actions. Each action represents the assignment of a new OptionList to a DSO, specifying a default value in the new list. The cascading effect is achieved by defining the DSOs as part of one or more groups. Groups are ordered lists of DSOs. A change in one DSO will only affect the DSOs listed beneath it in a group list. OptionLists are modeled as separate objects that are not associated with a particular DSO. In the program's original design, DSOs contained the OLManager as a property, much like the ActionManager. Because each DSO only references a single OptionList at any given time, and different DSOs could display the same OptionList (such as a blank list), it made more sense to have a single OLManager contain one set of OptionLists. This arrangement follows the reasoning that actions are intrinsic attributes of Select, while the option lists are not.

Swapping OptionLists between DSOs is the heart of the program. The Javascript Select has an array property named options, which contains Javascript option objects. Each option object has a value and text property, mirroring the values in the HTML OPTION tag. Assigning null to the options property will clear the select of its options. Adding a new list to a Select is a little trickier. At first glance, it would seem that simply assigning a new array of option objects to the options property would be all that is needed. Indeed, this approach works for Internet Explorer. Netscape browsers require a different approach (see the accompanying text box entitled "Javascript Compatibility").

Another section of the code worth examining is the traversal of the multidimensional array in the ActionManager. An ActionManager contains an array of all the actions that can be executed by its parent DSO. However, each action is dependent upon two other factors: The active list and item combination that triggers the event. Because more than one action can be assigned to the list/item combination, a third index is required. Again, you can flatten this 3D array into a 1D array by using a concatenation of the indices as the array index (or object property name). Why? You could perform the same task by making three nested arrays with numerical indices, but for arrays-within-arrays to work properly in Javascript, their dimension sizes need to be defined at declaration. Because I wanted to maintain the flexibility of adding actions on the fly, I opted for the flattened array. The array traversal takes place in the ActionManager.getActionsFor() method. Since the ActionManager already has the context of the DSO, it takes the list and item just selected as parameters. The action strings begin with a list and item alias, followed by a numerical index. A for loop cycles through the numerical indices, checking to see if any actions have the same list/index substring as the parameters. If a match is found, the action is added to an array returned by the function. If I cycled through the second index of the array instead of the third, more parsing would be required, but the technique remains the same.

HTML Page Layout

When the page containing the component is first loaded, the document's onLoad() event is triggered, calling the component's dSelectInit() function. This creates the objects defined by the user database. Once the data structures are initialized, the DSOs wait for calls from their peer's onChange() event. As you might expect, the onChange() event for a Select object is triggered when users change the active item in the list. The DSO then performs all actions related to the new list value by invoking executeActions() through its ActionManager. These actions are performed by calling the parent DSO's changeList() function. To account for any cascading effects of the change, the onChange() event then makes repeated calls to executeActions() for DSOs in the originating DSO's group. Only DSOs listed after the original DSO will be affected.

The user interface on the HTML form is constructed of several parts. First, the component must be included in the page. An elegant way to do this is through the <SCRIPT SRC=> HTML tag, which works like an #include directive for client-side scripts. It hides the code from end users, and as an added bonus, puts the script in the browser cache. Once loaded, any page using the component just uses the cached script. To activate the component, the HTML document's onLoad() function must contain a call to dSelectInit(), the component's constructor. And finally, the user database must be defined.

Because client-side Javascript cannot read or write files, the user database has to be stored in a format that the component can access. Hidden forms provide a nice, nonXML way to embed data in an HTML page. A hidden form is an HTML form containing input elements of TYPE=''HIDDEN.'' Each hidden input element provides two attributes: a name and value. Using character-delimited strings, you can pack a lot of information in those two attributes. Javascript has a split() function (much like Perl) that makes parsing these strings a trivial matter. Each major object (DSO, OptionList, and Action) is defined in a single hidden input element.

User Database Definition

When the HTML page is first loaded, the dSelectInit() function looks for elements with a NAME attribute that begins with D:, L:, or A:. This tells the function which type of object to construct. The definitions can be in any order, provided a DSO is defined before any of its Actions. OptionList objects are independent of the other two, and can occur anywhere in the form. The sample application defines DSOs, then OptionLists, and finally Actions.

DSO definitions are divided into two parts, corresponding to the NAME and VALUE fields of the hidden input type they occupy. Because the target audience for using the component includes HTML authors and end users, my reference to other objects will be in terms of string aliases. I originally built object definition using numeric references to other objects, but I found using names instead of numbers more intuitive. The NAME field of a DSO definition contains the object type flag: D, followed by the group name associated with the Select objects listed in the VALUE attribute. The syntax of the VALUE attribute is: formname.selectname|OptionListAlias. The component then constructs a DSO for the DOM Select object named document.formname.selectname, and adds it to the DSManager. If there is more than one DSO in a given group, they are separated by commas. The sample application defines a DSO group:

<INPUT TYPE=''hidden" NAME=''D:g1'' VALUE=

"search.product|typeList,

search.type|blankList,

search.subtype|blankList">

The group is named g1, and contains three Selects. Each Select is in the form named "search." All Selects defined as part of the same group do not need to be in the same input tag, but all Selects in an input tag will be part of the same group. Although not shown here, DSOs of the same group can belong to different forms.

The OptionList definition NAME attribute contains the object type flag: L, followed by the OptionList alias. The VALUE attribute contains a comma-delimited list of the individual items of the list. The item lists are defined as [submitValue|]itemText[*]. In its simplest form, the item list is just a list of strings that will appear in the OptionList. You can specify the value sent by the DSO when a form is submitted by preceding the item text with the submitValue followed by a pipe (|). If no separate value is defined, the itemText is used as the submitValue. An optional asterisk designates that item as the default. Here is an OptionList definition from the sample application that incorporates those options:

<INPUT TYPE=''hidden'' NAME=''L:book TypeList''

VALUE=''FIC|Fiction*,NFC|NonFiction, PER|Periodicals, ''>

The first item in the list displays the text Fiction, returns the value FIC on form submission, and is the default-selected item for this list. The Action definition uses the NAME and VALUE attributes to describe a cause-and-effect relationship between DSOs and OptionLists. The NAME attribute contains the object type flag: A, followed by a DSO alias (which is the same as the formname.selectname string in the Group/ DSO definition), an OptionList alias, and an itemText. The itemText can be replaced by a wildcard (*) stating any item selected from this list will trigger the event. The VALUE attribute describes the change that will occur when the action is executed. The format for both NAME and VALUE is DSOAlias,OptionListAlias,itemText. For example, the action definition

<INPUT TYPE=''hidden''

NAME= ''A:search.type:musicTypeList:*''

VALUE=''search.subtype,blankList,''>

is triggered when a user selects any value from the musicTypeList on the select named type on form search. The result is the assignment of blankList to the select named subtype on form search. The blank (space) item is selected. As this example shows, properties of Javascript object can contain spaces--indeed, a space is a valid object property name.

Sample Application

As Figure 2 shows, the sample page has the beginnings of a search form -- four DOM Select boxes on two HTML forms (the HTML code for the sample page is available electronically; see "Resource Center," page 5). The top three Selects are part of the same group, so changes can propagate from product through type to subtype. (One Select will only affect another Select if two conditions are met: First, there must be an action relating the first Select to the other, and both Selects must be part of a common group.) The Select on the bottom of the page mimics the functionality of the top three, but accomplishes it in a single select box. Because of this, the form element will only return one value (for the single DSO) when its form is submitted. I wouldn't necessarily recommend this as a GUI element, but it's nice to know you can use it if you want to. Another possibility not shown in the example application is the idea of a dependency tree. By including a DSO in more than one group, you can create a branched cascade, in which the effect of a single change can propagate along more than one group. I couldn't think of a practical use for this, but let me know if you find one.

Conclusion

Even if Javascript is not a first-class object-oriented language, it does a good job of impersonating one. There is no reason that you couldn't apply object-oriented design techniques to client-side scripting. Hopefully, the analysis of this component has given you another approach to use in your web design projects. If you would like to see the sample code in action, go to http:// www.digitalpaper.com/ddj/dynamic.html.

References

Flanagan, David. Javascript: The Definitive Guide, Third Edition. O'Reilly & Associates, 1998.

Goodman, Danny and Brendan Eich. Javascript Bible, Third Edition. IDG Books Worldwide, 1998.

DDJ

Listing One

// Copyright 1999, Stephen C. Johnson
function ds_onChange()
{
    var ga;
    var flag;

    // Cycle through action lists for the select
    this.actionManager.executeActions(this.currentList, 
        this.options[this.selectedIndex].text);
    // loop through all groups
    for(var g in dsm.groups)
    {
        ga = dsm.groups[g].split(",");
        flag = false                
        for(var i=0; i < ga.length; i++)
        {
            if(ga[i] == this.alias) 
                flag = true;
            else if(flag)
            {
                var s = dsm.dynamicSelects[ga[i]];
                s.actionManager.executeActions(s.currentList, 
                    s.options[s.selectedIndex].text);
            }
        }
    }
}
function ds_changeList(_listName, _itemName)
{                               
    var o = olm.optionLists[_listName];
    var p = this.options;
    
    p.length = 0;
    for(var i=0; i < o.optionArray.length; i++)
    {
        p[p.length] = new Option(o.optionArray[i].text, 
            o.optionArray[i].value);
    }
    if (_itemName == null)
        o.defaultItem
    else
    {
        for(var j=0; j < p.length; j++)
            if(p[j].text == _itemName) break;
    }
    this.currentList = _listName;
    this.selectedIndex = j;
}
function dynamicSelect(_htmlSelect)
{                           
    _htmlSelect.alias   = _htmlSelect.form.name +"."+ _htmlSelect.name;
    _htmlSelect.actionManager   = new ActionManager();
    _htmlSelect.currentList     = null;

    // if size is > 1, use the click method instead
    // of the change method
    if(_htmlSelect.size > 1)
        _htmlSelect.onclick = ds_onChange;
    else
        _htmlSelect.onchange = ds_onChange;
    _htmlSelect.changeList  = ds_changeList;
}
// Definition of the OptionList Object
function OptionList(_optionArray, _alias, _defaultItem)
{
    this.optionArray = _optionArray;
    this.alias       = _alias;
    this.defaultItem = 
   (_defaultItem == null)? (this.optionArray[0].text): _defaultItem;
}
// Definition of the OLManager
function olm_addOptionList(_optionList)
{
    this.optionLists[_optionList.alias] = _optionList;
}
function olm_getOptionList(_optionListName)
{
    return this.optionLists[_optionListName];
}
function OLManager()
{
    this.optionLists = new Array();
    this.addOptionList = olm_addOptionList;
    this.getOptionList = olm_getOptionList;
}
// Definition of the DSManager 
function dsm_addDynamicSelect(_dynamicSelect)
{   
    this.dynamicSelects[_dynamicSelect.alias] = _dynamicSelect;
}
function dsm_addGroup(_groupID, _dsString)
{
    this.groups[_groupID] = _dsString; 
}
function dsm_getDynamicSelect(_dynamicSelectName)
{
    return this.dynamicSelects[_dynamicSelectName]; 
}
function DSManager()
{
    this.dynamicSelects = new Array();
    this.groups         = new Array();
    this.addDynamicSelect   = dsm_addDynamicSelect;
    this.getDynamicSelect   = dsm_getDynamicSelect;
    this.addGroup           = dsm_addGroup;
}
// Definition of the ActionManager
function am_addAction(_list, _item, _action)
{
    var r = this.getActionsFor(_list, _item);
    this.actions[_list +"."+ _item +"."+ r.length] = _action;
}
function am_executeActions(_list, _item)
{
    var a = this.getActionsFor(_list, _item);
    for(var i=0; i <  a.length; i++)
    {
        var b = a[i].split(",");
        dsm.dynamicSelects[b[0]].changeList(b[1], b[2]);
    }
}
function am_getActionsFor(_list, _item)
{        
    var r = new Array();
    var startIndex = _list +"."+ _item;

    for(var i in this.actions)
    {
        if( (i.substring(0, startIndex.length) == startIndex) ||
             (i.substring(0, (_list.length+2)) == _list+".*"))
        {
            r[r.length] = this.actions[i];
        }
    }
    return r;
}
function ActionManager()
{
    this.actions = new Array();
    this.addAction      = am_addAction;
    this.executeActions = am_executeActions;
    this.getActionsFor  = am_getActionsFor;
}
// Initialize the objects
function dSelectInit()
{
    dsm = new DSManager();
    olm = new OLManager();

    var n,v,d,l,a;
    var e = document.dynamicSelectConfig.elements;
    
    for(var i = 0; i < e.length; i++)
    {
        var n = e[i].name.split(":");
        var v = e[i].value.split(",");
        var g = null;
        if(n[0] == "D")
        {               
            for(var j=0; j < v.length; j++)
            {                       
                d = v[j].split("|");
                if(dsm.dynamicSelects[d[0]] == null)
                {
                    var s = eval("document."+d[0]); 
                    if (s == null) {
                        return;
                    }
                    dynamicSelect(s);
                    dsm.addDynamicSelect(s);
                }
                dsm.dynamicSelects[d[0]].currentList = d[1];

                g = (g == null)? d[0]: (g+','+d[0]);
            }
            dsm.addGroup(n[1], g);
        }
        else if(n[0] == "L")
        {
            var o = new Array();
            var def = null;
            var x;

            for(var j=0; j < v.length; j++)
            {
                x = v[j].split("|");
                x[1] = (x[1] == null)? x[0]: x[1];

                if(x[1].substring(x[1].length-1, x[1].length) == "*")
                {
                    def = x[1] = x[1].substring(0, x[1].length-1);
                }
                o[j] = new Option(x[1], x[0]);
            }
            olm.addOptionList(new OptionList(o, n[1], def));
        }
        else if(n[0] == "A")
        {
            if(dsm.dynamicSelects[n[1]] != null)
            dsm.dynamicSelects[n[1]].actionManager.addAction(n[2],
                    n[3], e[i].value);
        }
    }
    // now initialize the lists
    for(var k in dsm.dynamicSelects)
    {
        var x = dsm.dynamicSelects[k];
        x.changeList(x.currentList, 
            olm.optionLists[x.currentList].defaultItem);    
    }
}



Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.