Skip to content

CheckBox Tree Usage

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

Content

* [Checkbox Access and Visibility](#checkbox-access-and-visibility) * [Tree Branch versus Leaf](#tree-branch-versus-leaf) * [Checkboxes in HTML forms](#checkboxes-in-html-forms) * [Working with Events](#working-with-events) * [Deleting Tree Node](#deleting-tree-nodes) * [Sorting Tree Node](#sorting-tree-nodes)

Checkbox Access and Visibility

The CheckBox Tree offers several options to manipulate the creation, access and visibility of checkboxes. By default, each tree node will automatically get a checkbox, the following properties influence the checkbox behavior:

<tr>
	<td rowspan="5">CheckBox Tree</td>
	<td>branchCheckBox</td>
	<td>true</td>
	<td>
		Determines if the checkbox of a tree branch is displayed. If <code>false</code>,
		the checkbox is still created and placed in the document but hidden from view.
	</td>
</tr>
<tr>
	<td>branchReadOnly</td>
	<td>false</td>
	<td>
		Determines if the checkbox of a tree branch is read-only. If <code>true</code>,
		The checkbox is displayed but grayed out and can not be clicked. This property
		only has affect if both tree properties <em>checkBoxes</em> and <em>branchCheckBox</em>
		are <code>true</code>.
	</td>
</tr>
<tr>
	<td>checkBoxes</td>
	<td>true</td>
	<td>
		Determines if the checkbox tree will get any checkboxes. If <code>false</code>
		no checkboxes will be created and/or displayed. As a result, there will be no
		checkboxes in the document.
	</td>
</tr>
<tr>
	<td>leafCheckBox</td>
	<td>true</td>
	<td>
		Determines if the checkbox of a tree leaf is displayed. If <code>false</code>,
		the checkbox is still created and placed in the document but hidden from view.
	</td>
</tr>
<tr>
	<td>leafReadOnly</td>
	<td>false</td>
	<td>
		Determines if the checkbox of a tree leaf is read-only. If <code>true</code>,
		the checkbox is displayed but grayed out and can not be clicked. This property
		only has affect if both tree properties <em>checkBoxes</em> and <em>leafCheckBox</em>
		are <code>true</code>.
	</td>
</tr>

<tr>
	<td rowspan="2">Tree Model</td>
	<td>checkedAll</td>
	<td>true</td>
	<td>
		If true, every store object will receive a so-called <em>checked</em> state property.
		The name of the actual property is defined by the value of the model property
		<a href="Model-API#wiki-checkedattr">checkedAttr</a>. If the 'checked' state
		property is added to a store object, its initial state is defined by the value
		of the model property <a href="Model-API#wiki-checkedstate">checkedState</a>.
	</td>
</tr>
<tr>
	<td>enabledAttr</td>
	<td>""</td>
	<td>
		The name of the store object property that holds the <em>enabled</em> state
		of the checkbox. Even though it is referred to as the <em>enabled</em> state
		the tree will only use this property to set the <em>readOnly</em> property of
		a checkbox, as disabling the widget (DOM element) would exclude it from HTTP
		POST operations.
	</td>
</tr>
Class property default description

Hidden Checkboxes

Hidden checkboxes behave like any normal checkbox, they're just hidden from view. Therefore, in your application you can still manipulate the checkbox state using either the Tree node widget accessors, get() and set(), or the model methods getChecked() or setChecked(), for example:

require(["dojo/query", "dijit/registry", "cbtree/Tree", ... ], function (query, registry, cbTree, ... ) {

	function checkboxState(nodeWidget) {
	   var state = nodeWidget.get("checked");
	   var label = tree.model.getLabel(item);

	   alert( "The state for " + label + " is: " + state );
	}

	// Create a tree without any visible checkboxes
	var tree = new cbTree({branchCheckBox: false, leafCheckBox: false, ... }, "myTree");
	tree.startup();
							   ...
	query(".dijitTreeRow").forEach(function (domNode) {
		checkboxState(registry.getEnclosingWidget(domNode));
	});
});

Even though the checkboxes are hidden from view, they will be included in HTML forms as long as the associated tree node is visible. See Checkboxes in HTML forms for additional information.

Read-only Checkboxes

Read-only checkboxes are normal checkboxes but with their property readOnly set to true which results in the checkbox being grayed out and is therefore not clickable. There are two ways to change the read-only property of a checkbox:

  1. Use the node widget set() accessor, or
  2. Use the model setEnabled() method.

The model method setEnabled() ONLY has affect if the model's enabledAttr is assigned a property name (see example below).

Using the widget set accessor.

require([ ... ], function ( ... ) {
	function checkBoxClicked(item, nodeWidget, event) {
		nodeWidget.set("enabled", false);    // Make checkbox read-only
					...
	}

	var tree  = new cbTree({model: model, ... }, "myTree");
	tree.on("checkBoxClick", checkBoxClicked);
	tree.startup();
});

Using the model setEnabled method.

The following example defines a simple dataset which is loaded into a store. The store property readable is used to determine if the checkbox associated with the store object is read-only or not. If a store object does not have the readable property the associated checkbox will be readable by default.

require([ ... ], function ( ... ) {
    var data = [
		{name: "homer", parent: null, readable: true},
		{name: "bart", parent: "homer", readable: true},
		{name: "lisa", parent: "homer", readable: false},
		{name: "maggie", parent: "homer"},
	];

	function checkBoxClick(item, nodeWidget, event) {
		this.model.setEnabled(nodeWidget.item, false);    // Make checkbox read-only
					...
	}

    var store = new Store({data: data, ... });
	var model = new ForestStoreModel({store: store, enabledAttr: "readable", ... });
	var tree  = new CBTree({model: model, ... }, "myTree");

	tree.on("checkBoxClicked", checkBoxClicked);
	tree.startup();
					...
});

No Checkboxes Showing

In the event the Checkbox Tree is displayed but no checkboxes are visible, like in the image below, you are missing the required Checkbox Tree css file for the dijit theme. Notice the empty space between the expando icons and the folder icons.

In order for any checkbox to be visible you MUST load the appropriate Checkbox Tree theme file. The Checkbox Tree theme files correspond with the dijit theme therefore, if you are running the dijit theme claro you must, at a minimum, load the following css files in the specified order:

<style type="text/css">
    @import url("./dijit/themes/claro/claro.css");
    @import url("./cbtree/themes/claro/claro.css");
</style>
       ...
<body class="claro">
       ...
</body>

Also notice that the body class MUST match the dijit theme name, in this case claro.

Checkboxes in HTML forms

By default, the Checkbox Tree checkboxes are NOT included in form submissions. The functionality can be enabled using the tree property attachToForm The simplest way to automatically include checkboxes in the form data set is to set the tree property attachToForm to true, in which case all eligible checked checkboxes will be included in the form submission.

About Checkboxes

To understand the Checkbox Tree checkboxes, you need to understand how the checkboxes are constructed. Each Checkbox Tree checkbox consists of two parts:

  1. The dijit checkbox widget and,
  2. The associated HTML <input ... > element of type 'checkbox'.

When a checkbox is created as part of a tree node, the Checkbox Tree sets the name property of the checkbox widget to the dijit generated id of the widget, the value property to the tree node label and the checked property to the associated property value of the store object or, if the store object doesn't have a corresponding checked state property, the default value defined on the model.

Eligible Form CheckBoxes

Checkboxes will be included in the form data set if, and only if, they meet all of following requirements:

  1. The checkbox must have an <input> element of type checkbox in the DOM and,
  2. The <input> element must have both its name and value attribute set and,
  3. The <input> element must be "on", that is, checked and,
  4. The <input> element must be enabled.

In the context of a Checkbox Tree, having an <input> element of type checkbox in the DOM basically means: the tree node associated with the checkbox must be visible. Children of collapsed tree branches are NOT visible and therefore not eligible. Also, hidden checkboxes although invisible, are eligible as long as the tree node is visible.

By default, the Checkbox Tree does NOT set the name attribute of the <input> element, only the name property of the checkbox widget, therefore Checkbox Tree checkboxes are not valid Successful Controls and thus not eligible.

To make Checkbox Tree checkboxes eligible for inclusion in the form data set, select one of the following options:

Basic Checkbox Submission

When the Checkbox Tree property attachToForm is set to boolean true, the Checkbox Tree will also set the name attribute of the <input> element to the checkbox widget id and the value attribute to the tree node label. Having both attributes set turns the checkbox into a Successful Control ready to be included in the form data set.

var myTree = new CBTree({attachToForm: true, ... });

All eligible checkboxes are submitted as separate name = value pairs.

Advanced Checkbox Submission

The Checkbox Tree property attachToForm can also be specified as an JavaScript key:value pairs object, in which case the Checkbox Tree tries to load the TreeOnSubmit extension if not already loaded. The TreeOnSubmit extension collects the checked states of some or all store objects and includes the result as a single parameter in the form data set. The parameter value is a JSON encoded array of objects, each object representing the checked state of a store object.

var myTree = new CBTree({attachToForm: {
                    name: "checkboxes",
                    checked: ["mixed", true],
                    domOnly: false
                }, ... });

On the server side, assuming the form uses the HTTP POST method, you can now simply do something like:

<?php
    $storeItems = json_decode($_POST["checkboxes"]);
    forEach ($storeItems as $item) {
               ...
    }
?>

The advanced approach also allows the options of submitting unchecked or "mixed" state checkboxes which would not be possible with the standard HTML (basic) approach. For example:

var myTree = new CBTree({attachToForm: {checked: [true, false]}, ... });

The Checkbox tree extension TreeOnSubmit collects the checked states directly from the underlaying store instead of the DOM. Store objects that would otherwise be ineligible (e.g not included in the DOM), will be included in the form data set.

Please refer to the TreeOnSubmit extension for a more detailed description and additional examples. As simple demo of Advanced Checkbox Submission can be found at cbtree/demos/store/tree30.html

Tree Branch versus Leaf

Each tree node widget has a property called isExpandable indicating, as the name implies, if the widget is expandable or in other words: if the tree node has children. The isExpandable widget property is also exposed as the expandable [1] attribute on all HTML elements with class dijitTreeRow:

<div class="dijitTreeRow" expandable="true" ... >

To determine if a tree node is a branch or a leaf on the tree simply check the isExpandable property of a tree node widget or the associated HTML attribute expandable. If the value is true it is a branch, otherwise it is a leaf.

Testing Tree node widgets

Whenever a tree node widget is passed as an argument to a function, typically an event handler, one can simple test the isExpandable property of the widget 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 as efficient, compared to the approach above, you can also access the HTML expandable attribute in your application like:

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

Testing HTML elements

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, ...) {
	         ...
    query(".dijitTreeRow[expandable='true']").forEach(function (domNode) {
        var nodeWidget = registry.getEnclosingWidget(domNode);
                      ...
        console.log(nodeWidget.label);
    });
}

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

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

[1]The HTML attribute expandable 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
event onEvent (item, event, value) User event successful.
load onLoad (void) Tree finished loading.
open onOpen (item, node) Node opened. a tree node is expanded.
submit 1 onSubmit (formNode, treeWidget, event) Submit button on a form is clicked.

1 The submit event on a tree is only generated if the Checkbox Tree has a form as one of its ancestors.

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.
close2 onClose (count, cleared) The store was closed.
new (event) A new store object was added.
remove (event) A store object was removed/deleted.
2 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.