The new Dijit CheckBox Tree

Introduction

For those of you familiar with the Dijit CheckBox Tree project this is the third installment in the checkbox tree series. The first document discussed the initial project requirements and implementation details based on dojo version 1.3 and 1.4. The second document showed the addition of the so-called Multi-State checkboxes whereas in this episode I’m going to introduce you to the completely new, AMD compliant, version of the Dijit CheckBox Tree.

CheckBox Tree

Dojo & Dijit

Since dojo 1.4 many things have changed with regards to the dojo toolkit, especially the introduction of dojo 1.6 set the wheels of Asynchronous Model Definition in motion. AMD is a new standard detailing how to write modular JavaScript code which can be loaded using an AMD compliant loader. Any AMD compliant module is fully self-contained that is, no more dependencies on global variables, a module can reference other AMD modules by explicitly declaring a dependency using the new require() syntax. This modular approach allows you to be very specific in what needs to be loaded in order to make your application shine. As a result, AMD compliant applications tend to load faster as only the modules you really need are loaded.

In addition to the dojo toolkit enhancements, I received several feature requests, most of them centered around the topic of making it easier to programmatically change the checked state of a checkbox. As of dojo 1.6 people also starting reporting rendering issues with the CheckBox Tree and finally there was of course my private nice to have list.

This first step was to solve the rendering issues, make it dojo 1.7 compliant and add functionality to make it easier to programmatically alter a checkbox checked state. The second step was to create a fully AMD compliant version of the Dijit CheckBox Tree, add some of the Nice To Have features and extend the API’s. This document focuses on the second part.

Special thanks goes to Vladimir Elistratov who tirelessly perfromed code reviews and provided valuable feedback and suggestions. If you ever feel lost go checkout his great JavaScript geo library

New is better

Now that the new AMD implementation of the ChecBox Tree is available the interim version for dojo 1.7, which addressed the rendering issues, will no longer be available for download.

A New Project

Due to new dojo features, the number of enhancement I had in mind and the new AMD approach, instead of trying to refactor the existing code base I decided to start the AMD implementation as a completely new project called cbtree, short for ‘CheckBox Tree’. The Dijit CheckBox Tree code repository is now available at GitHub allowing you to actively follow any new Dijit CheckBox Tree developments. The download section of the project holds the latest stable version of the project.
cbtree directory

A new project comes with a new directory layout. Please note that the cbtree directory is located at the same level as dojo and dijit. This is important as the included demo applications and unit tests all use relative paths when referencing dojo and dijit modules.

If you haven’t done so already and you want to setup a local HTTP server, the instructions can be found here. However, instead of downloading dojo 1.3.2 as recommended you need to get the latest dojo toolkit instead.

When migrating

If you are planning to migrate from the original Dijit CheckBox Tree to the new AMD implementation please refer to the documentation for a detailed description of all available properties. Also see the New Properties section for additional information.

What’s New

Following is a summary of some of the new features and enhancements, the table is by no means a complete overview. Please refer to the CheckBox Tree documentation for more details.

New and enhanced Feature Description
Tree Styling API The CheckBox Tree comes with a simple TreeStyling API which is loaded as a separate tree extension. The Tree Styling API allows you to dynamically manage tree node icons, labels and row styling either for the entire tree or on a per data item basis.
Store Model API The Store Model API extends the functionality of the standard CheckBox Tree Store Models. The API allows the user to programmatically build and maintain checkbox trees. For example, you can create your store starting with an empty JSON dataset or use an existing data store and use the Store Model API to add, move, remove or change store items.
Custom Widgets Instead of the default Multi-State checkbox that comes with the CheckBox Tree you are now able to specify alternative widgets to represent a store items checked state. The CheckBox Tree packages comes with two demos, tree04.html and tree05.html demonstrating this capability. Demo tree04.html uses the dojox TriStateCheckbox whereas tree05.html uses the dijit ToggleButton widget. An image of the CheckBox Tree with the ToggleButton widget is included below.
Read Only Checkboxes Checkboxes can now be marked as read-only using the tree properties branchReadOnly and leafReadOnly
Multi Model Support The new AMD CheckBox Tree implementation now handles multiple models operating on the same store correctly. Changes made by one model are automatically reflected by any tree associated with other models operating on the same store. For example: all trees on the left in the online demo are associated with just one store, checking a checkbox on one tree will automatically update all other trees.
Updated Event Handling The internal event handling system has been rewritten and optimized to even support trees with thousands of checkboxes. In addition, updates to the checked state of a data item directly in the underlaying store using, for example: store.setValue(), will result in an automatic update of any parent-child relationship keeping the store, model and tree in a synchronized state.
The new event handling also allows you to map any store item property to a tree node property. (see the advanced section in the documentation for details).

Design Principles

In trying to understand what benefits AMD brings to the table and how to leverage it I decided to redesign and, as a result, rewrite the entire Dijit CheckBox Tree. As I mentioned before, AMD is, among other things, about loading only what you need without having implicit external dependencies. Therefore the practice of declaring multiple modules in a single source file is strongly discouraged and as a side effect, it truly creates cleaner code. What about the additional overhead of loading all these smaller modules you ask? Well, I haven’t seen any HTTP 1.0 server in the last couple of years so I guess you won’t notice the difference. Keep in mind you load a lot less code to begin with then you would have with previous dojo implementations, meaning a lot less network I/O. In addition, loading modules cross domain using a CDN, for example, has become a piece of cake.

Code Separation

Originally all modules, that is, CheckBox, CheckBoxStoreModel, _CheckBoxTreeNode and CheckBoxTree were all included in a single source file. With the new implementation however, they have been split into separate files and turned into anonymous classes. In addition, the CheckBox Tree now comes with it own dedicated store models which no longer inherit from the original dijit tree models.

Simple Accessors

Starting with dojo 1.6 simple accessors like get() and set() have been made available for all dijit widgets. These accessors are now also available for the NEW CheckBox Tree, Tree Styling API and Store Model API. For example, setting or getting the checked state of a tree node is as simple as:

nodeWiget.set(“checked”,true)
nodeWiget.get(“checked”)

New Properties

Even though the project is still referred to as The Dijit CheckBox Tree, the reality is that with the new implementation any widget capable of presenting a so-called checked state can now be used. For example, when creating a CheckBox Tree you can, by means of the tree widget property, specify a toggle button, or any other widget for that matter, to be used instead of the default checkbox.

Toggle button Tree

The example on the left demonstrates the use of the ToggleButton widget. Because of these new capabilities the CheckBox Tree store models no longer refer to a CheckBox state, instead they refer to the more generic Checked state.

Therefore all properties formerly prefixed with checkBox are now simply prefixed with checked like: checkedAll instead of checkboxAll. In addition, new properties have been added while others have been removed or have moved from the tree to the model to better decouple the tree from the model.

If you are planning to migrate from the original Dijit CheckBox Tree to the new implementation please refer to the documentation for a detailed description of all available properties including many code samples how to use them.

Tree Styling

The AMD CheckBox Tree comes with a separate Tree Styling API which allows the user to apply styling to either the tree as a whole or set styling properties on a per data item basis using simple accessors like get() and set(). Styling can be applied to the tree node row, icon or label.

Custom Icons

In contrast to the standard dijit tree, which applies tree node styling using callback functions, the Tree Styling API offers the ability to programmatically set styling properties. For example, to change the label styling for a given data item simply call:

tree.set(“labelStyle”,{color:red}, item)

To set an icon, or a set of icons, for a for a given data item call:

