Dojo/Dijit Tree with Checkboxes


NOTE:

The new and fully AMD compliant version of the Dijit CheckBox Tree can be found HERE. Please refer to the updated version going forward.

Introduction

Like so many I have been looking for an implementation of a of Dijit tree structure with checkboxes but was unable to find one or at least one that provided the functionality I was looking for so I decided to create one myself. This document describes all the steps required to create a fully functional tree similar to what you are used to see with many software installation packages. I will try to keep things simple and at the same time try to explain some of the dojo and dijit fundamentals when needed. However, I do not claim to be a dojo expert (just a disclaimer).

If you have questions about this article or want to leave a comment please visit my blog which hosts this article as well.

Tree with Checkbox Download Button

Before we get started I want to define the functional requirements for our tree. I know, what do we need requirements for you say, let’s just start hacking. Well in order to set some goals and to make sure everybody is on the same page so I’ll go ahead anyway. The follow-up article describing this tree with multi state checkboxes can be found here. However, in order to understand the inner workings of the tree I would recommend reading this article first.

Function Requirements

The functional requirements I wanted to implement are as follows:

Well, if that’s what you have been looking for too, stick around and continue reading if not Oh well go have fun with what you are doing.

Step 1 - The Development Environment

In order to get started you need to make sure you have version 1.3.2 of dojo installed on your system. I would recommend to get the source version of dojo because it will include some very useful utilities. Dojo can be downloaded from:

http://download.dojotoolkit.org/release-1.3.2

The file you will be looking for is: dojo-release-1.3.2-src both zip and tar versions are available. All source samples in this tutorial are based on dojo version 1.3.2 and will NOT work with version 1.4. I picked version 1.3.2. because it is currently the most used version. I will address the relevant differences between dojo 1.3.2 and 1.4 with regards to the dijit Tree at the end and yes I do also have a version available that does work with dojo version 1.4.

You are also going to need some sort of http server, I’m not going to make any recommendations but I’m using the latest version of Apache. Apache by the way is used by many prepackaged web servers like WAMP or ZendServer CE.

Second, you will need to create the appropriate directory structure to follow this tutorial. If you don’t you will have to make the necessary changes while reading along. The directory structure I use looks as follows:

Directory Stricture

I renamed my dojo directory from ‘dojo-release-1.3.2’ to’ dojotoolkit’, If you don’t have the \dojotoolkit\util directory you did not install the dojo source version, just to make sure, it is not required for this tutorial. Beside the standard dojo directory structure I created two additional directories call ‘datastore’ and ‘tmpdir’. The datastore directory will hold the Json source file we will be using for this exercise and can be downloaded from: http://dojocampus.org/explorer/#Dijit_Tree_Basic
Next, after installing your web server point your web server docroot to the html subdirectory, I would recommend to configure a virtual server , something like this:

Listen 8080
<VirtualHost *:8080>
      DocumentRoot "D:\MyServer\html"
      <Directory "D:\MyServer\html">
             Options All +Indexes +FollowSymlinks
             AllowOverride all
             Order allow,deny
             Allow from all
      </Directory>
</VirtualHost>

If you are not familiar with Apache checkout the Apache httpd website for more information: http://httpd.apache.org/ or if you just need info on virtual host configuration http://httpd.apache.org/docs/2.0/vhosts/.

Ok, now that we have most of the basics out of the way and our system setup it is time to start talking about the standard dijit tree implementation. The latest installment of the dijit tree is implemented using the Model-View-Controller architectural pattern, the general idea here is to isolate any logic from the input and presentation. I’m not going to spend any time on the MVC pattern, there is enough MVC stuff on the web if you really want to get into it you could start at: http://en.wikipedia.org/wiki/Model-view-controller

The most important part you need to know is that dojo uses the following classes to implement the MVC pattern:

Model \html\js\dojotoolkit\dijit\tree\ForestStoreModel.js
View \html\js\dojotoolkit\dijit\Tree.js
Data \html\js\dojotoolkit\dojo\data\ItemFileReadStore.js

In this tutorial we will follow the same MVC model and again if you are not familiar with it I think you will start to like it once you see what it will do for us and our checkbox tree. The final result will show there is relatively little we need to do to the original dijit tree. Ok, its time to start get some coding going.

In case you can't wait until the end of the tutorial you can download the fully documented source files from here.

Step 2 – The Dijit Tree

To get started we need to create a template index.html file in our html directory or default.html for our Microsoft IIS fans. The goal here is to show the original dijit tree first before we start adding our checkboxes. This is also a good exercise to verify that your environment is working.

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  2.       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  3. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
  4.     <head>
  5.         <title>Dijit Tree with Checkboxes </title>
  6.         <link rel="stylesheet" href="/js/dojotoolkit/dijit/themes/soria/soria.css" />
  7.         <script type="text/javascript" src="/js/dojotoolkit/dojo/dojo.js"
  8.                 djConfig="parseOnLoad:true, isDebug:true"></script>
  9.         <script type="text/javascript">
  10.         dojo.require("dojo.data.ItemFileReadStore");
  11.         dojo.require("dijit.Tree");
  12.         function myTree( domLocation ) {
  13.             var store = new dojo.data.ItemFileReadStore( {
  14.                             url: "/js/datastore/countries.json"});
  15.             var model = new dijit.tree.ForestStoreModel( {
  16.                             store: store,
  17.                             query: {type: 'continent'},
  18.                             rootId: 'root',
  19.                             rootLabel: 'Continents'});
  20.             var tree = new dijit.Tree( {
  21.                             model: model,
  22.                             id: "MenuTree"});
  23.             tree.placeAt( domLocation );
  24.         }
  25.         </script>
  26.     </head>
  27.     <body class="soria">
  28.         <div id="CheckboxTree">
  29.             <script type="text/javascript">
  30.                 myTree("CheckboxTree");
  31.             </script>
  32.         </div>
  33.     </body>
  34. </html>

First, at line 6 we add some dojo CSS stuff, dojo has several ‘themes’ one of them is soria. At line 7 we actually start loading dojo and one of the most important things is the djConfig attribute, I’m not going to explain this configuration options so for the time being just use it, it’s all we need.
The option isDebug:true will fire-up Firebug if you are using FireFox or Firebug-Lite if you use any other browser. For development and debugging I personally like to use Firefox just because of Firebug but that’s just me. If you don’t have Firebug and you want to give it a spin go to: http://getfirebug.com/ another very good tool for Firefox would be the Web Developer plug-in. Just as a heads-up it appears the isDebug:true options no longer start Firebug-Lite with dojo version 1.4 you will have to manually add Firebug-Lite to your html scripts if you’re not using Firefox. See http://getfirebug.com/lite.html for details.

Continuing with the code, on line 11 and 12 we include the required dojo files to make the initial tree example work. The first one is the file store, the second is the actual dijit Tree widget. On line 14 I defined my method and on line 15 we create the dojo store followed by the dijit model. As you can see the store is one of the arguments/attributes of the model and this is how we establish the relationship between the model and the store. There are a couple of additional arguments used in the creation of the model:

