Creating a Dojo dijit.Tree with checkboxes

Tags:

Dojo provides a useful component called dijit.Tree, which is basically a quite typical tree component. However, it doesn’t do much out of the box, and I needed it to make some tree nodes selectable with checkboxes for my Zend Framework based packageizer script. While Zend Framework has a Zend_Dojo component, it doesn’t quite do trees the way I want yet.

Let’s see how I made the tree play nice with checkboxes and some ajax tricks.

The basic idea

If you’ve checked out the packageizer, you probably already know most of this:

I wanted the tree to load its nodes from the PHP script, separately for each leaf. I also wanted to have “folders” and “files” – folders would contain things, and files would have a checkbox that can be checked to select it. I also needed to be able to track the checked items, so the script would know what files to put in the package.

The dijit.Tree is basically two main components: dijit.tree and dijit._TreeNode. Typically you’d also need a store, like dojox.data.QueryReadStore in my case, and a model, like dijit.tree.ForestStoreModel. By extending some of these, we can easily modify the tree to fit our needs.

Getting to it

You can find the complete source code here.

dijit._TreeNode

Let’s first take a look at the dijit._TreeNode class, which represents each node in a tree. We can easily modify it to contain a checkbox, if certain requirements are met:

dojo.declare('CU.dojo._ChkTreeNode', dijit._TreeNode, {
    setLabelNode: function(label) {
        if(this.item.root || this.tree.model.store.getValue(this.item, 'type') != 'class')
            return this.inherited(arguments);
 
        var chk = dojo.doc.createElement('input');
        chk.type = 'checkbox';
        this.labelNode.innerHTML = '';
        dojo.place(chk, this.expandoNode, 'after');
        this.labelNode.appendChild(dojo.doc.createTextNode(label));
        this.checkNode = chk;
        this.expandoNodeText.parentNode.removeChild(this.expandoNodeText);
    }
});

The above code checks if the item is of type “class”, and inserts a checkbox after the expando (the +/- sign). The setLabelNode function gets automatically called when the tree node gets created.

But why class? This is because the data received from the PHP script assigns type as “class” for all classes and “package” for packages, which can contain classes or other packages. If you wanted to add checkboxes to all nodes, you can simply remove the if clause.

dijit.Tree

This is going to be a bit longer, so I’m just going to explain it and show some snippets. You can get the complete code and follow from there.

In the declaration for CU.dojo.ChkTree, we can see the _createTreeNode function. This is a function in dijit.Tree, which gets called when the code needs to create a new tree node, so this is the place where we need to return our shiny new ChkTreeNode.

The getIconClass function determines the CSS class name for the icon of the tree node. This is modified to output “package” or “class” so we can use CSS styles to modify how the icons look. Same is done for the getLabelClass function.

The onNodeChecked and onNodeUnchecked function’s purprose is to serve as a base which you can dojo.connect to, or just simply override to get a notification when a node is checked or unchecked.

onClick: function(item, node) {
    if(item.root || this.model.store.getValue(item, 'type') == 'package')
    {
        this._onExpandoClick({ node: node });
    }
    else
    {
        if(!node.checkNode)
            return;
 
        if(this._clickTarget && this._clickTarget.nodeName != 'INPUT')
            node.checkNode.checked = !node.checkNode.checked;
 
        if(node.checkNode.checked)
        {
            this.onNodeChecked(node);
        }
        else
        {
            this.onNodeUnchecked(node);
        }
    }
},

The onClick is a bit more tricky. It gets called when any node is clicked, but we want to have a bit different behavior depending on the node type. If it’s a package, we want it to be expanded or minimized on click, but if it’s a class, the checkbox should be toggled.

The first block checks if it’s a package, and calls the _onExpandoClick function, which toggles the node. If it’s a class, we check if it actually has a checkbox node in it or not. Then, we need to check if what was clicked was the checkbox: If the checkbox was the click target, it was already toggled, and toggling it again would make it impossible to use. Finally, calls are made to the related event functions.

And lastly in the ChkTree, we have _onClick, which is needed for the checkbox to function correctly. Unless we have this function in, every time we click on the checkbox, it would focus the label, and it would not be possible to check it by clicking the checkbox directly, only by clicking the label and thus invoking the onClick handler for it.

dijit.tree.ForestStoreModel

And the last extended class, CU.dojo.QueryStoreModel, is required to add some custom behavior to the store so the tree works correctly. We modify getChildren to use some custom logic to fetch the child nodes from the store, as the backend PHP code requires some additional parameters to be able to correctly determine the nodes.

We also override mayHaveChildren to allow children for packages but not for classes.

Usage

Using this class is very similar to how you would normally use a dijit.Tree. Here’s a simplified example from the packageizer:

    var store = new dojox.data.QueryReadStore({ url: '/pack/1.6' });
    var model = new CU.dojo.QueryStoreModel({
        store: store,
        query: { package: '', format: 'json' },
        rootId: 'treeRoot',
        rootLabel: 'Packages',
        childrenAttrs: 'children'
    }, 'store');
    var tree = new CU.dojo.ChkTree({ model: model }, 'tree');
 
    dojo.connect(tree, 'onNodeUnchecked', function(node) {
       alert('A node was unchecked!');
    });
 
    dojo.connect(tree, 'onNodeChecked', function(node) {
        alert('A node was checked!');
    });
 
    tree.startup();

Here’s an example of the JSON sent by the PHP backend:

{
 "numRows":3,
 "items":[
  {"name":"Zend_Auth_Storage_Exception","label":"Zend_Auth_Storage_Exception","parent":"Zend_Auth_Storage","type":"class"},
  {"name":"Zend_Auth_Storage_NonPersistent","label":"Zend_Auth_Storage_NonPersistent","parent":"Zend_Auth_Storage","type":"class"},
  {"name":"Zend_Auth_Storage_Session","label":"Zend_Auth_Storage_Session","parent":"Zend_Auth_Storage","type":"class"}
 ],
 "identity":"name"
}

In closing

In the end, this required less code than I expected in the beginning. I’ve done something similar using the Yahoo UI library, and it was a bit more complex. What Dojo lacks in documentation (at the moment of writing this), it has in design and flexibility.

You can see a live example of the tree here.

Hope you found this educational or useful =)