Skip to content

CheckBox Tree Usage

Peter Jekel edited this page Oct 19, 2013 · 29 revisions

Content

* [Branch versus Leaf](#branch-versus-leaf) * [Working with Events](#working-with-events) * [Deleting Tree Node](#deleting-tree-nodes) * [Sorting Tree Node](#sorting-tree-nodes)

Branch versus Leaf

One of the most common questions asked is: _`How do I distinguish between tree branches and tree leafs?'_. The simple answer is: check the [isExpandable](CheckBox-Tree-API#wiki-isExpandable) property of a tree node widget or the associated DOM attribute **_branch_** [1]. The **_isExpandable_** property and **_branch_** attribute both indicates if a tree node has children, that is, DOM child nodes.

Tree nodes are template driven widgets which are automatically generated when the tree is created. The cbtree node template used is located at cbtree/templates/cbtreeNode.html. Each generated HTML element with the class dijitTreeRow also gets the branch attribute whose value is the value of the tree node widget property isExpandable.

Test for branches programmatically

Whenever a tree node widget is passed as an argument to a function, typically an event handler, one can simple test the isExpandable property to determine if the tree node is a branch or not.

function onCheckBoxClick(item, nodeWidget, event) {
    if (nodeWidget.isExpandable) {
       // Node is a branch
	         ...
	} else {
       // Node is a leaf
	         ...
	}
}

Although not very efficient, compared to the approach above, you can also access the DOM branch attribute in your application like:

require(["dojo/dom-attr", ... ], function (domAttr, ... ) {
	         ...
    function onCheckBoxClick(item, nodeWidget, event) {
        var isBranch = domAttr.get(nodeWidget.rowNode, "branch");
	}
}

Test using a selector

The following example collects all tree node branches in the DOM and iterates over them. For each DOM node the dijit registry is called to get the associated tree node widget.

require(["dojo/query", "dijit/registry", ... ], function (query, regsitry, ...) {
	         ...
    var allTreeBranches = query(".dijitTreeRow[branch=\"true\"]");
    allTreeBranches.forEach(function (domNode) {
		var nodeWidget = registry.getEnclosingWidget(domNode);
                      ...
	    console.log(nodeWidget.label);
    });
}

Next, lets assume you want to give the label of tree nodes a different background color depending on whether or not a tree node is a branch.

<style type="text/css">
    .dijitTreeRow[branch="true"] .dijitTreeLabel {
        background-color: yellow;
                 ...
	}
</style>

[1]The branch attribute is available since cbtree release cbtree-v0.9.3-4

Working with Events

With dojo 1.7 a new event handling module was introduced called dojo/on. The intend of this module is/was to make event handling with dojo easier and mimic some of the DOM4 Event system. However, dojo/on is not a self contained DOM-4 Event system implementation. In addition, dijit widgets inherit a modified version of dojo/on to make it backward compatible with callback methods and non-DOM compliant events. The dijit/_WidgetBase.on() method tries to map event types, case insensitive that is, to callback functions whose name starts with "on" like onClick() or onDblClick().

The next sections discuss the difference in implementation of event listeners for callbacks and events.

Callbacks

Before registering an event listener with a dijit widget we must first check if the widget has a method that could be auto linked with the event type. For example, if we want to listen for an event named "myEvent", does the widget have a method called onMyEvent() or onmyevent()? If so, we need to define our event listener as a callback function with the same set of argument as the widget's onMyEvent() method.

Let's assume our widget has a method (callback) called onMyEvent() with a signature like: onMyEvent( item, node, event ) in which case we can establish our event listener in two ways:

  1. Using dojo/aspect or,
  2. Call the widget's on() method.
Using dojo/aspect.

Using dojo/aspect we attach after advice to the widget method so our event listener gets called each time the widget calls onMyEvent() like:

require(["dojo/aspect", "cbtree/Tree", ... ], function (aspect, Tree, ... ) {
    // Event listener
    function clickEvent( item, node, event ) {
      console.log( "A Tree Node was clicked" );
    }
                   ...
    var myTree = new Tree( {model:someModel, ... } );
    aspect.after( myTree, "onMyEvent", clickEvent, true );
                   ...
})

Make sure the fourth argument is set to true otherwise clickEvent() is not called with the original arguments of onMyEvent() but with the result instead. Also, notice that when calling aspect.after() we specify the callback function name "onMyEvent" and not an event type.

Using widget on() method.

Using the widget's on() method we specify the event type "myEvent" and not the callback function name, like:

require(["cbtree/Tree", ... ], function (Tree, ... ) {
    // Event listener
    function clickEvent( item, node, event ) {
      console.log( "A Tree Node was clicked" );
    }
                   ...
    var myTree = new Tree( {model:someModel, ... } );
    myTree.on( "myEvent", clickEvent );
                   ...
})

The above example accomplishes exactly the same thing as dojo/aspect did in the first example simply because the underlaying _WidgetBase.on() method is able to map the event type "myEvent" to the callback onMyEvent() and as a result calls dojo/aspect on our behalf.

Events

This time, let's assume our widget does NOT have a callback that could be auto linked to the event type "myEvent". Instead the widget actually emits events of type "myEvent". In this case both dojo/aspect and the widget's on() method will automatically create the callback, which in our case will be named "onmyEvent".

The big difference between the widget calling callback functions programmatically and emitting events is the way the event listener will be called. In the latter case the event handlers are called with just one argument, the event.

require(["cbtree/Tree", ... ], function (Tree, ... ) {
    // Event listener
    function clickEvent( event ) {
      console.log( "A Tree Node was clicked" );
    }
                   ...
    var myTree  = new Tree( {model:someModel, ... } );
    myTree.on( "myEvent", clickEvent );
                   ...
})

In the above example notice that the function clickEvent() now is called with just a single argument event. The event is a JavaScript key:value pairs object.

In case of true events that is, the event name can not be mapped to a callback function, the event name is handled case sensitive. Therefore, as a good practise, always use the correct case for event names regardless if you are registering for an event or callback. Note that DOM-3/4 always handles event types case sensitive.

Callbacks and Events

The CheckBox Tree modules use both callbacks and events. Callbacks are used whenever dojo or dijit API's require them, for example the dijit model API dijit/tree/model, otherwise events are used.

The tables below list the event names and associated callback by CheckBox Tree module. If for any given event type a callback is specified, the arguments column specifies the list of arguments passed to the event listener.

The arguments passed to event handles are defined as follows:

Argument Description
event Event object, the object is context specific
item Store object
node Tree node widget

Tree Events

Event Type Callback Arguments Description
click onClick (item, node, event) A tree node is clicked.
checkBoxClick onCheckBoxClick (item, node, event) A checkbox is clicked.
close onClose (item, node) Node closed, a tree node collapsed.
dblClick onDblClick (item, node, event) Double click
open onOpen (item, node) Node opened. a tree node is expanded.
event onEvent (item, event, value) User event successful.
load onLoad (void) Tree finished loading.

Model Events

The CheckBox Tree models, although not widgets, offer the same on() functionality as widgets do. Therefore you can simply register a model event listener like model.on( "dataValidated", myEventHandler );

Event Type Callback Arguments Description
change onChange (item, propertyName, newValue, oldValue) Property of a store item changed.
childrenChange onChildrenChange (parent, newChildrenList) The children of a node changed.
dataValidated onDataValidated (void) The store data has been validated.
delete onDelete (item) An item was removed from the store.
pasteItem onPasteItem (item, insertIndex, before) Item was pasted at a new location.
rootChange onRootChange (item, action) The children of the tree root changed.
reset onReset (void) The model is being reset.

Store Events

Store events are available if, and only if, the store is made Eventable or the store is a cbtree/store/Object store.

Event Type Callback Argument Description
change (event) Property of a store object changed.
close1 onClose (count, cleared) The store was closed.
new (event) A new store object was added.
remove (event) A store object was removed/deleted.
1 The close event is the only store event using a callback and therefore accessible even if the store isn't made eventable.

An example

The following example demonstrate the use of events and callbacks:

require( ["cbtree/Tree",
          "cbtree/model/ForestStoreModel",
          "cbtree/store/Memory",
          "cbtree/store/Eventable"], function (Tree, ForestStoreModel, Memory, Eventable) {
                    ...
  // Create an eventable store and listen for events of type "new".
  var myStore = Eventable( new Memory( {data: myData, idProperty:"name"} ));
  myStore.on( "new", function (event) {
    console.log( "An event of type: " + event.type + "was recieved" );
    console.log( "Object with id: " + this.getIdentity( event.item ) + " was added");
  });

  var myModel = new ForestStoreModel( {store:myStore, query:{type:"parent"}} );
  myModel.on( "dataValidated", function () {
    console.log( "Store data has been validated" );
  });

  var myTree  = new Tree( {model:myModel}, "CheckboxTree" };
  myTree.on( "load", function () {
    console.log( "The Tree has been loaded." );
  }
  myTree.startup();
                    ...
  myStore.put( {name:"Lisa", lastName:"Simpson", hair:"blond"} );
}

DOM-4 Events

If you want to learn more about the DOM-4 Events checkout DOM Events. If you're interested in a dojo style implementation of a fully DOM-4 compliant Event system checkout the event system as part of my IndexedDB project.

Deleting Tree Nodes

In contrast to the standard dijit Tree, the CheckBox Tree offers the options to delete tree nodes using the keyboard. However, the functionality is disabled by default. There are two tree properties associated with this feature:

  • enableDelete
  • deleteRecursive

The enableDelete enables or disables the keyboard delete feature whereas deleteRecursive determines if the descendants of the to be deleted item(s) are to be removed from the store as well, the default is false.

To delete an item using the keyboard simply select the tree node(s) and press the CTRL+DELETE keys.

Sorting Tree Nodes

The order in which tree nodes are displayed depends on the model query and the order of the store records. Lets assume we have the following set or records in our store:

{ name:"Root", parent:[] },

{ name:"Lisa", parent:["Homer","Marge"], hair:"blond", age: 7 },
{ name:"Bart", parent:["Homer","Marge"], hair:"blond", age: 8 },
{ name:"Maggie", parent:["Homer","Marge"], hair:"blond", age: 3 },
{ name:"Patty", parent:["Jacqueline"], hair:"blond", age: 40 },
{ name:"Selma", parent:["Jacqueline"], hair:"blond", age: 40 },
{ name:"Rod", parent:["Ned"], hair:"blond", age: 9 },
{ name:"Todd", parent:["Ned"], hair:"blond", age: 10 },
{ name:"Abe", parent:["Root"], hair:"none", age: 75 },
{ name:"Mona", parent:["Root"], hair:"none", age: 72 },
{ name:"Jacqueline", parent:["Root"], hair:"none", age: 70 },
{ name:"Homer", parent:["Abe","Mona"], hair:"none", age: 40 },
{ name:"Marge", parent:["Jacqueline"], hair:"none", age: 38 },
{ name:"Ned", parent:["Root"], hair:"none", age: 38 },
{ name:"Apu", parent:["Root"], hair:"black", age: 50 },
{ name:"Manjula", parent:[Apu], hair:"black", age: 52}

When we create the model using most of the model's default settings like:

var model = new TreeStoreModel( { store: store, query: {name: "Root"} });

The root children will be displayed in the order they appear in the store, that is: Abe, Mona, Jacqueline, Ned and finally Apu. The same goes for any children, for example, expanding tree node 'Homer' displays Lisa, Bart and Maggie.

As of cbtree v0.9.3-3 the Tree models have a new property called options which is a JavaScript key:value pairs object defining the sort order of the tree. The options object is currently defined as follows:

options     = "{" "sort" ":" sortOptions "}"
sortOptions = "[" SortInfo ["," SortInfo]* "]" / Function
SortInfo    = "{" attribute ["," descending] ["," ignoreCase] "}"
attribute   = "attribute" ":" JSidentifier
descending  = "descending" ":" boolean
ignoreCase  = "ignoreCase" ":" boolean
boolean     = "true" / "false"

See also sort directives. For example:

var options = {sort: [
  {attribute: "name", descending: true, ignoreCase: true},
  {attribute: "hair", ignoreCase: true},
  {attribute: "age"}
]};

The value of both the descending and ignoreCase sort info properties defaults to false. The sort operation is performed in the order in which the sort information appears in the sort options array. Given the example above, tree nodes are first sorted by name followed by hair color and finally age. Creating a model with the options property specified would look like:

var mySortOptions = {sort: [
  {attribute: "name", descending: true, ignoreCase: true},
  {attribute: "hair", ignoreCase: true},
  {attribute: "age"}
]};
var model = new TreeStoreModel( { store: store,
                                  query: {name: "Root"},
                                  options: {sort: mySortOptions}
                                });

Using a custom sort function

Alternatively, instead of specifying the options sort property as an object you can also specify a custom sort function. Consider the following example:

function mySortFunc(itemA, itemB) {
  if (itemA.name > itemB.name) {
    return 1;
  } else if (itemA.name < itemB.name) {
    return -1;
  }
  return 0;
}
var model = new TreeStoreModel( { store: store, query: {name: "Root"}, options: {sort: mySortFunc}});

Here the custom sort function mySortFunc is called with two store items that matched the associated model query. The custom sort function MUST return -1, 0 or 1. See JavaScript Array.prototype.sort for additional information.

Be aware that using the sort feature overrides the store's optional before property when adding records to the store. The store will still insert the record at the desired location but because the model query results are sorted the significance of the actual location is lost to the model and tree.

Clone this wiki locally