Query The query specifies what element in your Json file the model is going to ask the store to retrieve when fetching items from the Json file. Required
rootId The Identification the model will assign to the root element of the tree. The actual tree root is a fake element which is used to anchor the tree. Even though it is considered to be part of the tree it does not represent a valid element in the Json file and/or the datastore. Optional
rootLabel The text to display for the root element if you decide you want to show the root. The tree attribute showRoot will actually determine if the root will be displayed. The showRoot default is true Optional

Later in this tutorial I will spend some more time on some other useful attributes.

On line 20 we create the tree and this time the model is one of the arguments for the tree, this establishes the relationship between the tree and the model. If you paid attention you will now know that the tree does not have any direct relation with the underlying store.

On line 29 we start the body with the class soria which is the theme I selected previously on line 6, next we create a div with the id=CheckboxTree and finally we call the method myTree and specify CheckboxTree as the name of the DOM element where we want to place our tree.

Ok, let fire it up, go to your browser and enter the url: http://localhost:8080 (that is if you created a virtual server as I mentioned earlier. If everything went according to plan you should see the following:

Dijit Tree step 2

Well, that was easy just a couple of lines in your index.html file and you have a fully functional tree. Ok, I know, the page title doesn’t reflect what you are seeing right now but soon it will, trust me.

Step 3 - Adding Checkboxes

It is time to start creating our own widget which is going to extend the default dijit Tree widget. To stay with our theme here we are going to create the following Javascript file ‘CheckboxTree.js’ in our tmpdir directory: ‘\Myserver\js\tmpdir\CheckboxTree.js’

Dojo recommends using your own namespace when creating new widgets and for this tutorial I picked the namespace ‘tmpdir’.

  1. dojo.provide("tmpdir.CheckBoxTree");
  2. dojo.require("dijit.Tree");

On line 1 we tell dojo what widget is included in this file and here you can already see the use of our own namespace ‘tmpdir’. On line 2 we include the original dijit tree widget because our tree and tree nodes will inherit from them and we are basically extending the classes dijit.Tree and dijit._TreeNode. The tree consists of two classes: 1) the tree and 2) its tree nodes, the tree acts as a tree node container so next we will declare both of them:

  1. dojo.declare( "tmpdir._CheckBoxTreeNode", dijit._TreeNode,
  2. {
  3. });
  4. dojo.declare( "tmpdir.CheckBoxTree", dijit.Tree,
  5. {
  6. });

Declare is a build-in function of dojo and on line 3 we declare the object _CheckboxTreeNode using the namespace tmpdir. Our tree node inherits from dijit._TreeNode. We do the same for our tree at line 8 for our tree object which inherits from dijit.Tree. Well belief it or not but you just created your own dijit widget, it’s not doing much but he, we have a widget.

To start using our newly created widget we have to go back to our index.html file and make some changes.

  1.       dojo.registerModulePath("tmpdir","/js/tmpdir");
  2.       dojo.require("dojo.data.ItemFileReadStore");
  3.       dojo.require("tmpdir.CheckBoxTree");
  4.       function myTree( domLocation ) {
  5.         var store = new dojo.data.ItemFileReadStore( {url: "/js/datastore/countries.json"});
  6.         var model = new dijit.tree.ForestStoreModel( {
  7.                   store: store,
  8.                   query: {type: 'continent'},
  9.                   rootId: 'root',
  10.                   rootLabel: 'Continents'
  11.                   });
  12.         var tree = new tmpdir.CheckBoxTree( {
  13.                   model: model,
  14.                   id: "MenuTree"
  15.                   });

The first thing we need to do is to register our namespace and tell dojo were any files associated with the namespace tmpdir will be located, this can be an absolute or relative path. We will be using a path relative to our html directory. On line 13 we now include our file CheckboxTree.js and on line 23 we are actually going to use our widget. So instead of creating a dijit.Tree object we are now creating a tmpdir.CheckBoxTree object. Ok, that’s it for the index.html file, now lets continue with our widget an add some meaningful code.

  1. dojo.provide("tmpdir.CheckBoxTree");
  2. dojo.require("dijit.Tree");
  3. dojo.declare("tmpdir._CheckBoxTreeNode", dijit._TreeNode,
  4. {
  5.     _checkbox: null,
  6.     _createCheckbox: function() {
  7.         this._checkbox = dojo.doc.createElement('input');
  8.         this._checkbox.type = 'checkbox';
  9.         this._checkbox.checked = false;
  10.         dojo.place(this._checkbox, this.expandoNode, 'after');
  11.     },
  12.     postCreate: function() {
  13.         this._createCheckbox();
  14.         this.inherited( arguments );
  15.     }
  16. });
  17. dojo.declare( "tmpdir.CheckBoxTree", dijit.Tree,
  18. {
  19.     _onClick: function(/*Event*/ e) {
  20.         var domElement = e.target;
  21.         if(domElement.nodeName != 'INPUT') {
  22.             return this.inherited( arguments );
  23.         }
  24.         var nodeWidget = dijit.getEnclosingWidget(domElement);
  25.         if(!nodeWidget || !nodeWidget.isTreeNode){
  26.             return;
  27.         }
  28.         nodeWidget._checkbox.checked ^ true;
  29.     },
  30.     _createTreeNode: function( args ) {
  31.         return new tmpdir._CheckBoxTreeNode(args);
  32.     }
  33. });

As we are going to create a checkbox on each node of the tree we need a local reference or handle for the checkbox, we declare it on line 5. Next we declare the method that will create the actual checkbox. On line 8 we tell dojo to create a DOM element ‘input’ and set the type to ‘checkbox’. Line 11 needs some explanation because we are asking dojo to place our checkbox after something called ‘expandoNode’. So the question is where did this.expandoNode come from?

Most dijit widgets inherit from dijit._Templated allowing those widget to use.... yes, templates. If we take a quick look at the html template for dijit._TreeNode located at \dojotoolkit\dijit\templates\TreeNode.html we find that a so called dojoAttachPoint is created with the name expandoNode. Every dojoAttachPoint can directly be referenced within the object like this.dojoAttachPointName which in our case is this.expandoNode so dojoAtachPoints act as placeholders in the DOM. So basically what we are doing on line 11 is to place our checkbox right after the +/- sign of a tree node.

On line 14 we declare our postCreate method which inherits from dijit._TreeNode and is called as part of a widget lifecycle. Every widget that inherits from dijit._Widget will automatically get the postCreate method which you can overwrite. On line 15 we call our own method to create the checkbox and on line 16 we call the parent postCreate method. The this.inherited(arguments) call is dojo shorthand for some nasty looking Javascript apply functionality. Ok, that’s it for our tree node and the creation of the checkbox, now lets move on to the tree itself.

Lets start at the bottom first, on line 34, the method _createTreeNode is called once from within the tree for every item in our Json data file. However, you need to understand that this only happens for tree nodes that need to be rendered. If you take a look at our first tree sample only 6 out of many nodes have been rendered. So until a node needs rendering it is not created and thus does not exist. This is important to keep in mind when we start sending events from our model to the tree.

The last part we need to do is to handle any click event associated with our checkbox. Again the _Onclick method is one of those we inherit from dijit.Tree. Because we are only interested in click events associated with our checkbox, line 24 first checks if the node name of the DOM element equals ‘input’ (see line 8 again). Next, on line 27, we try to locate the widget that actually encloses the DOM element and if it is a widget of type TreeNode we are in business, the only thing left to do is to toggle the checkbox checked attribute, line 31, and voila we have a working tree widget WITH checkboxes.

Go back to your browser and give it a try, after expanding some tree nodes your tree should look something like this:

Tree with Checkbox

If you do not see the checkboxes make sure you clear your browser cache, browsers have a tendency to cache a lot of stuff in the background.

 And oh BTW, the page title is correct now.... This concludes step 3, you may be surprised to know that most of the work we needed to do on the tree widget and its nodes is almost done. In the next step we are going to add custom events and do some lite plumbing so the tree can interact with the model. From thereon is all about the model.

Step 4 - Adding Custom Events

One of the most powerful features of the dojo framework is the event system. To make a long story short, if you know the method name within a widget you can get notified when it is called. To get notified we use the dojo function connect and what it does for you is adding your application as a listener to the event whenever the method is called.

Although the documentation on the dojo event system is sketchy at best (like most of the dojo documentation) there are some basic rules I would recommend you follow. In general protected and/or private method names start with an underscore like _onClick, you can connect to them but I would not recommend doing that outside your widget for the simple reason that those methods may change between versions of dojo. As a perfect example: the _onClick method for the tree changed between 1.3.2 and 1.4. If an event is intended for public consumption there will always be a public method associated with it. So in case of the onClick event connect to the method 'onClick' instead of '_onClick'.

Lets go ahead and add some custom events incase our checkbox is checked or unchecked. Make the following changes to our widget:

  1. dojo.declare( "tmpdir.CheckBoxTree", dijit.Tree,
  2. {
  3.   onNodeChecked: function(/*dojo.data.Item*/ storeItem, /*treeNode*/ nodeWidget) {
  4.   },
  5.   onNodeUnchecked: function(/*dojo.data.Item*/ storeItem, /* treeNode */ nodeWidget) {
  6.   },
  7.   _onClick: function(/*Event*/ e) {
  8.     var domElement = e.target;
  9.     if(domElement.nodeName != 'INPUT') {
  10.         return this.inherited( arguments );
  11.     }
  12.     var nodeWidget = dijit.getEnclosingWidget(domElement);
  13.     if(!nodeWidget || !nodeWidget.isTreeNode){
  14.         return;
  15.     }
  16.     nodeWidget._checkbox.checked ^ true;
  17.     if(nodeWidget._checkbox.checked) {
  18.         this.onNodeChecked( nodeWidget.item, nodeWidget);
  19.     } else {
  20.         this.onNodeUnchecked( nodeWidget.item, nodeWidget);
  21.     }
  22.   },
  23.   _createTreeNode: function( args ) {
  24.     return new tmpdir._CheckBoxTreeNode(args);
  25.   }
  26. });

On line 22 and 25 we declare our methods which will be called when the checkbox is either checked or unchecked. Keep in mind those are custom event callback methods so we have to make sure they get called. On line 38 we check the new state (true/false) of our checkbox and call the appropriate method. You may say: there is nothing in those methods so why call them, well that is how events are being generated with dojo, call a method and an event will be generated if anybody is listing. Also notice I did not use the underscore in the method names. For the event itself, we pass two arguments: the first being the item from our data store and the second the node widget.

Now the only thing left to do is actually catch the events, in order to do so we have to make the following changes to our index.html file.

  1.       var tree = new tmpdir.CheckBoxTree( {
  2.               model: model,
  3.               id: "MenuTree"
  4.               });
  5.       tree.placeAt( domLocation );
  6.       dojo.connect( tree,"onNodeChecked", function(storeItem, nodeWidget){
  7.         alert( store.getValue(storeItem,"name") + " got checked..");
  8.         }
  9.       );
  10.     }