tree.set(“icon”, {iconClass:”myIcon”}, item)

or

tree.set(“iconClass”,”myIcon”, item)

Alternatively, you can designate a data item property as the default icon for the given item by setting the new iconAttr property of the store model. If set, both the model and the tree will recognise the item property as an icon class. Please refer to the Tree Styling documentation for details.

Note: With the Tree Styling API loaded you can still use the original dijit tree callback style styling.

Online Demo

The CheckBox Tree package includes a set of demo applications as well as some unit tests which you can you use to verify proper installation. A comprehensive AMD CheckBox Tree demo is also available online.

Custom Icons

The online demo illustrates the CheckBox Tree configurable properties, the tree
styling API, drag and drop feature and the extended store model API.

Also, the left accordion panel demostrate the multi model support, that is, click a checkbox on one tree and all other trees with checkboxes are automatically updated.

Download

The Dijit CheckBox Tree source code, documentation and demos can be downloaded straight from the GitHub repository or as zip file from the associated download section. Note, the download section holds the latest stable version of the code. When installing the CheckBox Tree package make sure you install it according the above shown directory structure.

Documentation

All included documentation is written using the Markdown format and is also available online here.

Code Sample

The following is a simple application which creates a CheckBox Tree and connect to the onCheckBoxClick event. Whenever a checkbox on the tree is clicked the function checkBoxClicked() is called displaying the updated checked state for the associated tree node.

  1 <html lang="en">
  2   <head>
  3     <title>Dijit Tree with Checkboxes</title>
  4     <style type="text/css">
  5       @import "../../dijit/themes/claro/claro.css";
  6       @import "../themes/claro/claro.css";
  7     </style>
  8     <script type="text/javascript">
  9       var dojoConfig = {
 10             async: true,
 11             parseOnLoad: true,
 12             isDebug: true,
 13             baseUrl: "../../",
 14             packages: [
 15               { name: "dojo",  location: "dojo" },
 16               { name: "dijit", location: "dijit" },
 17               { name: "cbtree",location: "cbtree" }
 18             ]
 19       };
 20     </script>
 21     <script type="text/javascript" src="../../dojo/dojo.js"></script>
 22   </head>
 23   <body class="claro">
 24     <h1 class="DemoTitle">Dijit Tree with Multi State CheckBoxes</h1>
 25     <div id="CheckboxTree">
 26       <script type="text/javascript">
 27         require([
 28           "dojo/_base/connect",
 29           "dojo/data/ItemFileWriteStore",
 30           "dojo/domReady",
 31           "cbtree/Tree",                    
 32           "cbtree/models/ForestStoreModel"  
 33           ], function( connect, ItemFileWriteStore, domReady, Tree, ForestStoreModel) {
 34             function checkBoxClicked( item, nodeWidget, evt ) {
 35               alert( "The new state for " + this.getLabel(item) + " is: " + nodeWidget.get("checked") );
 36             }
 37             var store = new ItemFileWriteStore( { url: "../datastore/Family-1.7.json" });
 38             var model = new ForestStoreModel( {
 39                                 store: store,
 40                                 query: {type: 'parent'},
 41                                 rootLabel: 'The Family'
 42                              });
 43             var tree = new Tree( {
 44                               model: model,
 45                               id: "MenuTree",
 46                               branchIcons: true,
 47                               branchReadOnly: false,
 48                               checkBoxes: true,
 49                               nodeIcons: true
 50                             });
 51             connect.connect( tree, "onCheckBoxClick", model, checkBoxClicked );
 52             domReady( function() {
 53               tree.placeAt('CheckboxTree');
 54             });
 55         });
 56       </script>
 57     </div>
 58     <h2>Click a checkbox</h2>
 59   </body>
 60 </html>

The CheckBox Tree package and documentation include many more examples.

How Does It Work

The easiest way to exaplain how the Dijit CheckBox Tree works is by using some simplified sequence diagrams and a high level overview of some of the internal functions. If you want all the details then the source code is going to be your best friend. In this How To section I’m going to use two diagrams to explain how the above sample code works and what happens under the hood.

Creating the CheckBox Tree

On lines 37-50 we instantiate the store, model and tree. When the model is created it connects, or better yet subscribes, to the onSet event of the store. Whenever a property of a data item changes, the store will generate an onSet event. The onSet event holds information about the data item, the property that changed and what the new property value is.
When the tree is created on line 43, it internally connects to the onChange event of the model. As part of the post create stage of the tree it starts loading the data store by calling validateData() and for each data item a tree node and checkbox is created.

The models validateData() is an essential part of the CheckBox Tree, it loads the store data items and synchronizes their checked state when the parent-child checkbox relationship is requested.

The diagram below shows the sequence of events involved in creating a CheckBox Tree.

Custom Icons

If you take a closer look at the sequence diagram you will notice that the tree never interacts with the store directly, this is part of the Model-View-Controller design pattern.

Getting notified

In order for our sample application to get notified whenever the user clicks a tree checkbox it needs to connect to the onCheckBoxClick event of the tree. On line 51 the application connects to the event specifying checkBoxClicked as the callback function.

connect.connect( tree, "onCheckBoxClick", model, checkBoxClicked );

The function checkBoxClicked() on line 34 will be called in the context of the model, that is, the “this” object equates to model.
So what happens when a user clicks a checkbox? The following diagram show the sequence of events that take place each time a user clicks a tree checkbox. The first thing the tree node will do is to verify the click event took place on a DOM element of type “input”, if so, it gets the new checked state from the checkbox.

Custom Icons

Next, the tree node will tell the store model to update the checked state for the data item associated with the node by calling setChecked(). Simply speaking, the model will take the request and in turn asks the underlying store to do the same. On completion the store will generate an onSet event.

Because the model subscribed to the onSet event of the store, as shown in the first diagram, it gets notified about the store update and this is were the interesting stuff starts to happen. Any data item in the store may have any number of parent items so, assuming the parent-child relationship for the model is enabled, the model will take the onSet event and updates the checked state of all parent items according to the new state of the checkbox that just got updated.
Although not visible on the sequence diagram, any update to a parent item results in another call to the setChecked() function untill all have been updated. So a single click on a tree checkbox can result in several store updates. In addition, if the tree node whose checkbox was clicked has any children those children will also be updated.

Being the system architect that you are, you are wondering: why not update the parent items and potential children as soon as the original setChecked() call comes in? It would make things alot easier as you don’t have to handle the onSet event from the store.
The answer is: there can be multiple models operating on the same store. For example, lets assume you have two trees A and B, and each tree has its own model A and B. If a checkbox gets clicked on tree A, the tree will call setChecked() of model A therefore model B has no notion of any click event. However, because both models are subscribed to the onSet event of the common store, both models will be notified of the store update thus allowing both model A and B to update their own tree.

Finally, when the model receives the onSet event it will generate a matching onChange event which is caught by all trees associated with the model. Note, you can associated multiple trees with a single model. The tree will then search for all tree nodes associated with the data item and send a set(“checked”, newValue) request to each of them.

Dijit Tree with Multi State Checkboxes 1.7

Note: This article has been superceded by the The new Dijit CheckBox Tree. The interim 1.7 release referred to in this post is therefore no longer available for download.

More than 2 years after I posted the first version of the dijit tree with multi state checkboxes for dojo 1.4,  and after 20,000+ downloads it was time to get an updated version out.

Actually, it wasn’t until dojo 1.6/1.7 was released that I started to get feedback that the old version wasn’t working properly any more.

Well friends, the 1.7 tree is here with full support for the claro theme and I believe some nice additions. See the picture below.

