-
Notifications
You must be signed in to change notification settings - Fork 34
CheckBox Tree Usage
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 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 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:
- Use the node widget
set()
accessor, or - 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).
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();
});
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();
...
});
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.
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.
To understand the Checkbox Tree checkboxes, you need to understand how the checkboxes are constructed. Each Checkbox Tree checkbox consists of two parts:
- The dijit checkbox widget and,
- 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.
Checkboxes will be included in the form data set if, and only if, they meet all of following requirements:
- The checkbox must have an <input> element of type checkbox in the DOM and,
- The <input> element must have both its name and value attribute set and,
- The <input> element must be "on", that is, checked and,
- 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:
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.
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
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.
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");
}
}
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
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.
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:
- Using
dojo/aspect
or, - Call the widget's
on()
method.
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 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.
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.
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 |
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.
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 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. |
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"} );
}
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.
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.
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}
});
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.