Uncategorized

Hierarchical, Scrollable Javascript Checkbox List

January 24, 2007

author:

Hierarchical, Scrollable Javascript Checkbox List

I needed a way to present the user with a hierarchical list of categories in an ASP.Net application. I also needed the code to be light-weight and the UI to be simple and customizable with CSS. Having AJAX functionality was not important as the number of categories in the list will not be very large. After checking-out several commercial tree components, existing JS libraries and various code snippets, I decided to write my own as I did not find anything that matched my needs.

The resulting “checkboxList” class is simple and easy to use. I have completed the Javascript coding, but still need to wire it up into an ASP.Net component. Below is the working Javascript code with some usage notes.

Usage:

checkboxList(instanceVariableName, trackingFieldId, containerId)

instanceVariableName: Name of variable with a reference to the object
trackingFieldId: The client-side ID of a text field where a list of checked item IDs will be stored delimited by “|”
containerId: The client-side ID of an HTML container element (such as DIV) where the list will be injected

Example: var myList = new checkboxList(“myList”, “trackingField”, “container”);

Adding items to the checkbox list is done using the appropriately named “add()” method as follows:

checkboxList.add(itemId, itemLabel, isChecked[, parentItemId])

Examples:
myList.add(“node1”, “My First Node”, false);
myList.add(“node2”, “My Second Node”, false);
myList.add(“node3”, “Child of First Node”, true, “node1”);

After adding nodes as desired, call the “render()” method to render the checkbox list to the browser:

myList.render();

One important thing to remember is that the “render()” method expects to already find the container element. If the browser has not yet created the container element, then the checkbox list will not be rendered. The result will look something like this:

Cblist

The text field where the values will be stored should ideally be hidden (displayed here just to show the data values). For simplicity, I decided not to include images and instead rely on CSS styling to indicate if a node has children and is expandable. There are three style classes:

node: appearance of the text label
nodeChild: appearance of the SPAN element used for a child node block
nodeParent: appended to “node” for nodes that contain child nodes

Here’s the code:

 
<style>
body
{
    font: 10pt Verdana,sans-serif;
    color: navy;
}
.node
{
    cursor: pointer;
    cursor: hand;
    display: block;
}
 
.nodeChild
{
    display: none;
    margin-left: 16px;
}
 
.nodeParent
{
    font-weight: bold;
}
 
.scrollingList
{
    height: 120px;
    width: 250px;
    overflow: auto;
    border:1px solid #e0e0e0;
}
style>
 
<script language="JavaScript">
 
function checkboxList(instanceName, trackingElementId, containerElementId)
{
    this.instanceName = instanceName;
    this.add = addNode;
    this.render = renderCheckboxList;
    this.allNodes = new Object; 
    this.tracker = document.getElementById(trackingElementId);
    this.containerElementId = containerElementId;
}
 
function node(id, text, checked)
{
    this.id = id;
    this.text = text;
        this.checked = checked;
    this.parentId = null;
    this.show = expandParent;
    this.rootInstance = '';
}
 
function expandParent()
{
    // Expands the parent node causing a node to be displayed
    // This is automatically done when rendering to ensure
    // that all checked nodes are visible
 
    var p = this.parent;
    while(p)
    {
        var el = document.getElementById(p.id);
        el.style.display = 'block';
        p = p.parent;        
    }
}
 
 
function renderCheckboxList()
{
    // Renders all checkboxes
    var checkboxListString = '';
    for(var n in this.allNodes)
    {
        if (!this.allNodes[n].parentId)
            checkboxListString += this.allNodes[n].render(this);
    }
 
    var container = document.getElementById(this.containerElementId);
 
    if (container)
        container.innerHTML = checkboxListString;
 
    this.updateValue(true);
 
}
 
checkboxList.prototype.updateValue = function(display)
{
    // "display" controls if the UI is rendered to ensure that every checked node is
    // visible. If "display" is true, a node's show() method is called.
 
        var checkedString = "";
    for(var n in this.allNodes)
    {
        if (this.allNodes[n].checked)
        {
            if (display)
                this.allNodes[n].show();
            checkedString += (checkedString == "" ? "" : "|") + this.allNodes[n].id;
        }
    }
    this.tracker.value = checkedString;    
}
 
checkboxList.prototype.toggle = function(nodeId, checked)
{
    // If the user clicks a checkbox, the corresponding object in the associative array is updated
 
    for(var n in this.allNodes)
    {
        if (this.allNodes[n].id == nodeId)
        {
            this.allNodes[n].checked = checked;
            break;
        }    
    }
 
    // The tracking field is updated, but the nodes are not auto-expanded
    this.updateValue(false);
 
}
 
 
function addNode(id, label, checked, parentId)
{
    var n = new node(id, label, checked);
 
    // If the specified parentId node is not present, the node is
    // added to the top level
    if (this.allNodes[parentId])
        n.parentId = parentId;
    else
        n.parentId = null;
    n.rootInstance = this.instanceName;
    this.allNodes[n.id] = n;
}
 
node.prototype.render = function(root)
{
    // Renders a node to the browser
 
    
    // Obtain a list of child nodes by looking for nodes which have
    // this node's Id as their parentId
    var childNodes = new Array();
    for(var n in root.allNodes)
    {
        if (root.allNodes[n].parentId == this.id)
            childNodes[childNodes.length] = root.allNodes[n].id;
    }
 
    var numNodes = childNodes.length;
 
    // Compose the HTML string for rendering this node
    // Toggling the checkbox calls the toggle method of the root list object
 
    var nodeString = '';
    nodeString += ';
    nodeString += 'onClick="' + this.rootInstance + '.toggle(\'' + this.id + '\', this.checked)"';
    nodeString += (this.checked ? ' checked' : '') + '> ';
    nodeString += ' + (numNodes > 0 ? ' onClick="showNode(\'' + this.id + '\')"' : '') + '>' + this.text + '';
    nodeString += '';
 
    // If any child nodes are present, recursively render them also
    if (numNodes > 0)
    {
        nodeString += '';
        for(var j=0;j
            nodeString += root.allNodes[childNodes[j]].render(root);
        nodeString += '';
    }
    
    return nodeString;
}
 
 
function showNode(node)
{
    // When a user clicks on a node label
    // this function is called to toggle the display
 
    var objNode = document.getElementById(node).style;
    if(objNode.display=="block")
        objNode.display="none";
    else
        objNode.display="block";
}
 
 
var myCheckboxList = new checkboxList("myCheckboxList","trackingField", "scrollingList");
 
myCheckboxList.add('node1','Node 1', false);
myCheckboxList.add('node2', 'Node 2', false, 'node1');
myCheckboxList.add('node3', 'Node 3', true, 'node2');
myCheckboxList.add('node4', 'Node 4', false, 'node2');
myCheckboxList.add('node5', 'Node 5', false);
myCheckboxList.add('node6', 'Node 6', true, 'node5');
 
script>


"text" id="trackingField" style="width:400px">

 
"scrollingList" class="scrollingList" />