Tree - claro

Issues reported

The issues reported using the Dijit Multi State Checkbox Tree version 1.4 with dojo 1.6/1.7 can be summarized as follows:

  1. Updates to checkboxes could not be seen until one would hover over the checkbox.
  2. The properties ‘branchIcons’ and ‘nodeIcons’ no longer worked.
  3. No support for the dojo 1.5 claro theme.
  4. Firebug reported the use of deprecated functions.

I’m happy to report, all of the issues have been resolved.

In addition, some people asked if there was any easy way to change the checkbox state programmatically. But then again, others asked if I could write their entire application using my dijit tree because they liked it so much.

I will follow this blog up with a more detailed explanation of the changes required to get everything working again for dojo 1.7. Most of the time went actually into setting up my environment again, you known, getting the latest and greatest version of every tool and then getting everything to work again. All-in-all it took about a day to get every updated and back to normal again. But here is just an excerpt of some of the reasons causing the problems:

As it appears the dojo team had decided to adopt the new AMD technology which stands for Asynchronous Module Definition. Starting with dojo 1,6 an entire refactoring was kicked off which, as far as I understand, should lead to the new and improved dojo 2.0 platform. The implementation of AMD introduced new language structures and as a result of the refactoring new feature were added and of course others got dropped or some behavior slightly changed.

If you are interested in reading more about AMD, and who isn’t right? checkout the following sites:

http://dojotoolkit.org/blog/learn-more-about-amd

https://github.com/amdjs/amdjs-api/wiki/AMD

A good exercise to see how well your code holds up in relation to dojo 1.6/1.7 is to download the latest version of firebug (still the best javascript tool available, don’t leave home without it) and run dojo 1.7 in debug mode. Most likely you will get some warnings regarding the use of deprecated methods/functions.

What’s new in the 1.7 Checkbox Tree

As mentioned before, some people asked if there was an easy way to change the checkboxes programmatically. Well, if you are into dojo everything is simple, just do a fetch() on the model store and call the updateCheckbox() method for each store item with its new state.

Ok, to make is even simpler, version 1.7 has two new methods called check() and uncheck() which allow you to do just that. Both methods take three arguments:

  1. Query
  2. Callback
  3. Scope

The query argument can be either a string or query object. The optional callback argument is a function which is called on completion with the number of store items that matched the query and the number of store items that matched the query and actually changed state. Finally, the optional scope argument specifies the context in which the callback function is to be executed. The release notes have several sample of the new methods. As an example:

myTree.model.check( { name: "John" }, notifyMe, this );

The store will be queried for every item whos name property equals “John” and if the associated checkbox is unchecked it will get checked. How easy is that…

In addition, a new property was added to the tree model which allows to make checkboxes associated with branches read-only.

Download the latest version

The latest AMD version of the Dijit Tree with Multi State Checkboxes can be downloaded here.

And for those who are wondering: “will the 1.7 tree work with dojo 1.6” the answer is: NO, a quick test reveiled that the new language structures required for AMD support are not available in 1.6.

Dijit Tree with Multi State 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

This article is a follow-up on my previous blog regarding a dijit tree with checkboxes. In the past couple of weeks I received a number of requests if it would be possible to get the checkbox tree with so called multi or triple state checkboxes. By default a checkbox has two states:

  1. True (checked)
  2. False (unchecked)

This BTW: applies to the default HTML checkbox as well as the dijit checkbox so in order to make a multi/triple state checkbox work we have to create our own checkbox that holds an additional state. Second, we have to be able to visually represent the third state. Going forward I will refer to this third state as the ‘mixed’ state.

MultiStateDemo

Click here to take a quick look at a demo. Please let me know what you think, leave a comment or ask a question. The source package for the Checkbox Tree with Multi State Checkboxes can be downloaded here.

The Basics

The default dijit checkbox uses the standard HTML checkbox controls but overlays the visual image with its own sprite and apply some CSS styling. We are going to use the same concept by extending the dijit checkbox widget, create a new sprite and associated CSS classes. To make it real fancy I’m going to create separate sprites for each dijit theme. Having said that I still want to be able to support the default HTML checkbox, the visual aspect of a mixed state will not be supported BUT at the application level you will be able to check if the HTML checkbox is in a mixed state.

For this tutorial I’m going to use the dojo version 1.4 source of our CheckBoxTree which can be downloaded here, just in case you are wondering: will there be a dojo 1.3.2 version available of the multi state checkbox tree, the answer is yes.

The Multi State Checkbox