On line 29, after we create our tree object, we are going to connect to our newly created event 'onNodeChecked' using dojo.connect. Dojo connect takes two arguments: the event name and a callback function to be called when the event is triggered. As you probably already know functions/methods in Javascript are objects, we can pass the entire function declaration as an argument to a method. If you don’t like this you could declare the function separately, give it a name and just pass the function name as an argument to dojo connect, whatever floats your boat.

Now go back to your browser, reload the page and click any of the checkboxes. You should see something like this:

Dojo Event alert

And that is the dojo event system at work for you. Next we are going to use this dojo event system inside our widget so the model, which we will be creating soon, will be able to tell the tree of any updates to our data store. Remember, because of the separation between the tree and the store it is the model who will tell the tree of any updates not the store itself. The model acts as the glue between the two

Take another look at our requirement c:

The tree must be able to maintain a parent-child checkbox relationship. What I mean by this is that for example if a parent checkbox is checked all child and grandchild checkboxes will automatically be checked as well or if one child is unchecked the parent and potentially its parent is unchecked automatically.

In order to make this work we need to do 2 things:

  1. Make sure the tree is listening to update events from our model
  2. Inform the model of any changes to the tree (when a checkbox is clicked).

In the remainder of this section we will deal with some parts of the first requirement just to give you an idea of how things will work going forward. Go ahead and make the following changes to our widget:

  1.   _onCheckboxChange: function(/*dojo.data.Item*/ storeItem ) {
  2.     var model    = this.model,
  3.       identity = model.getIdentity(storeItem),
  4.       node    = this._itemNodeMap[identity];
  5.     if( node ) {
  6.       if( node._checkbox != null ) {
  7.         node._checkbox.checked = this.model.getCheckboxState( storeItem );
  8.       }
  9.     }
  10.   },
  11.   postCreate: function() {
  12. //    this.connect(this.model, "onCheckboxChange", "_onCheckboxChange");
  13.     this.inherited(arguments);
  14.   },
  15.   _createTreeNode: function( args ) {
  16.     return new tmpdir._CheckBoxTreeNode(args);
  17.   }

We need to add two methods to our Tree, the _onCheckboxChange and postCreate. Remember, as I mentioned before the postCreate method overwrites the default method of our parent dijit.Tree widget. Lets start with the postCreate method, on line 58 we are telling our tree widget to listen to any ‘onCheckboxChange’ events generated by our model, and if triggered, call our _onCheckboxChange callback function on line 45. For the time being I have commented out line 58 just because the method _onCheckboxChange on our model doesn't exist yet.

On line 45 we declare our callback method which will be invoked with a single argument: the storeItem whose checkbox state changed. Remember what I said before, a tree node is NOT created until it needs to be rendered therefore on line 50 we check if the tree node exists for this particular store item. At line 51 we check if the node actually has a checkbox associated with it, if so, update its state. At this point in the game, if the node exist, it will have a checkbox more on this later.

One last thing, if you are wondering why your tree appears the way you left it last (ie: expanded or not) that’s because the tree creates a cookie which captures the current state of the tree. I will tell you later when I go over some of the other useful attributes how to disable the use of a cookie.

Step 5 - Create the Custom Model

The default dijit Tree relies on a dojo ItemFileReadStore to load our Json file however, in order to satisfy several of our requirements we need to be able to maintain the status of our checkboxes at the data store level. To do that we need a ItemFileWriteStore instead. Lets make the changes to our index.html to reflect our requirement:

  1.   <script type="text/javascript">
  2.       dojo.registerModulePath("tmpdir","/js/tmpdir");
  3.       dojo.require("dojo.data.ItemFileWriteStore");
  4.       dojo.require("tmpdir.CheckBoxTree");
  5.     function myTree( domLocation ) {
  6.       var store = new dojo.data.ItemFileWriteStore( {
  7.               url: "/js/datastore/countries.json"
  8.               });
  9.       var model = new tmpdir.tree.ForestStoreModel( {
  10.               store: store,
  11.               query: {type: 'continent'},
  12.               rootId: 'root',
  13.               rootLabel: 'Continents',
  14.               });

On line 12 we are now including the ItemFileWriteStore and on line 16 we create the write store instead of a read store. In reality nothing will happen to the current behavior of our widget because the ItemFileWriteStore just extends the ItemFileReadStore.

Next we are going to create our model and put all the basic hooks and callbacks in place. Open your CheckBoxTree.js file and make the following enhancements:

  1. dojo.provide("tmpdir.CheckBoxTree");
  2. dojo.provide("tmpdir.CheckBoxStoreModel");
  3. dojo.require("dijit.Tree");
  4. dojo.declare( "tmpdir.CheckBoxStoreModel", dijit.tree.ForestStoreModel,
  5. {
  6.   updateCheckbox: function(storeItem, newState ) {
  7.   },
  8.   onCheckboxChange: function( storeItem ) {
  9.   }
  10. });
  11. dojo.declare( "tmpdir._CheckBoxTreeNode", dijit._TreeNode,
  12. {
  13.   _checkbox: null,
  14.   _createCheckbox: function() {
  15.       this._checkbox = dojo.doc.createElement('input');
  16.       this._checkbox.type = 'checkbox';
  17.       this._checkbox.checked = currState;
  18.       dojo.place(this._checkbox, this.expandoNode, 'after');
  19.     }
  20.   },
  21.   postCreate: function() {
  22.     this._createCheckbox();
  23.     this.inherited( arguments );
  24.   }
  25. });
  26. dojo.declare( "tmpdir.CheckBoxTree", dijit.Tree,
  27. {
  28.   onNodeChecked: function(/*dojo.data.Item*/ storeItem, /*treeNode*/ nodeWidget) {
  29.   },

On line 2 we tell dojo that our CheckBoxTree.js file now also includes our new model called .CheckBoxStoreModel. On line 5 we start the declaration of our model which currently has two methods: updateCheckbox and onCheckboxChange. The last one should sound familiar to you.

The method updateCheckbox will be called by the tree whenever a checkbox is clicked. Basically this is the way the tree will let the model know things have changed on the tree side. The onCheckboxChange method is a callback which will trigger an event if something on the store side has changed. So next we add the call to updateCheckbox to our tree, where you would ask? Well as part of the _onClick method of the tree.

  1.   _onClick: function(/*Event*/ e) {
  2.     var domElement = e.target;
  3.     if(domElement.nodeName != 'INPUT') {
  4.       return this.inherited( arguments );
  5.     }
  6.     var nodeWidget = dijit.getEnclosingWidget(domElement);
  7.     if(!nodeWidget || !nodeWidget.isTreeNode){
  8.       return;
  9.     }
  10.     nodeWidget._checkbox.checked ^ true;
  11.     this.model.updateCheckbox( nodeWidget.item, nodeWidget._checkbox.checked );
  12.     if(nodeWidget._checkbox.checked) {
  13.       this.onNodeChecked( nodeWidget.item, nodeWidget);
  14.     } else {
  15.       this.onNodeUnchecked( nodeWidget.item, nodeWidget);
  16.     }
  17.   },
  18.   postCreate: function() {
  19.     this.connect(this.model, "onCheckboxChange", "_onCheckboxChange");
  20.     this.inherited(arguments);
  21.   }, 

On line 49 we call the updateCheckbox method of our model and pass the store item associated with the tree node widget and the new state of our checkbox. I have also uncommented line 58 and thus we will start listening to any onCheckboxChange events triggered by our model. As a final result our event driven CheckboxTree widget has all the plumbing in place.

To recap, any browser click events are caught by the tree _onClick method which will call the updateCheckbox method on our model and if any changes occur on our store/model the model will trigger the onCheckboxChange event which will be caught by the tree which will then update the checkbox on the tree node. Are you still with me? No? Let's try a picture then:

  

Step 6 - Adding Attributes

Lets go to the next phase of our little project and take a look at our original requirement d:

I want to be able to specify the initial state (checked/unchecked) of a checkbox but I don’t want to be forced having to do this for every checkbox. For example, if all checkboxes have a default state of ‘checked’ with the exception of only a few, I want to be able to define the default state once and only deal with the exceptions.

I’m going to implement this requirement in several smaller steps, first we need something that could tell us what the initial state of the checkbox needs to be. The most obvious place is of course our Json source file. I’m going to introduce a new attribute called the checkbox Identifier or checkboxIdent for short and add that to our file. Create a new file called \html\js\datastore\countriesChk.json

  1. { identifier: 'name',
  2.   label: 'name',
  3.   items: [
  4.      { name:'Africa', type:'continent', checkbox: true,
  5.          children:[{_reference:'Egypt'}, {_reference:'Kenya'}, {_reference:'Sudan'}] },
  6.      { name:'Egypt', type:'country', checkbox: true },
  7.      { name:'Kenya', type:'country', checkbox: true,
  8.          children:[{_reference:'Nairobi'}, {_reference:'Mombasa'}] },
  9.      { name:'Nairobi', type:'city', checkbox: true },
  10.      { name:'Mombasa', type:'city', checkbox: true },
  11.      { name:'Sudan', type:'country', checkbox: true,
  12.          children:{_reference:'Khartoum'} },

Each item in our data file gets an additional “key : value” pair, the key being ‘checkbox’ and the value could be either true or false. Not to be as flexible as a hardwood door I want to make the key configurable just because you never know. To do so, I’m going to introduce two attributes to our model, one which will define the key and the other the default state in case an item in our Json file doesn’t have the key specified. (ie: the checkbox default value).

To read the initial state of our checkboxes we also need a method that can get the new “key : value” pair from our store. Go ahead and make the following changes to our model and the CheckBoxTreeNode:

  1. dojo.declare( "tmpdir.CheckBoxStoreModel", dijit.tree.ForestStoreModel,
  2. {
  3.   checkboxIdent: "checkbox",
  4.   checkboxState: false,
  5.   updateCheckbox: function(storeItem, newState ) {
  6.   },
  7.   getCheckboxState: function(/*dojo.data.Item*/ storeItem) {
  8.     var currState = undefined;
  9.     if ( storeItem == this.root ) {
  10.       if( typeof(storeItem.checkbox) == "undefined" ) {
  11.         currState = this.root.checkbox = this.checkboxState;
  12.       } else {
  13.         currState = this.root.checkbox;
  14.       }
  15.     } else {
  16.       currState = this.store.getValue(storeItem, this.checkboxIdent);
  17.     }
  18.     return currState
  19.   },
  20.   onCheckboxChange: function( storeItem ) {
  21.   }
  22. });
  23. dojo.declare( "tmpdir._CheckBoxTreeNode", dijit._TreeNode,
  24. {
  25.   _checkbox: null,
  26.   _createCheckbox: function() {
  27.     var  currState = this.tree.model.getCheckboxState( this.item );
  28.     if( currState !== undefined ) {
  29.       this._checkbox = dojo.doc.createElement('input');
  30.       this._checkbox.type = 'checkbox';
  31.       this._checkbox.checked = currState;
  32.       dojo.place(this._checkbox, this.expandoNode, 'after');
  33.     }
  34.   },
  35.   postCreate: function() {
  36.     this._createCheckbox();
  37.     this.inherited( arguments );
  38.   }
  39. });

Starting on line 7 we introduce the new attributes checkboxIdent with the default value of “checkbox” and checkboxState with its default value of false. Both attribute can be used as configurable arguments when we create our model (more on that later).

On line 13 we declare our getCheckboxState method with only one argument being a store item. On line 16 we check if the store item passed to us isn’t our root element of our tree. Remember, as I said before the tree root does not represent any item in our data store and therefore we need to treat it differently. If it is a true store item we will fetch its initial checkbox state from the store using our new attribute checkboxIdent at line 23.

Now that we have the capability to fetch the checkbox state from our file we are going to use it when we create our checkbox on the tree nodes. On line 37 we get the checkbox state followed by a single check to see if we got something meaningful back. Undefined in this context means we were unable to locate the requested key value pair for this item in our data store. So any item in our Json file that doesn’t have “checkbox : true/false” specified will return the state undefined.

This behavior immediately satisfies our original requirement b):

It must be able to support both trees with and without checkboxes so I only need one widget going forward.

Cool, one simple check....

That BTW that was the last change we had to make to our CheckBoxTreeNode, we are done with that one. Ok, go back to your browser and reload your page, see what happens. If you didn’t jump ahead your tree should look something like this:

Step 6 Dijit Tree without checkbox

All the checkboxes are gone with the exception of our tree root. Any idea why? Well we haven’t updated our index.html file yet telling the store to use our new file coutriesChk.json.

  1.     function myTree( domLocation ) {
  2.       var store = new dojo.data.ItemFileWriteStore( {
  3.               url: "/js/datastore/countriesChk.json"
  4.               });
  5.       var model = new tmpdir.CheckBoxStoreModel( {
  6.               store: store,
  7.               query: {type: 'continent'},
  8.               rootId: 'root',
  9.               rootLabel: 'Continents'
  10.               });

In our index.html file update the URL on line 17, save it and reload your page again. If you did set the checkboxes all to true your tree should look like:

Dijit Tree with Checkbox

Ok, go play around with the new countriesChk.json file, change some of the checkbox initial states and/or remove the key value pair completely for several items. The last thing I want to take care of in this step is second part of our requirement, just having to specify the exceptions:

I want to be able to specify the initial state (checked/unchecked) of a checkbox but I don’t want to be forced having to do this for every checkbox. For example, if all checkboxes have a default state of ‘checked’ with the exception of only a few, I want to be able to define the default state once and only deal with the exceptions.

I’m going to introduce another attribute for our model called checkboxAll with a default value of true. In addition I want to be able to specify if the tree root will get a checkbox, so one more attribute: checkboxRoot and again we’ll give it a default value of true.

  1. dojo.declare( "tmpdir.CheckBoxStoreModel", dijit.tree.ForestStoreModel,
  2. {
  3.   checkboxIdent: "checkbox",
  4.   checkboxAll: true,
  5.   checkboxRoot: true,
  6.   checkboxState: false,
  7.   updateCheckbox: function(storeItem, newState ) {
  8.   },
  9.   getCheckboxState: function(/*dojo.data.Item*/ storeItem) {
  10.     var currState = undefined;
  11.     if ( storeItem == this.root ) {
  12.       if( typeof(storeItem.checkbox) == "undefined" ) {
  13.         this.root.checkbox = undefined;
  14.         if( this.checkboxRoot ) {
  15.           currState = this.root.checkbox = this.checkboxState;
  16.         }
  17.       } else {
  18.         currState = this.root.checkbox;
  19.       }
  20.     } else {
  21.       currState = this.store.getValue(storeItem, this.checkboxIdent);
  22.       if( currState == undefined && this.checkboxAll) {
  23.         currState = this.checkboxState;
  24.       }
  25.     }
  26.     return currState
  27.   },
  28.   onCheckboxChange: function( storeItem ) {
  29.   }
  30. });

On line 4 and 5 we define the new attributes checkboxAll and checkboxRoot both with their default value of true. Next on line 17 and 18 we determine if the tree root needs a checkbox. On line 25 we check if every store item requires a checkbox in case the key value pair was not specified in the Json file. If set to true we will return the default state checkboxState. Remember, the CheckBoxTreeNode will create a checkbox as long as the value returned is not undefined.

So how can we use all these newly created attributes? Good question, lets take a look at our index.html file again.

  1.     function myTree( domLocation ) {
  2.       var store = new dojo.data.ItemFileWriteStore( {
  3.               url: "/js/datastore/countries.json"
  4.               });
  5.       var model = new tmpdir.CheckBoxStoreModel( {
  6.               store: store,
  7.               query: {type: 'continent'},
  8.               rootId: 'root',
  9.               rootLabel: 'Continents',
  10.               checkboxAll: true,
  11.               checkboxRoot: false,
  12.               checkboxState: true
  13.               });

Just to demonstrate the power of using attributes I changed my URL on the store back to the original countries.json file and added all three checkbox related attributes we just created. Keep in mind, all these checkbox attributes have default values and therefore are optional. Go ahead and create some different scenarios, change the attribute values, try some different combinations, switch back and forth between your Json files and change their content.

That concludes step 6 of our project.

Step 7 - Parent - Child Checkbox Relationship

Up till this point we have been laying the foundation for our checkbox tree, created the model, put in the plumbing and added some attributes that make the checkbox tree pretty configurable but now there is only one of the original requirements left:

The tree must be able to maintain a parent-child checkbox relationship. What I mean by this is that for example if a parent checkbox is checked all child and grandchild checkboxes will automatically be checked as well or if one child is unchecked the parent and potentially its parent is unchecked automatically.

The key is to keep the parent-child relations consistent, a quick analysis of the situations reveals we need a couple of methods to be successful:

  1. We must be able to update the store. (_setCheckboxState)
  2. We must be able to update child checkboxes if a parent is checked. (_updateChildCheckbox).
  3. We must be able to update the parent checkbox if a child changes state (_updateParentCheckbox and _getParentItem).
  4. We must be able to validate the store before we begin. (validateData)
  5. Receive checkbox status change updates from the tree. (updateCheckbox)

And finally in case we do not want to maintain a strict parent-child checkbox relationship I’m going to introduce one last attribute checkboxStrict.

Because of the tight relationship between the above mentioned methods I’m going to take a slightly different approach in this step. I’m going to address each method separately one-by-one instead of doing a couple of line in each function at the time. The at the end we are going to put everything together to build our final solution. With that out of the way lets get started with the most fundamental function of them all _setCheckboxState.

 49   _setCheckboxState: function( storeItem,  newState ) {
 50     var stateChanged = true;
 51 
 52     if( storeItem != this.root ) {
 53       var currState = this.store.getValue(storeItem, this.checkboxIdent);
 54       if( currState != newState && ( currState !== undefined || this.checkboxAll ) ) {
 55         this.store.setValue(storeItem, this.checkboxIdent, newState);
 56       } else {
 57         stateChanged = false;  
 58       }
 59     } else {  
 60       if( this.root.checkbox != newState && ( this.root.checkbox !== undefined || this.checkboxRoot ) ) {
 61         this.root.checkbox = newState;
 62       } else {
 63         stateChanged = false;
 64       }
 65     }
 66     if( stateChanged ) {  
 67       this.onCheckboxChange(storeItem);
 68     }
 69     return stateChanged;  
 70   },
 71 

On line 52, the first thing we do again is check if we are dealing with the tree root. If it’s not the root we get the current checkbox state from the store and see if the state has actually changed. If the current state is not undefined (we do have a checkbox in the store for this item) we update the store using the setValue method of the store. If the current state is undefined we check the checkboxAll attribute at line 54 and if true we create the missing checkbox in the store and update the current state. The one thing you need to know about the stores setValue method is that if the attribute does not exist it will create it for you therefore there is no method like createValue or createAttribute.
Starting at line 59 we do pretty much the same for the root element with the only exception we do not use the actual store and validate the checkboxRoot attribute to see if a root checkbox is required.

Finally at line 66 we check if a state change did happen. If so, we call the by now infamous onCheckboxChange method to trigger the update event for the store, Yippy, we finally closed all loops.

Let go make our final changes to the getCheckboxState:

 27   getCheckboxState: function( storeItem) {
 28     var currState = undefined;
 29 
 30     if ( storeItem == this.root ) {
 31       if( typeof(storeItem.checkbox) == "undefined" ) {
 32         this.root.checkbox = undefined;    
 33         if( this.checkboxRoot ) {
 34           currState = this.root.checkbox = this.checkboxState;
 35         }
 36       } else {
 37         currState = this.root.checkbox;
 38       }
 39     } else {  
 40       currState = this.store.getValue(storeItem, this.checkboxIdent);
 41       if( currState == undefined && this.checkboxAll) {
 42         this._setCheckboxState( storeItem, this.checkboxState );
 43         currState = this.checkboxState;
 44       }
 45     }
 46     return currState  
 47   },

On line 41 we add the check if all store item require a checkbox and on line 42 we call the storeCheckboxState method to create the checkbox state on the store for this item.

The next method we need is to update child checkboxes. Whenever a parent checkbox is checked or unchecked all children need to be checked or unchecked as well

 72   _updateChildCheckbox: function( parentItem,  newState ) {
 73     if( this.mayHaveChildren( parentItem ) ) {
 74       this.getChildren( parentItem, dojo.hitch( this,
 75         function( children ) {
 76           dojo.forEach( children, function(child) {
 77             this._setCheckboxState(child, newState);
 78             if( this.mayHaveChildren( child )) {
 79               this._updateChildCheckbox( child, newState )
 80             }
 81           }, this );
 82         }),
 83         function(err) {
 84           console.error(_this, ": updating child checkboxes: ", err);
 85         }
 86       );
 87     }
 88   },

On line 73 we perform a lightweight check to see if the parent item potentially has children. The reason why I use potentially here is because the mayHaveChildren method only checks if the specified item has the so called 'children' attribute. However, the children attribute doesn’t always mean the item actually does have any children.
On line 74 we are going to fetch the children for the specified item from the data store. Once the child items are fetched the getChildren method will call our callback function which we wrap in the dojo hitch function because the callback normally executes in the scope of the store. Dojo hitch allows the callback to execute in any specified scope. In our case we tell dojo hitch to use this as the scope. Be aware that the function on line 75 is a callback and thus executed asynchronous. The callback function will iterate all children using the dojo.forEach function.

Because we already know the parent checkbox has changed state we can immediately update the child checkbox state on line 77. The last thing we have to do it to see if the child potentially has children of its own. If true, we just make a recursive call to _updateChildCheckbox again.

If you are not familiar with dojo you may want to look into dojo.hitch(), dojo.forEach() and dojo.some() as we will be using these functions a couple more times.

Now that we are able to update child checkboxes we also need a method to update a parent checkbox in case any of its children changes state. When a checkbox is unchecked the theory is simple, just uncheck the parent and then its parent all the way up to the root. If a checkbox gets checked on the other hand, we need to validate the state of all siblings and if all of them are checked as well we need to check the parent checkbox and so on.

The challenge here is that the store doesn’t offers a method to fetch an items parent. As you already have seen it is fairly simple to traverse down the tree using the getChildren method but up is a completely different story so let’s go ahead and write our own _getParentItem method first. This is one of the area’s that’s going to change for dojo 1.4.

123   _getParentItem: function( storeItem ) {
124     var parent = null;
125 
126     if( storeItem != this.root ) {
127       var references = storeItem[this.store._reverseRefMap];
128       for(itemId in references ) {
129         parent = this.store._itemsByIdentity[itemId];
130         break;
131       }
132       if (!parent) {
133         parent = this.root;
134       }
135     }
136     return parent 
137   },

If an item in the store is referenced by another item (its parent) the child will have a reverse reference to its parent. In other words: A child will have a parent reference if the parent specified the '_reference' attribute in our Json source file. For example: children:[{_reference:'Mexico'}, {_reference:'Canada'}, ... This BTW implies that any store item can have multiple parents but because the dojo 1.3.2. Tree implementation does NOT support multiple parents we will have to stop as soon as we get the first parent and break out of our loop started at line 130. Ok, let’s go do the _updateParentCheckbox next.

 90   _updateParentCheckbox: function( storeItem,  newState ) {
 91     var parentItem = this._getParentItem(storeItem);
 92     if( !parentItem ) {
 93       return;
 94     }
 95 
 96     if( newState ) { 
 97       this.getChildren( parentItem, dojo.hitch( this,
 98         function(siblings) {
 99           var allChecked  = true;
100           dojo.some( siblings, function(sibling) {
101             siblState = this.store.getValue(sibling,this.checkboxIdent);
102             if( siblState !== undefined && allChecked )
103               allChecked = siblState;
104             return !(allChecked);
105           }, this );
106           if( allChecked ) {
107             this._setCheckboxState( parentItem, true );
108             this._updateParentCheckbox( parentItem, true );
109           }
110         }),
111         function(err) {
112           console.error(_this, ": updating parent checkboxes: ", err);
113         }
114       );
115     } else {   
116       if( this._setCheckboxState( parentItem, false ) ) {
117         this._updateParentCheckbox( parentItem, false );
118       }
119     }
120   },

On line 91 we call our newly created _getParentItem method, if we don’t get anything back the store item apparently doesn’t have a parent. This should only be true in case of our Tree root, take another look at our _getParentItem method as to why. Once we set the parent checkbox state to true ,on line 107, we move up one level in our tree (line 108) and repeat the process, remember the parent is always somebody’s else's child. We keep doing this until we reach the Tree root and voila, there you have it. Notice that if a checkbox gets unchecked (line 115) we don't care about any of the siblings because it only takes one unchecked child to uncheck the parent. If the parent does not change state, in other words, it was already unchecked we don't need to move up the tree.

As per our analysis there are only two more things to do:

  1. We must be able to validate the store before we begin. (validateData)
  2. Receive checkbox status change updates from the tree. (updateCheckbox)

In order to guarantee a consistent data store we have to start-off with one. This means we need to validate that the checkbox data provided in our Json source file is correct to begin with. For example: does the parent checkbox state accurately reflect the state of all its children. If not, we will have to make changes to our store BEFORE we even start rendering the tree. Simple right....

Now think of the following scenario: while we modify the store, update events will be sent to our tree but there are no tree nodes yet. This is exactly why the _CheckboxChange method of our tree checks if a node exists before trying to change the checkbox state on the tree. Again, there is the checkbox state maintained by our store and there is the matching state maintained by the tree and both need to be in sync.

139   validateData: function( storeItem,  scope ) {
140     if( !scope.checkboxStrict ) {
141       return;
142     }
143     scope.getChildren( storeItem, dojo.hitch( scope,
144       function(children) {
145         var allChecked  = true;
146         var childState;
147         dojo.forEach( children, function( child ) {
148           if( this.mayHaveChildren( child )) {
149             this.validateData( child, this );
150           }
151           childState = this.getCheckboxState( child );
152           if( childState !== undefined && allChecked )
153             allChecked = childState;
154         }, this);
155         if( this._setCheckboxState( storeItem, allChecked) ) {
156           this._updateParentCheckbox( storeItem, allChecked);
157         }
158       }),
159       function(err) {
160         console.error(this, ": validating checkbox data: ", err);
161       }
162     );
163   },

On line 140 we check if a strict checkbox relationship is requested, this by the way, is our last attribute for our model. The default for checkboxStrict is true. Because our validation method is called from the store “this” equates to our tree. In order to get the proper scope the tree passes our model as the scope. On line 143 we use dojo.hitch to adjust the scope for our getChildren callback function so now we can start using “this” again. Basically, we traverse the store and update checkbox states if needed. There is however a downside to the implementation of the strict checkbox relationships and that is that all the data needs to be loaded first before we can start rendering the tree. This is not an issue with dojo version 1.3.2 but version 1.4 offers a feature called deferred loading. What that means is that the data is only loaded from the file when needed for example when you expand the tree nodes.

The last bit we need to do is to make sure our validateData method gets called, we do that by adding one line to our Tree postCreate method:

230   postCreate: function() {
231     var store = this.model.store;
232     if(!store.getFeatures()['dojo.data.api.Write']){
233       throw new Error("tmpdir.CheckboxTree: store must support dojo.data.Write");
234     }
235     this.connect(this.model, "onCheckboxChange", "_onCheckboxChange");
236     this.model.validateData( this.model.root, this.model );
237     this.inherited(arguments);
238   },

On line 236 we call the validateData method and pass “this.model.root” as the starting point in our tree and “this.model” as the scope. At this point there is only one thing left to do. We talked about the updateCheckbox method before, we even declared the method on our model but just to recap, the updateCheckbox method is called by our Tree to inform the model that the user has checked/unchecked one of our checkboxes. With all the plumbing in place updateCheckbox has to perform three steps:

  1. Change the status of the checkbox in our data store.
  2. Update any child checkboxes (in case the tree node is a parent)
  3. Update any parent checkboxes (incase all siblings are checked/unchecked)

The final updateCheckbox method looks as follows:

 19   updateCheckbox: function( storeItem,  newState ) {
 20     this._setCheckboxState( storeItem, newState );
 21     if( this.checkboxStrict ) {
 22       this._updateChildCheckbox( storeItem, newState );
 23       this._updateParentCheckbox( storeItem, newState );
 24     }
 25   },

Believe it or not but we are done, this basically concludes our CheckBoxTree tutorial. We have implemented all of the requirements we wanted to accomplish. Sit back relax and take a look at your new shiny dijit CheckBoxTree widget.

DOJO Version 1.4

Just when I started writing this article I noticed a new release of dojo was made available so I changed my recommendations to download the latest and greatest because newer is better right... Just to be on the safe side I downloaded and installed 1.4 myself and guess what, our CheckboxTree widget stopped working. Ok, things happen so let try the release notes, it turns out several enhancements had been made to the dijit Tree widget but none of them explained why our widget stopped working, we are talking enhancements here. The only thing left to do was to open up the source but unfortunately dojo does not maintain ANY sort of change history in their code.

After some investigation it became clear why our widget no longer worked, to make a long story short: The internal event handling had changed, in dojo 1.3.2 it was the Tree instance that would listen for and catch the _onClick events whereas in 1.4 this functionality has been moved to the TreeNode. Once a TreeNode catches the _onClick event the only thing it does is calling the "legacy" _onClick method of the Tree but instead of just passing the event data, the TreeNode also passes itself (“this”) as the first argument to the _onClick method of the Tree. The sequence diagram below shows the new flow.

Sequence Diagram Dojo 1.4

As a final warning, dojo 1.4 also changed the interface for the public onClick and onDblClick events and what that means is that if you use dojo.connect in your application to listen for those events your callback functions needs to change as well.

Dojo function declarations

Method Dojo 1.3.2 Dojo 1.4
_onClick _onClick: function( evt ) {...} _onClick: function( nodeWidget, evt ) {...}
onClick onClick: function( item, nodeWidget ) {...} onClick: function( item, nodeWidget, evt ) {...}
onDblClick onDblClick: function( item, nodeWidget ) {...} onDblClick: function( item, nodeWidget, evt ) {...}

Considering it is you who defines the external interface for your widget you will have to decide if you want to follow dojo's lead and implement the version 1.4 public interface for onClick and onDblClick. If you are concerned about backward compatibility of your application (dojo.connect callback functions) I would recommend to stick to the 1.3.2 implementation at least for the time being.

The next issue is related to an enhancement dojo made. Remember when we talked about our _getParentItem method in step 7, I mentioned the dijit Tree did not support multiple parent references whereas the data store did. As of dojo 1.4 this issue has been fixed and as a result the internal mapping table “itemNodeMap” changed name to “itemNodesMap” in addition, in dojo 1.3.2 each entry in the mapping table held a single reference to a TreeNode instance whereas in 1.4 each entry holds an array of references.

So now that we know what the issues are let’s go ahead a make the changes to our CheckboxTree widget in order to support dojo 1.4. Make sure you keep a copy of the 1.3.2 code because the 1.4 code is NOT backward compatible. The methods we need to changes are:

_onClick tmpdir.CheckBoxTree
_onCheckboxChange tmpdir.CheckBoxTree
_getParentsItem tmpdir.CheckBoxStoreModel
_updateParentCheckbox tmpdir.CheckBoxStoreModel
_updateChildCheckbox tmpdir.CheckBoxStoreModel

Let's start with the onClick method:

201   _onClick: function( nodeWidget,  e) {
202     var domElement = e.target;
203 
204     if(domElement.nodeName != 'INPUT') {
205       return this.inherited( arguments );
206     }
207     this._publish("execute", { item: nodeWidget.item, node: nodeWidget} );
208 
209     this.model.updateCheckbox( nodeWidget.item, nodeWidget._checkbox.checked );
210     this.onClick( nodeWidget.item, nodeWidget, e );
211     if(nodeWidget._checkbox.checked) {
212       this.onNodeChecked( nodeWidget.item, nodeWidget);
213     } else {
214       this.onNodeUnchecked( nodeWidget.item, nodeWidget);
215     }
216     this.focusNode(nodeWidget);
217   },
218 

On line 201 we adjust the _onClick function declaration to accommodate the new nodeWidget argument  and on line 210 we update the call to onClick. As you can see I have decided to make our CheckboxTree widget dojo 1.4 compliant, again it's up to you. Notice that because the TreeNode is now passing itself as the nodeWidget argument we no longer need to call the dijit.getEnclosingWidget function. Let's go to the next one, _onCheckboxChange.

219   _onCheckboxChange: function( storeItem ) {
220     var model    = this.model,
221       identity = model.getIdentity(storeItem),
222       nodes    = this._itemNodesMap[identity];
223 
224     if( nodes ) {
225       dojo.forEach( nodes, function(node) {
226         if( node._checkbox != null ) {
227           node._checkbox.checked = this.model.getCheckboxState( storeItem );
228         }
229       }, this );
230     }
231   },

On line 222 we now use "nodes" (pural) as the itemNodesMap array holds an array of nodes as to only one in dojo version 1.3.2. As a result we need to start a loop on line 225 validating each possible node. This can have some interesting visual effects for example: if your tree represents a family structure (mother, father and children) checking any of the children will have an effect on both parents.

Ok, now for our CheckboxStoreModel. In order to support the multi parent references we will no longer break out of our _getParentsItem (notice Parents vs Parent in 1.3.2) method after fetching the first parent. We actually return an array of parents.

124   _getParentsItem: function( storeItem ) {
125     var parents = [];
126 
127     if( storeItem != this.root ) {
128       var references = storeItem[this.store._reverseRefMap];
129       for(itemId in references ) {
130         parents.push(this.store._itemsByIdentity[itemId]);
131       }
132       if (!parents.length) {
133         parents.push(this.root);
134       }
135     }
136     return parents 
137   },

On line 125 we declare parents as an array and on line 130 we add every individual parent to our array which is returned on line 136. This implies that _updateParentCheckbox will get an array back instead of a single instance.

 94   _updateParentCheckbox: function( storeItem,  newState ) {
 95     var parents = this._getParentsItem(storeItem);
 96     dojo.forEach( parents, function( parentItem ) {
 97       if( newState ) { 
 98         this.getChildren( parentItem, dojo.hitch( this,
 99           function(siblings) {
100             var allChecked  = true;
101             dojo.some( siblings, function(sibling) {
102               siblState = this.getCheckboxState(sibling);
103               if( siblState !== undefined && allChecked )
104                 allChecked = siblState;
105               return !(allChecked);
106             }, this );
107             if( allChecked ) {
108               this._setCheckboxState( parentItem, true );
109               this._updateParentCheckbox( parentItem, true );
110             }
111           }),
112           function(err) {
113             console.error(this, ": updating parent checkboxes: ", err);
114           }
115         );
116       } else {   
117         if( this._setCheckboxState( parentItem, false ) ) {
118           this._updateParentCheckbox( parentItem, false );
119         }
120       }
121     }, this );
122   },

If you take a look at _updateParentCheckbox you will notice that the original function (line 97 thru 120) is basically wrapped in a dojo.forEach loop. Last but not least we need to make some minor changes to the _updateChildCheckbox method to support multiple parents.

 71   _updateChildCheckbox: function( parentItem,  newState ) {
 72     if( this.mayHaveChildren( parentItem )) {
 73       this.getChildren( parentItem, dojo.hitch( this,
 74         function( children ) {
 75           dojo.forEach( children, function(child) {
 76             if( this._setCheckboxState(child, newState) ) {
 77               var parents = this._getParentsItem(child);
 78               if( parents.length > 1 ) {
 79                 this._updateParentCheckbox( child, newState );
 80               }
 81             }
 82             if( this.mayHaveChildren( child )) {
 83               this._updateChildCheckbox( child, newState );
 84             }
 85           }, this );
 86         }),
 87         function(err) {
 88           console.error(this, ": updating child checkboxes: ", err);
 89         }
 90       );
 91     }
 92   },

On line 77 we fetch the parent(s) first and on line 78 we check if this store item has more then one. If so, we need to update all parents. And that my friends should do it for dojo version 1.4

Child Items with Multiple Parents

The last thing I want to do is to demonstrate the Checkbox Tree where child items have multiple parent. As a mentioned before this is new functionality in dojo version 1.4. The first thing we need to do is to create a new data file, create a new file in our datastore directory called family: \js\datastore\family.json

  1. { identifier: 'name',
  2.   label: 'name',
  3.   items: [
  4.      { name:'John', type:'parent', checkbox: true,
  5.          children:[{_reference:'Chuck'}, {_reference:'Melissa'}, {_reference:'Nancy'}] },
  6.      { name:'Mary', type:'parent', checkbox: true,
  7.          children:[{_reference:'Chuck'}, {_reference:'Melissa'}, {_reference:'Nancy'}] },
  8.      { name:'Chuck', type:'child', checkbox: true },
  9.      { name:'Melissa', type:'child', checkbox: true },
  10.      { name:'Nancy', type:'chidl', checkbox: true }
  11. ]}

Notice that I changed the type on all entries in our file to either "parent" or "child". I haven't talked much about the content of our Json file so this is probably not a bad time. In Step 6 when talking about adding attributes to our model, we added a so called key value pair to the Countries Json file. If you take a look at our new file above you see, for example on line 4, other "key: value" pairs such as "name:John", "type:parent" and "checkbox:true". Name, type and checkbox are attributes of the datastore items and if you like you can add additional attributes and use the store method getValue to retrieve their content similar to what we did in our own method getCheckboxState.  Because I changed the values of our attribute "type" we will also have to change the query attribute when we create our CheckBoxStoreModel so let's do that next. Open you index.html file and make the following changes:

  1.     function myTree( domLocation ) {
  2.       var store = new dojo.data.ItemFileWriteStore( {
  3.               url: "/js/datastore/Family.json"
  4.               });
  5.       var model = new tmpdir.CheckBoxStoreModel( {
  6.               store: store,
  7.               query: {type: 'parent'},
  8.               rootLabel: 'The Family',
  9.               checkboxAll: true,
  10.               checkboxRoot: false,
  11.               checkboxState: true
  12.               });

On line 17 change the URL to point to our new file "Family.Json", the next thing is to change the query attribute at line 21 the value associate with the key "type" is now going to be "parent". I also changed the root label on line 22 to stay with our new family theme. Fire-up your browser and see what we got:

Step 8 Family Tree
Step 8 Family Tree Unchecked

The picture on the left is what you should see as soon as the Checkbox Tree gets loaded. Now uncheck Mary's child Chuck and notice what happens to his father John and John's child Chuck.

Well my friends, that concludes our dijit tree with checkboxes. You can download the fully documented code here, if you have questions or want to leave a comment please visit my blog which hosts this article as well.