I’m going to include the extended dijit checkbox widget in our CheckBoxTree source file as I currently don’t have any plans for using the multi state checkbox anywhere else. However, it is very easy to extract it if you want to. And now for some code:

  1 dojo.provide("tmpdir.CheckBox");
  2 dojo.provide("tmpdir.CheckBoxTree");
  3 dojo.provide("tmpdir.CheckBoxStoreModel");
  4 
  5 dojo.require("dijit.Tree");
  6 dojo.require("dijit.form.CheckBox");
  7 dojo.require("dijit.tree.ForestStoreModel");
  8 
  9 dojo.declare( "tmpdir.CheckBox", dijit.form.CheckBox,
 10 {
 11   baseClass: "tmpdirCheckBox",
 12 
 13   value: "unchecked",
 14 
 15   _setStateClass: function(){
 16     var newStateClasses = this.baseClass.split(" ");
 17 
 18     function multiply(modifier){
 19       newStateClasses = newStateClasses.concat(dojo.map(newStateClasses,
 20         function(c){ return c+modifier; }),
 21         "dijit"+modifier);
 22     }
 23 
 24     if(this.state){  multiply(this.state); }
 25     if(this.attr('value') == "mixed" ){
 26       multiply("Mixed");
 27     } else if(this.checked){
 28       multiply("Checked");
 29     }
 30     if(this.disabled){
 31       multiply("Disabled");
 32     }else if(this._active){
 33       multiply(this.stateModifier+"Active");
 34     }else{
 35       if(this._focused){
 36         multiply("Focused");
 37       }
 38       if(this._hovering){
 39         multiply(this.stateModifier+"Hover");
 40       }
 41     }

On line 1 we tell dojo that our CheckBoxTree file also provides our new checkbox using the same namespace (tmpdir) as we did before. We include the dijit checkbox on line 6 and declare the new checkbox on line 9 which inherits from dijit.from.Checkbox.

On line 11 we define the base class for our checkbox widget. This will distinguish our checkbox from the default dijit checkbox allowing us to create our own CSS classes and thus introduce the third (mixed) state. On line 13 we set the value attribute to “unchecked”. We are going to use the checkbox value attribute to hold the mixed state which can be either “checked”, “unchecked” or “mixed”. The value attribute maps the standard value property of  the default HTML checkbox and therefore at the application layer you can use this value to determine if a checkbox is in a mixed state regardless if you use our dijit style checkbox or the default HTML checkbox. (more on this later).

On line 15 we overwrite the _setStateClass method which will generate the appropriate CSS classes depending on the current state of the checkbox. For example, if the checkbox is checked it will generate a CSS class “tmpdirCheckBoxMixed” if on the other hand the checkbox is in a mixed state it will generate a CSS class “tmpdirCheckBoxMixed” or we could get a state like “tmpdirCheckBoxMixedHover”.

Next we are going to overwrite the default  set and get ValueAttr functions in order to be able to use the checkbox value attribute to hold our mixed state.

 63   _setValueAttr: function( newValue){
 64 
 65     if(typeof newValue == "string"){
 66       this.value = newValue;
 67       dojo.attr(this.focusNode, 'value', newValue);
 68     }
 69   },
 70 
 71   _getValueAttr: function(){
 72     return this.value;
 73   }

The default _setValueAttr method also manipulates the ‘checked’ attribute of the checkbox and that is something we don’t want. Believe it or not but that’s all we have to do for our extended checkbox, next we will work on our CSS class definitions and create new sprites for our checkbox.

CSS Classes and Sprites

First we have to create a couple of new directories to support our creative work. In your tmpdir directory create a subdirectory called ‘themes’ and the themes directory needs another subdirectory for each dijit theme. Finally each dedicated themes directory will have a subdirectory called ‘images’. Your directory structure should look like this:

  Directory Structure 2

This directory structure follows the dojo/dijit directory structure, if you want to change it feel free to do so but for this tutorial we are going to use the above structure. In the \themes\nihilo\ directory create a new file called tmpdir.css and add the following lines:

  1 /*
  2  *  Adds cosmetic styling to tmpdir. Users may swap with a custom theme CSS file.
  3 */
  4 @import url("Checkbox.css");

In the same \themes\nihilo\ directory create a new file call Checkbox.css and add the follow class definitions:

  1 /*
  2  *  tmpdir CheckBox Widgets CSS
  3  *
  4  *  Order of images in the sprite (from L to R):
  5  *    checkbox  normal    - checked
  6  *                        - unchecked
  7  *                        - mixed
  8  *              disabled  - checked
  9  *                        - unchecked
 10  *                        - mixed
 11  *              hover     - checked
 12  *                        - unchecked
 13  *                        - mixed
 14 */
 15 .nihilo .tmpdirCheckBox,
 16 .nihilo .tmpdirCheckBoxIcon {
 17   background-image: url('images/spriteCheckbox.gif'); /* sprite image */
 18   background-repeat: no-repeat;
 19   width: 16px;
 20   height: 16px;
 21   margin: 0;
 22   padding: 0;
 23 }
 24 
 25 .nihilo .tmpdirCheckBox,
 26 .nihilo .tmpdirCheckBoxIcon {
 27   /* unchecked */
 28   background-position: -16px;
 29 }
 30 
 31 .nihilo .tmpdirCheckBoxChecked,
 32 .nihilo .tmpdirCheckBoxIcon {
 33   /* checked */
 34   background-position: 0px;
 35 }
 36 
 37 .nihilo .tmpdirCheckBoxDisabled {
 38   /* disabled */
 39   background-position: -48px;
 40 }
 41 
 42 .nihilo .tmpdirCheckBoxCheckedDisabled {
 43   /* disabled but checked */
 44   background-position: -48px;
 45 }
 46 
 47 .nihilo .tmpdirCheckBoxMixed {
 48   /* checked & mixed */
 49   background-position: -32px;
 50 }
 51 
 52 .nihilo .tmpdirCheckBoxMixedDisabled {
 53   /* mixed & Disabled*/
 54   background-position: -64px;
 55 }
 56 
 57 .nihilo .tmpdirCheckBoxHover {
 58   /* hovering over an unchecked enabled checkbox */
 59   background-position: -112px;
 60 }
 61 
 62 .nihilo .tmpdirCheckBoxCheckedHover {
 63   /* hovering over a checked enabled checkbox */
 64   background-position: -96px;
 65 }
 66 
 67 .nihilo .tmpdirCheckBoxMixedHover {
 68   /* checked & mixed */
 69   background-position: -128px;
 70 }

The only thing missing from a creative standpoint is the sprite itself which we declare as our background image on line 17. A sprite is basically a single image composed of multiple smaller images, loading a single sprite instead of multiple smaller images improves performance and creates less overhead and eliminates screen flickering. There are many, many image editors on the market you could use to create the sprite but I prefer to use IconWorkshop from Axialis because the professional version has special support for sprites or Image strips as they call them. Lets create the spriteCheckbox.gif file in the \themes\nihilo\images directory.

  spriteCheckbox

As you can tell from the above image the sprite is composed of 9 smaller images, each 16 pixels high and wide representing the different states of our checkbox. The three images on the right are the hover states which currently are the same as the enabled states on the left. Feel free to adopt the image to your own needs, both the PNG and GIF version are included in the source package.

You will have to repeat the above steps for the other dijit themes Soria and Tundra but I guess you get the picture and don’t forget to replace ‘.nihilo’ in the CSS file with the appropriate theme you are creating. (i.e: ‘.tundra’ and ‘.soria’)

Update The Checkbox Tree

To incorporate the new checkbox widget in our tree we will have to make enhancements to all three components of our tree, these components are:

  1. The Model
  2. The Tree Nodes
  3. The Tree

Update The Model

The first thing we will have to do is to introduce a new attribute for each store item which will hold the additional mixed state of our store item. As I said in the previous tutorial the data store and model have no direct relationship to any of the TreeNodes or the associated checkboxes (remember the MVC pattern). Also, as of dojo 1.4 there may actually be multiple checkboxes referring to, or should I say representing, a single store item.

  1   checkboxAll: true,
  2   checkboxState: false,
  3   checkboxRoot: false,
  4   checkboxStrict: true,
  5   checkboxIdent: "checkbox",
  6   checkboxMState: "mixed",

On line 6 we introduce a new attribute to our model called ‘checkboxMState’ which holds the name of the attribute we will be using to store the mixed state for each store item. Typically there is not need to overwrite the ‘checkboxMState’ attribute unless you are planning on using the attribute name “mixed” in your Json file. If so, you will have to overwrite the checkboxMState value when creating the model.

First we need to enhance the getCheckboxState method to support the additional mixed state.

 99   getCheckboxState: function( storeItem) {
100     var state = { checked: undefined, mixed: false };  
101 
102     if ( storeItem == this.root ) {
103       if( typeof(storeItem.checkbox) == "undefined" ) {
104         this.root.checkbox = undefined;    
105         this.root.mixedState = false;
106         if( this.checkboxRoot ) {
107           this._setCheckboxState( storeItem, this.checkboxState, false );
108           state.checked = this.checkboxState;
109         }
110       } else {
111         state.checked = this.root.checkbox;
112         state.mixed    = this.root.mixedState;
113       }
114     } else {  
115       state.checked = this.store.getValue(storeItem, this.checkboxIdent);
116       state.mixed    = this.store.getValue(storeItem, this.checkboxMState);
117       if( state.checked == undefined && this.checkboxAll) {
118         this._setCheckboxState( storeItem, this.checkboxState, false );
119         state.checked = this.checkboxState;
120       }
121     }
122     return state  
123   },

Instead of passing two states around we create a object called ‘state’ with two properties: checked and mixed. On line 111 and 112 we copy the store item states to our new object and return it at line 122. Now that we can retrieve the additional state we must also be able to store it.

125   _setCheckboxState: function( storeItem,  newState,  mixedState ) {
126     var stateChanged = true;
127 
128     if( storeItem != this.root ) {
129       var currState = this.store.getValue(storeItem, this.checkboxIdent);
130       var currMixed = this.store.getValue(storeItem, this.checkboxMState);
131       if( (currState != newState || currMixed != mixedState) && 
              (currState !== undefined || this.checkboxAll) ) {
132         this.store.setValue(storeItem, this.checkboxIdent, newState);
133         this.store.setValue(storeItem, this.checkboxMState, mixedState);
134       } else {
135         stateChanged = false;
136       }
137     } else {  
138       if(( this.root.checkbox != newState || this.root.mixedState != mixedState) &&
139          ( this.root.checkbox !== undefined || this.checkboxRoot ) ) {
140         this.root.checkbox   = newState;
141         this.root.mixedState = mixedState;
142       } else {
143         stateChanged = false;
144       }
145     }
146     if( stateChanged ) {  
147       this.onCheckboxChange(storeItem, mixedState);
148     }
149     return stateChanged;
150   },

The most important thing with the updated _setCheckboxState method is that we will now have to validate BOTH states in order to determine if an update to the actual checkbox(es) is required. For example: if a child checkbox is checked/unchecked the parent’s checked state may not change but it could alter its mixed state and thus we will have to let the tree know it needs to update the parent checkbox(es).

The next method of our model we need to address is the _updateParentCheckbox. Here is were we determine if a checkbox gets a mixed state, only parent checkboxes can get a mixed state. If any child has a mixed state the parent will automatically get the mixed state too regardless of the states of the other children.

171   _updateParentCheckbox: function( storeItem ) {
172     var parents = this._getParentsItem( storeItem );
173     dojo.forEach( parents, function( parentItem ) {
174       this.getChildren( parentItem, dojo.hitch( this,
175         function(children) {
176           var hasChecked     = false,
177             hasUnChecked   = false,
178             isMixed      = false;
179           dojo.some( children, function(child) {
180             state = this.getCheckboxState(child);
181             isMixed |= state.mixed;
182             switch( state.checked ) {  
183               case true:
184                 hasChecked = true;
185                 break;
186               case false:
187                 hasUnChecked = true;
188                 break;
189             }
190             return isMixed;
191           }, this );
192           isMixed |= !(hasChecked ^ hasUnChecked);
193           if( this._setCheckboxState( parentItem, isMixed ? true: hasChecked, 
                                                      isMixed ? true: false ) ) {
194             this._updateParentCheckbox( parentItem );
195           }
196         }),
197         function(err) {
198           console.error(this, ": fetching mixed state: ", err);
199         });
200     }, this );
201   },

If a store item gets a mixed state, by default it will also enter the checked state (line 193). We could have gone both ways but if you opt to use the default HTML checkboxes (which do not support the visual aspect of a mixed state) at least you can SEE some child is checked if the parent branch is collapsed. I guess this is like the glass is half full/empty discussion. On lines 182-189 we explicitly check for ‘true’ or ‘false’ as the checked state could also be ‘undefined’ in which case we need to ignore it. Remember, the undefined state occurs when no ‘checkbox’ attribute we specified for the store item in the Json file and the model ‘checkboxAll’ attribute is set to false.

The last changes I made to the model is how the initial store data validation takes place. During some performance testing I found that loading the entire Json file at once, instead of partial or lazy loading, offered by far the best performance. On average loading the file this way is about 5-6 times faster than partial loading. Also, keep in mind that if a strict parent-child checkbox relation is required (which is the default) we will have to load the file first anyway to perform our initial store data validation. As a result I broke the original validateData method into two pieces.

218   validateData: function( storeItem,  scope ) {
219 
220     if( scope.checkboxStrict ) {
221       try {
222         scope.store._forceLoad();    
223       } catch(e) {
224         console.log(e);
225       }
226       dojo.hitch( scope, scope._validateStore ) ( storeItem );
227     }
228   },

As before we first check if a strict parent-child checkbox relationship is required. If so, we’ll try a forced load followed by a call to the second part of our validation, now called _validateStore.

230   _validateStore: function( storeItem ) {
231     this.getChildren( storeItem, dojo.hitch( this,
232       function(children) {
233         var hasGrandChild = false,
234           oneChild    = null;
235         dojo.forEach( children, function( child ) {
236           if( this.mayHaveChildren( child )) {
237             this._validateStore( child );
238             hasGrandChild = true;
239           } else {
240             oneChild = child;
241           }
242         },this );
243         if( !hasGrandChild && oneChild ) {    
244           this._updateParentCheckbox( oneChild );
245         }
246       }),
247       function(err) {
248         console.error(this, ": validating checkbox data: ", err);
249       }
250     );
251   },

We walk down to the lowest branch of the tree and request a parent update for at least one child on the branch. The updateParentCheckbox method will go back up the tree all the way to the root.

Update The Tree Nodes

In the original version of our Checkbox Tree the Tree Node simply created a HTML checkbox and inserted it into the DOM. In this episode we are going to offer the option of using our enhanced dijit checkbox widget or use the default HTML checkbox. In addition, I wanted to offer the option to hide the Open/Closed folder icon and/or the leaf icon preceding the node label.

  OpenCloseIcon

In order to accomplish these additional requirements we are going to introduce four more attributes to our Tree:

Attribute Default
checkboxStyle “dijit”
branchIcons true
nodeIcons true
checkboxMultiState true

Lets go ahead and update our _createCheckbox method of the CheckBoxTreeNode widget.

258 dojo.declare( "tmpdir._CheckBoxTreeNode", dijit._TreeNode,
259 {
260   _checkbox: null,
261 
262   _createCheckbox: function() {
263     var  state = this.tree.model.getCheckboxState( this.item );
264     if( state.checked !== undefined ) {
265       if (this.tree.checkboxStyle == "dijit" ) {
266         this._checkbox = new tmpdir.CheckBox().placeAt(this.expandoNode,'after');
267       } else {
268         this._checkbox = dojo.doc.createElement('input');
269         this._checkbox.className = 'tmpdirHTMLCheckBox';
270         this._checkbox.type      = 'checkbox';
271         dojo.place(this._checkbox, this.expandoNode, 'after');
272       }
273       this._setCheckedState( state );
274     }
275     if( this.isExpandable ) {
276       if ( !this.tree.branchIcons ) {
277         dojo.removeClass(this.iconNode,"dijitTreeIcon");
278       }
279     } else {
280       if( !this.tree.nodeIcons ) {
281         dojo.removeClass(this.iconNode,"dijitTreeIcon");
282       }
283     }
284   },

On line 263, as before, we fetch the checkbox state from the store however, this time we get an object back instead of a simple state. If the checked state is undefined no checkbox is required for this tree node. On line 265 we check the new ‘checkboxStyle’ attribute to determine what type of checkbox is needed. On line 275 we first check if this particular tree node is expandable (is it a parent checkbox?), than if the Open/Closed Folder or leaf icon aren’t required we simply remove the appropriate CSS class from the icon Node.

Previously, when a store item changed state the model would inform the Tree and the Tree would then simply update the checkbox checked state accordingly. This time around we need a little more sophisticated approach because we are now dealing with two states and two checkbox styles each requiring different ways of storing the state information. We add a new method to the CheckBoxTreeNode called _setCheckedState which will handle the updates for the actual checkboxes. This method will be called by the Tree as soon as it receives an update notification from the model.

286   _setCheckedState: function(  state ) {
287 
288     if( this.tree.checkboxStyle == "dijit" ) {
289       if( this.tree.checkboxMultiState ) {
290         this._checkbox.attr('value',state.mixed ? "mixed" : state.checked ? "checked" : "unchecked" );
291       } else {
292         this._checkbox.attr('value',state.checked ? "checked" : "unchecked" );
293       }
294       this._checkbox.attr('checked',state.checked );
295     } else {  
296       if( this.tree.checkboxMultiState ) {
297         this._checkbox.value = state.mixed ? "mixed" : state.checked ? "checked" : "unchecked";
298       } else {
299         this._checkbox.value = state.checked ? "checked" : "unchecked";
300       }
301       this._checkbox.checked = state.checked;
302     }
303   },

On line 288 we first determine the checkbox style, next we check if multi (mixed) state support is required. On line 297 you can actually see that even if we use the default HTML checkbox we can still support mixed states. In other words, under the hood we support multi state checkboxes regardless of the checkbox style.

Update The Tree

Last but not least we have to make some minor changes to the Tree as well. I already talked about the additional attributes:

317 dojo.declare( "tmpdir.CheckBoxTree", dijit.Tree,
318 {
319   checkboxStyle: "dijit",
320 
321   checkboxMultiState: true,
322 
323   branchIcons: true,
324 
325   nodeIcons: true,

I also talked about the Tree no longer simply updating the checkbox checked state, going forward the _onCheckboxChange method of the Tree will call the _setCheckedState method of the CheckBoxTreeNode to have both states (checked and mixed) updated.

352   _onCheckboxChange: function( storeItem ) {
353 
354     var model  = this.model,
355       state    = model.getCheckboxState( storeItem ),
356       identity = model.getIdentity(storeItem),
357       nodes    = this._itemNodesMap[identity];
358 
359     if( nodes ) {
360       dojo.forEach( nodes, function(node) {
361         if( node._checkbox != null )
362           node._setCheckedState( state );
363       }, this );
364     }
365   },

Well, that’s it for our CheckboxTree and extended Checkbox widgets, the only thing left to do is to update our index.html file to include our new CSS file and add some of our new attributes. The new attributes aren’t really necessary if you are Ok with their default value. I have included them just to show how to overwrite their default values if needed.

  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/nihilo/nihilo.css" />
  7     <link rel="stylesheet" href="/js/tmpdir/themes/nihilo/tmpdir.css" />
  8 
  9     <script type="text/javascript" src="/js/dojotoolkit/dojo/dojo.js"
 10         djConfig="parseOnLoad:true, isDebug:false"></script>
 11 
 12     <script type="text/javascript">
 13       dojo.registerModulePath("tmpdir","/js/tmpdir");
 14       dojo.require("dojo.data.ItemFileWriteStore");
 15       dojo.require("tmpdir.CheckBoxTree");
 16 
 17     function myTree( domLocation ) {
 18       var store = new dojo.data.ItemFileWriteStore( {
 19               url: "/js/datastore/Family.json"
 20               });
 21       var model = new tmpdir.CheckBoxStoreModel( {
 22               store: store,
 23               query: {type: 'parent'},
 24               rootLabel: 'The Family',
 25               checkboxAll:  true,
 26               checkboxRoot: true,
 27               checkboxState: true,
 28               checkboxStrict: true
 29               });
 30       var tree = new tmpdir.CheckBoxTree( {
 31               model: model,
 32               id: "MenuTree",
 33               allowMultiState: true,
 34               branchIcons: true,
 35               nodeIcons: true
 36               });
 37       tree.placeAt( domLocation );
 38     }
 39     </script>
 40   </head>
 41 
 42     <body class="nihilo">
 43     <div id="CheckboxTree">
 44       <script type="text/javascript">
 45         myTree("CheckboxTree");
 46       </script>
 47     </div>
 48   </body>
 49 </html>

The only real import change we are making to our index.html file is on line 7, the inclusion of our CSS file. Please make sure the tmpdir theme is the same as the one you select for dijit on line 6.

If the CSS themes do not match your are NOT going to see any checkboxes unless you set the checkboxStyle attribute to anything else but “dijit”.

Below you see three different representations of the same Family Tree using some of our configurable attributes. These examples are all using dojo 1.4 to demonstrate the multi parent support. For example: In our Json file Chantal is referenced by both Peter and Mary as a child. In addition, Peter is defined as a ‘parent’ and but also referenced by John as a child.

 MultiStateTreeCollection

The example on the right uses the default HTML checkboxes BUT, as mentioned before, under the hood it still maintains the mixed state and thus your application can check if any of the parent checkboxes (John, Mary, Peter, Joan and Chantal) are in a mixed state. You could disable the mixed state support by setting the checkboxMultiState to false.

Checkbox Tree Configurable Attributes

Over the course of these two tutorials we have introduced a number of attributes to our CheckBoxTree and CheckBoxStoreModel widgets. The tables below list all attributes, default value and a short description.

Note: Any attribute that has a default value assigned is considered optional.

CheckBoxStoreModel Attributes

Attribute Type Default Description
checkboxAll Boolean true If true, every node in the tree will receive a checkbox regardless if the attribute ‘checkbox’ is specified or not for a dojo.data item. If the ‘checkbox’ attribute is not specified the default value checkboxState will be applied.
checkboxIdent String “checkbox” The name of the attribute (attribute of the dojo.data.item) that specifies the items checkbox initial state.

Example: { name:’Egypt’, type:’country’, checkbox: true }

If a dojo.data.item has no ‘checkbox’ attribute specified it will depend on the attribute ‘checkboxAll’ if one will be created automatically and if so what the initial state will be as specified by ‘checkboxState’.

checkboxMState String “mixed” The name of the attribute (attribute of the dojo.data/item) that will hold the checkbox mixed state. The mixed state is separate from the default checked/unchecked state of a checkbox.
checkboxRoot Boolean false If true, the root node will receive a checkbox even though it’s not a true entry in the store.

This attribute is independent of the showRoot attribute of the Tree itself. If the Tree attribute ‘showRoot’ is set to false the checkbox for the root will not show either.

checkboxState Boolean false The default value/state applied to every checkbox unless otherwise specified for the dojo.data item.

(see also: checkboxIdent)

checkboxStrict Boolean true If true, a strict parent-child checkbox relation is maintained. For example, if all children are checked the parent will automatically be checked or if any of the children are unchecked the parent will be unchecked.

CheckBoxTree Attributes

Attribute Type Default Description
branchIcons Boolean true Determines if the FolderOpen/FolderClosed icon is displayed.
checkboxMultiState Boolean true Determines if Multi State (mixed) checkbox behavior is required. If set to false the value property of a checkbox can only be ‘checked’ or ‘unchecked’. If true the value can be ‘checked’, ‘unchecked’ or ‘mixed’
checkboxStyle String “dijit” Sets the style of the checkbox to be used. The default is “dijit” any other value will force the use of the native HTML style checkbox. The visual representation of a mixed state checkbox is only supported with a dijit style checkbox.
nodeIcons Boolean true Determines if the Leaf icon is displayed.

Other Tree/Model Attributes

Attribute Type Default Description
showRoot Boolean true Determines if the root node should be displayed or hidden.
persist Boolean true Enables/disables the use of cookies for state saving.
autoExpand Boolean false Fully expand the tree on load. Overrides `persist`
 
rootLabel String “ROOT” Label of the fabricated root item (also see showRoot)

Step 8 – 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 save 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.

SequenceDiagram1.4

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

Let’s start with the onClick method:

182   _onClick: function( nodeWidget,  e) {
183     var domElement = e.target;
184 
184     if(domElement.nodeName != 'INPUT') {
185       return this.inherited( arguments );
186     }
187     this._publish("execute", { item: nodeWidget.item, node: nodeWidget} );
188 
188     this.model.updateCheckbox( nodeWidget.item, nodeWidget._checkbox.checked );
189     this.onClick( nodeWidget.item, nodeWidget, e );
190     if(nodeWidget._checkbox.checked) {
191       this.onNodeChecked( nodeWidget.item, nodeWidget);
192     } else {
193       this.onNodeUnchecked( nodeWidget.item, nodeWidget);
194     }
195     this.focusNode(nodeWidget);
196   },

On line 182 we adjust the _onClick function declaration to accommodate the new nodeWidget argument  and on line 189 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.

197   _onCheckboxChange: function( storeItem ) {
198     var model    = this.model,
199       identity = model.getIdentity(storeItem),
200       nodes    = this._itemNodesMap[identity];
201 
201     if( nodes ) {
202       dojo.forEach( nodes, function(node) {
203         if( node._checkbox != null ) {
204           node._checkbox.checked = this.model.getCheckboxState( storeItem );
205         }
206       }, this );
207     }
208   },

On line 200 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 202 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.

118   _getParentsItem: function( storeItem ) {
119     var parents = [];
120 
120     if( storeItem != this.root ) {
121       var references = storeItem[this.store._reverseRefMap];
122       for(itemId in references ) {
123         parents.push(this.store._itemsByIdentity[itemId]);
124       }
125       if (!parents.length) {
126         parents.push(this.root);
127       }
128     }
129     return parents 
130   },

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

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

If you take a look at _updateParentCheckbox you will notice that the original function (line 195 thru 214) is basically wrapped in a dojo.forEach loop. 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 I 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 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:

 13     function myTree( domLocation ) {
 14       var store = new dojo.data.ItemFileWriteStore( {
 15               url: "/js/datastore/Family.json"
 16               });
 17       var model = new tmpdir.CheckBoxStoreModel( {
 18               store: store,
 19               query: {type: 'parent'},
 20               rootLabel: 'The Family',
 21               checkboxAll: true,
 22               checkboxRoot: false,
 23               checkboxState: true
 24               });

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

Step-8-1
Step-8-2

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.

This ends our 1.4 tutorial..

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 situation 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.

111   _setCheckboxState: function(storeItem, newState ) {
112     var stateChanged = true;
113     var currState = false;
114 
114     if( storeItem != this.root ) {
115       currState = this.store.getValue(storeItem, this.checkboxIdent);
116       if( currState != newState && (currState !== undefined || this.checkboxAll ) ) {
117         this.store.setValue(storeItem, this.checkboxIdent, newState);
118         currState = newState;
119       } else {
120         stateChanged = false;  // No changes to the checkbox
121       }
122     } else {  // Tree root instance
123       if( this.root.checkbox != newState && ( this.root.checkbox !== undefined || this.checkboxRoot ) ) {
124         currState = this.root.checkbox = newState;
125       } else {
126         stateChanged = false;
127       }
128     }
129     if( stateChanged ) {  // In case of any changes trigger the update event.
130       this.onCheckboxChange(storeItem);
131     }
132     return currState;  // the new state of the checkbox
133   },

On line 114, 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 116 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 122 we do pretty much the same for the root element with the only exception that we do not use the actual store and validate the checkboxRoot attribute to see if a root checkbox is required.

Finally at line 129 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.

Lets go make our final changes to the getCheckboxState:

 18   getCheckboxState: function( storeItem) {
 19     var currState = undefined;
 20 
 20     if ( storeItem == this.root ) {
 21       if( typeof(storeItem.checkbox) == "undefined" ) {
 22         this.root.checkbox = undefined;    
 23         if( this.checkboxRoot ) {
 24           currState = this.root.checkbox = this.checkboxState;
 25         }
 26       } else {
 27         currState = this.root.checkbox;
 28       }
 29     } else {  
 30       currState = this.store.getValue(storeItem, this.checkboxIdent);
 31       if( currState == undefined && this.checkboxAll) {
 32         currState = this._setCheckboxState( storeItem, this.checkboxState );
 33       }
 34     }
 35     return currState  
 36   },

On line 31 we add the check if all store item require a checkbox and on line 32 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

 76   _updateChildCheckbox: function( parentItem,  newState ) {
 77     if( this.mayHaveChildren( parentItem ) ) {
 78       this.getChildren( parentItem, dojo.hitch( this,
 79         function( children ) {
 80           dojo.forEach( children, function(child) {
 81             this._setCheckboxState(child, newState);
 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 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 78 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 79 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 81. 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.

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

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 123. Ok, let’s go do the _updateParentCheckbox next.

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

On line 94 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 106, we move up one level in our tree (line 107) 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 114) we don’t care about any of the siblings because it only takes one unchecked child to uncheck the parent.

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.

133   validateData: function( storeItem,  scope ) {
134     if( !scope.checkboxStrict ) {
135       return;
136     }
137     var parentState = scope.getCheckboxState( storeItem );
138     scope.getChildren( storeItem, dojo.hitch( scope,
139       function(children) {
140         var allChecked = true;
141         var parentState = this.getCheckboxState( storeItem );
142         dojo.some( children, function( child ) {
143           if( this.mayHaveChildren( child )) {
144             this.validateData( child, this );
145           }
146           return !(allChecked = this.getCheckboxState( child ));
147         }, this);
148 
148         if( parentState != allChecked ) {
149           this._setCheckboxState( storeItem, allChecked);
150           this._updateParentCheckbox( storeItem, allChecked);
151         }
152       }),
153       function(err) {
154         console.error(this, ": validating checkbox data: ", err);
155       }
156     );
157   },

On line 134 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 138 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:

210   postCreate: function() {
211     this.connect(this.model, "onCheckboxChange", "_onCheckboxChange");
212     this.model.validateData( this.model.root, this.model );
213     this.inherited(arguments);
214   },

On line 212 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:

 11   updateCheckbox: function( storeItem,  newState ) {
 12     this._setCheckboxState( storeItem, newState );
 13     if( this.checkboxStrict ) {
 14       this._updateChildCheckbox( storeItem, newState );
 15       this._updateParentCheckbox( storeItem, newState );
 16     }
 17   },

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.

The last thing I want to do is to explain the differences between dojo version 1.3.2 and version 1.4. The last and final step, STEP 8

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

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-5 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.

 15     function myTree( domLocation ) {
 16       var store = new dojo.data.ItemFileWriteStore( {
 17               url: "/js/datastore/countries.json"
 18               });
 19       var model = new tmpdir.CheckBoxStoreModel( {
 20               store: store,
 21               query: {type: 'continent'},
 22               rootId: 'root',
 23               rootLabel: 'Continents',
 24               });

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:

Step-5 Tree with checkbox true

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.

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

On line 8 and 9 we define the new attributes checkboxAll and checkboxRoot both with their default value of true. Next on line 21 and 22 we determine if the tree root needs a checkbox. On line 29 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.

 15     function myTree( domLocation ) {
 16       var store = new dojo.data.ItemFileWriteStore( {
 17               url: "/js/datastore/countries.json"
 18               });
 19       var model = new tmpdir.CheckBoxStoreModel( {
 20               store: store,
 21               query: {type: 'continent'},
 22               rootId: 'root',
 23               rootLabel: 'Continents',
 24               checkboxAll: true,
 25               checkboxRoot: false,
 26               checkboxState: true
 27               });

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. In STEP 7 we are going to build the Parent-Child relationship.

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:

 10   <script type="text/javascript">
 11       dojo.registerModulePath("tmpdir","/js/tmpdir");
 12       dojo.require("dojo.data.ItemFileWriteStore");
 13       dojo.require("tmpdir.CheckBoxTree");
 14 
 15     function myTree( domLocation ) {
 16       var store = new dojo.data.ItemFileWriteStore( {
 17               url: "/js/datastore/countries.json"
 18               });
 19       var model = new tmpdir.CheckBoxStoreModel( {
 20               store: store,
 21               query: {type: 'continent'},
 22               rootId: 'root',
 23               rootLabel: 'Continents',
 24               });

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

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.

 40   _onClick: function(/*Event*/ e) {
 41     var domElement = e.target;
 42     if(domElement.nodeName != 'INPUT') {
 43       return this.inherited( arguments );
 44     }
 45     var nodeWidget = dijit.getEnclosingWidget(domElement);
 46     if(!nodeWidget || !nodeWidget.isTreeNode){
 47       return;
 48     }
 49     nodeWidget._checkbox.checked ^ true;
 50     this.model.updateCheckbox( nodeWidget.item, nodeWidget._checkbox.checked );
 51     if(nodeWidget._checkbox.checked) {
 52       this.onNodeChecked( nodeWidget.item, nodeWidget);
 53     } else {
 54       this.onNodeUnchecked( nodeWidget.item, nodeWidget);
 55     }
 56   },
 57 
 58   postCreate: function() {
 59     this.connect(this.model, "onCheckboxChange", "_onCheckboxChange");
 60     this.inherited(arguments);
 61   },
 

On line 50 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:

SequenceDiagram1

Next STEP 6 Adding attributes.

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:

 21 dojo.declare( "tmpdir.CheckBoxTree", dijit.Tree,
 22 {
 23   onNodeChecked: function(/*dojo.data.Item*/ storeItem, /*treeNode*/ nodeWidget) {
 24   },
 25 
 26   onNodeUnchecked: function(/*dojo.data.Item*/ storeItem, /* treeNode */ nodeWidget) {
 27   },
 28 
 29   _onClick: function(/*Event*/ e) {
 30     var domElement = e.target;
 31     if(domElement.nodeName != 'INPUT') {
 32       return this.inherited( arguments );
 33     }
 34     var nodeWidget = dijit.getEnclosingWidget(domElement);
 35     if(!nodeWidget || !nodeWidget.isTreeNode){
 36       return;
 37     }
 38     nodeWidget._checkbox.checked ^ true;
 39     if(nodeWidget._checkbox.checked) {
 40       this.onNodeChecked( nodeWidget.item, nodeWidget);
 41     } else {
 42       this.onNodeUnchecked( nodeWidget.item, nodeWidget);
 43     }
 44   },
 45 
 46   _createTreeNode: function( args ) {
 47     return new tmpdir._CheckBoxTreeNode(args);
 48   }
 49 });

On line 23 and 26 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.

 24       var tree = new tmpdir.CheckBoxTree( {
 25               model: model,
 26               id: "MenuTree"
 27               });
 28       tree.placeAt( domLocation );
 29 
 30       dojo.connect( tree,"onNodeChecked", function(storeItem, nodeWidget){
 31         alert( store.getValue(storeItem,"name") + " got checked..");
 32         }
 33       );
 35     }

On line 30, 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:

Step-4 Event generated

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:

 46   _onCheckboxChange: function(/*dojo.data.Item*/ storeItem ) {
 47     var model    = this.model,
 48       identity = model.getIdentity(storeItem),
 49       node    = this._itemNodeMap[identity];
 50 
 51     if( node ) {
 52       if( node._checkbox != null ) {
 53         node._checkbox.checked = this.model.getCheckboxState( storeItem );
 54       }
 55     }
 56   },
 57 
 58   postCreate: function() {
 59 //    this.connect(this.model, "onCheckboxChange", "_onCheckboxChange");
 60     this.inherited(arguments);
 61   },
 62 
 63   _createTreeNode: function( args ) {
 64     return new tmpdir._CheckBoxTreeNode(args);
 65   }
 66 });

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 59 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 46. For the time being I have commented out line 58 just because the method _onCheckboxChange on our model doesn’t exist yet.

On line 46 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 51 we check if the tree node exists for this particular store item. At line 52 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.

Let’s go the next step, STEP 5.

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:

  4 dojo.declare( "tmpdir._CheckBoxTreeNode", dijit._TreeNode,
  5 {
  6 
  7 });
  8 
  9 dojo.declare( "tmpdir.CheckBoxTree", dijit.Tree,
 10 {
 11 
 12 });

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 9 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.

 14       dojo.registerModulePath("tmpdir","/js/tmpdir");
 15       dojo.require("dojo.data.ItemFileReadStore");
 16       dojo.require("tmpdir.CheckBoxTree");
 17 
 18       function myTree( domLocation ) {
 19         var store = new dojo.data.ItemFileReadStore( {url: "/js/datastore/countries.json"});
 20         var model = new dijit.tree.ForestStoreModel( {
 21                   store: store,
 22                   query: {type: 'continent'},
 23                   rootId: 'root',
 24                   rootLabel: 'Continents'
 25                   });
 26         var tree = new tmpdir.CheckBoxTree( {
 27                   model: model,
 28                   id: "MenuTree"
 29                   });

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 16 we now include our file CheckboxTree.js and on line 26 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 
  4 dojo.declare( "tmpdir._CheckBoxTreeNode", dijit._TreeNode,
  5 {
  6   _checkbox: null,
  7 
  8   _createCheckbox: function() {
  9     this._checkbox = dojo.doc.createElement('input');
 10     this._checkbox.type    = 'checkbox';
 11     this._checkbox.checked = false;
 12     dojo.place(this._checkbox, this.expandoNode, 'after');
 13   },
 14 
 15   postCreate: function() {
 16     this._createCheckbox();
 17     this.inherited( arguments );
 18   }
 19 });
 20 
 21 dojo.declare( "tmpdir.CheckBoxTree", dijit.Tree,
 22 {
 23   _onClick: function(/*Event*/ e) {
 24     var domElement = e.target;
 25     if(domElement.nodeName != 'INPUT') {
 26       return this.inherited( arguments );
 27     }
 28     var nodeWidget = dijit.getEnclosingWidget(domElement);
 29     if(!nodeWidget || !nodeWidget.isTreeNode){
 30       return;
 31     }
 32     nodeWidget._checkbox.checked ^ true;
 33   },
 34 
 35   _createTreeNode: function( args ) {
 36     return new tmpdir._CheckBoxTreeNode(args);
 37   }
 38 });

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 6. Next we declare the method that will create the actual checkbox. On line 9 we tell dojo to create a DOM element ‘input’ and set the type to ‘checkbox’. Line 12 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 12 is to place our checkbox right after the +/- sign of a tree node.

On line 15 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 16 we call our own method to create the checkbox and on line 17 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 35, 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 9 again). Next, on line 28, 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 32, 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-step-3

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,STEP 4, 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 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 
 10     <script type="text/javascript">
 11     dojo.require("dojo.data.ItemFileReadStore");
 12     dojo.require("dijit.Tree");
 13 
 14     function myTree( domLocation ) {
 15       var store = new dojo.data.ItemFileReadStore( {
 16               url: "/js/datastore/countries.json"});
 17       var model = new dijit.tree.ForestStoreModel( {
 18               store: store,
 19               query: {type: 'continent'},
 20               rootId: 'root',
 21               rootLabel: 'Continents'});
 22       var tree = new dijit.Tree( {
 23               model: model,
 24               id: "MenuTree"});
 25       tree.placeAt( domLocation );
 26     }
 27     </script>
 28   </head>
 29     <body class="soria">
 30     <div id="CheckboxTree">
 31       <script type="text/javascript">
 32         myTree("CheckboxTree");
 33       </script>
 34     </div>
 35   </body>
 36 </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 22 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:

Tree-phase-1

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.

In the next step, STEP 3, we will be adding some checkboxes.