The Tree
dijit is an amazing
piece of engineering. Using completely native DHTML, it looks and acts
just like you'd expect a hierarchical tree to look and act, it
supports drag-and-drop operations, and it's flexible enough to bind to
an arbitrary data source. Like any other complex piece of machinery,
there are a few fundamentals to pick up before you get rolling with
it, but they're all fairly intuitive once you've connected the dots
that first time. This is one of the longer sections in the chapter
because the Tree
is quite powerful
and offers an extensive set of features. Although we won't elaborate
on a11y, you should also be cognizant that the Tree
is quite accessible with the keyboard
via arrow keys, the Enter key, and so on.
Tip
A good understanding of the dojo.data
API is especially helpful for
working with the Tree
dijit. See
Chapter 9 for more details.
Before reading through any code, it's helpful to be aware of at least a few things:
- Trees and forests
A tree is a hierarchical data structure that contains a single root element. A forest, on the other hand, is a hierarchical structure just like a tree except that it does not have a single root node; instead, it has multiple root nodes. As we'll see, distinguishing between a tree and a forest is a common issue because many data views are conveniently expressed as a tree with a single root node even though the data that backs the view is a forest with an implied root node.
- Nodes
A tree is a hierarchical organization of nodes and the linkages between them. The specific type of node that is used by
dijit.Tree
isdijit._TreeNode
; the leading underscore in this case signals that you'd never be using a_TreeNode
outside of aTree
. There are, however, several properties of_TreeNode
that are useful to manipulate directly, as we'll see in upcoming examples.- Data agnosticism
The
Tree
dijit is completely agnostic to the data source that backs it. Prior to version 1.1, it read directly from an implementation of thedojo.data
API, which is quite flexible and provides a uniform layer for data access, but as of the 1.1 release, the enhancement of an additional intermediating layer between thedojo.data
model and theTree
was added. These intermediating layers aredijit.tree.TreeStoreModel
anddijit.tree.ForestStoreModel
, respectively. Much of the motivation for the change was to make theTree
much more robust and amenable to drag-and-drop operations.
Tip
When you execute dojo.require("dijit.Tree")
the ForestStoreModel
and TreeStoreModel
come along with the
Tree
itself.
To ease in to what the Tree
can do for you, assume that you have a really simple data source
that serves up dojo.data.ItemFileReadStore
JSON along the
lines of the following:
{ identifier : 'name', label : 'name', items : [ { name : 'Programming Languages', children: [ {name : 'JavaScript'}, {name : 'Python'}, {name : 'C++'}, {name : 'Erlang'}, {name : 'Prolog'} ] } ] }
So far, so good. Instead of parsing the data yourself on the
client, you get to use dojo.data
to abstract the data for you. Hooking up an actual ItemFileReadStore
is as easy as pointing
it to the URL that serves the data and then querying into it. The
following tag, when instantiated by the parser, would do the trick
if the file were served up from the working directory as
programmingLanguages.json, and it would have a
global identifier of dataStore
that would be accessible:
<div dojoType="dojo.data.ItemFileReadStore" jsId="dataStore" url="./programmingLanguages.json"></div>
Before the data gets fed into the Tree
, however, it will be mediated through
a TreeStoreModel
. (We'll work
through the implications of using a ForestStoreModel
in a moment.) The
complete API listing for an intermediating TreeStoreModel
will be presented
momentarily, but for now, all that's pertinent is that we have to
point the TreeStoreModel
at the
ItemFileReadStore
and provide a
query. The following TreeStoreModel
would query the dojo.data
store with global identifier
dataStore
for all name
values:
<div dojoType="dijit.tree.TreeStoreModel" jsId="model" store="dataStore" query="{name:'*'}"></div>
Finally, the only thing left to do is point the Tree
dijit at the TreeStoreModel
like so:
<div dojoType="dijit.Tree" model="model"></div>
That's it. Example 15-7 puts it all together, and Figure 15-3 shows the result.
Example 15-7. Simple Tree with a root
<html> <head> <title>Tree Fun!</title> <link rel="stylesheet" type="text/css" href="http://o.aolcdn.com/dojo/1.1/dojo/resources/dojo.css" /> <link rel="stylesheet" type="text/css" href="http://o.aolcdn.com/dojo/1.1/dijit/themes/tundra/tundra.css" /> <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js" djConfig="parseOnLoad:true,isDebug:true"> </script> <script type="text/javascript"> dojo.require("dijit.Tree"); dojo.require("dojo.data.ItemFileReadStore"); dojo.require("dojo.parser"); </script> </head> <body class="tundra"> <div dojoType="dojo.data.ItemFileReadStore" jsId="dataStore" url="./programmingLanguages.json"></div> <div dojoType="dijit.tree.TreeStoreModel" jsId="model" store="dataStore" query="{name:'*'}"></div> <div dojoType="dijit.Tree" model="model"></div> </body> </html>
Many applications do not expressly represent a single root node, so let's adjust the previous example to work as a forest instead of a tree so that you can see the difference. First, a forest would have had a data source that didn't have a single root. Consider the following example, which lists programming languages as a forest because it does not include an explicit "programming languages" root:
{ identifier : 'name', label : 'name', items : [ { name : 'Object-Oriented', type : 'category', children: [ {name : 'JavaScript', type : 'language'}, {name : 'Java', type : 'language'}, {name : 'Ruby', type : 'language'} ] }, { name : 'Imperative', type : 'category', children: [ {name : 'C', type : 'language'}, {name : 'FORTRAN', type : 'language'}, {name : 'BASIC', type : 'language'} ] }, { name : 'Functional', type : 'category', children: [ {name : 'Lisp', type : 'language'}, {name : 'Erlang', type : 'language'}, {name : 'Scheme', type : 'language'} ] } ] }
With the updated JSON data, you see that there isn't a single
root node, so the data is delivered such that it lends itself to a
forest view. The only notable updates from Example 15-7 are that an additional
parameter, showRoot
, must be
added to the Tree
to expressly
hide the root of it, the query
needs to be updated to identify the top-level nodes for the tree,
and the TreeStoreModel
is changed
to a ForestStoreModel
. Example 15-8 shows the updated
code with the updates emphasized.
Example 15-8. Updates to show a forest instead of a tree
<body class="tundra"> <div dojoType="dojo.data.ItemFileReadStore" jsId="dataStore" url="./programmingLanguages.json"></div> <div dojoType="dijit.tree.ForestStoreModel
" jsId="model" store="dataStore" query="{type:'category'
}"></div> <div dojoType="dijit.Tree" model="model"showRoot=false
></div> </body>
Just because your data lends itself to being displayed as a
forest, however, doesn't mean you can't update it to be rendered as
a tree. As shown in Example 15-9, you can fabricate a
root-level dojo.data
item that
backs a fabricated node via the rootId
and rootLabel
attributes on the ForestStoreModel
.
Example 15-9. Updates to fabricate a root-level node so that a forest appears like a tree
<body class="tundra">
<div dojoType="dojo.data.ItemFileReadStore" jsId="dataStore"
url="./programmingLanguages.json"></div>
<div dojoType="dijit.tree.ForestStoreModel" jsId="model" store="dataStore"
query="{type:'category'}" rootId="root" rootLabel="Programming Languages"
></div>
<div dojoType="dijit.Tree" model="model" ></div>
</body>
For all practical purposes, the fabricated root node may now
be treated uniformly with a dojo.data
API such as getLabel
or getValue
. It may not seem like much, but
having this façade behind the fabricated node is very convenient
because you are freed from handling it as a special case. Figure 15-4 shows a simple forest.
Although displaying information in a tree is quite nice,
wouldn't it be even better to respond to events such as mouse
clicks? Let's implement the onClick
extension point to demonstrate the
feasibility of responding to clicks on different items. Both the
actual _TreeNode
that was clicked
as well as the dojo.data
item are
passed into onClick
and are
available for processing. To implement click handling, you might
update the example as shown in Example 15-10.
Example 15-10. Responding to clicks on a Tree
<body class="tundra"> <div dojoType="dojo.data.ItemFileReadStore" jsId="dataStore" url="./programmingLanguages.json"></div> <div dojoType="dijit.tree.ForestStoreModel" jsId="model" store="dataStore" query="{type:'category'}" rootId="root" rootLabel="Programming Languages"></div> <div dojoType="dijit.Tree" model="model" > <script type="dojo/method" event="onClick" args="item,treeNode"> //use the item or the node at will... console.log("onClick:",dataStore.getLabel(item)); //display the label </script> </div> </body>
Note that although an intervening model provides a layer of
abstraction between the Tree
and
the dojo.data
store, you still
use the store directly to access the item; there's no need to have
the intervening model that facilitates display
provide unnecessary cruft between the dojo.data
item and the usual means of
accessing it.
If you've followed along with the examples and have a solid
understanding of the dojo.data
APIs, then you know a lot more about the Tree
than you might think at this point.
Still, Table 15-10's more formal API listing makes
for a good reference and is helpful to skim over before we enter the
next section, which covers drag-and-drop for the Tree
. As you'll see, the Tree
itself really just has a few simple
attributes. Most of the heavy lifting is tucked away into the
dijit.tree.model
APIs or behind
the scenes entirely.
Tip
As of version 1.1, it is technically still possible to wire
up a Tree
directly to a
dojo.data
store; however,
because it is quite likely that this pattern may be removed in
version 2.0 and complicates the pattern for using a Tree
, it is not presented in this
chapter or included in the following API listing.
Table 15-10. Tree API
Name | Type | Comment |
---|---|---|
|
| Interface for uniformly accessing data. |
| Object | The data store query
that returns the top-level item(s) for the tree. If the
query returns exactly one item, use the |
| Boolean | Whether to display
the root of the |
| Array | A collection of
|
| Boolean | If set to |
| Boolean | Uses cookies to save
state of nodes being expanded or collapsed. |
| Function | An extension point
for handling a click (as well as an Enter key press) on an
item. Both the |
Next up is the dijit.Tree.model
API, shown in Table 15-11. Anything that presents
this interface is just a valid model as the TreeStoreModel
used in the previous
example. As would be the case with any other API, this means you can
essentially create whatever abstraction you need to populate a
Tree
as long as it meets the
spec—regardless of the underlying data source—whether it be a
dojo.data
API, some other open
API, or a completely proprietary API.
Table 15-11. dijit.Tree.TreeStoreModel API
Name | Comment |
---|---|
| Used for traversing
the |
| Used for traversing
the |
| Used for traversing
the |
| Used for inspecting
items. Returns the identity for an |
| Used for inspecting items. Returns the label for an item. |
| Part of the |
| Part of the |
| Callback used to
update a label or icon. Changes to an item's children or
parent(s) trigger |
| Callback used for responding to newly added, updated, or deleted items. |
| Destroys the object and releases connections to the store so that garbage collection can occur. |
On top of the TreeStoreModel
, the ForestStoreModel
(documented in Table 15-12) provides two
additional functions that respond to events related to the
fabricated root-level node; namely, adding and removing items from
the top level. These functions are needed to adjust the query
criteria so that the top level of the tree remains valid when
changes occur. As a data agnostic view, the Tree
itself has no responsibility for
updating or manipulating items; the burden is on the application
programmer to ensure that the query criteria remains satisfied.
Hence, the reason these additional functions exist is to enable that
to happen.
To update Example 15-9,
adjusting an item to meet the top-level query
criteria might be as simple as
adjusting its type
to be
"category" instead of "language". For example, you might move "Java"
to the top level, update its type to "category" and then provide an
operation for adding specific Java implementations (having a
type
of "language") as children.
As you'll see in the next section, the most common use case for
needing to meet these stipulations probably involves
drag-and-drop.
Table 15-12. dijit.tree.ForestStoreModel API additions
Name | Comment |
---|---|
| Called when an
|
| Called when an
|
The enhancements discussed in the previous section regarding
the dijit.tree.model
API were in
no small part implemented to make drag-and-drop operations with the
Tree
a lot simpler and more
consistent. In general, though, drag-and-drop is not a
one-size-fits-all type of operation, so expect to get your hands
dirty if you want a customized implementation of any sophisticated
widget that responds to drag-and-drop. It's especially important to
spend sufficient time answering these common questions:
What happens when a drag is initiated?
What happens when a drop is attempted?
What happens when a drop is cancelled?
The current architecture for implementing drag-and-drop with
the tree entails implementing much of the API as defined in the
dojo.dnd
module (introduced in
Chapter 7) and passing it into the Tree
via its dndController
attribute. Because starting
all of that work from scratch is a hard job, the version 1.1 release
includes a dijit._tree
module
that contains an implementation providing a lot of the boilerplate
that you can use as you see fit; you might use subclass and override
parts of it, you might mix stuff into it, or you might just use it
as set of guidelines that provide some inspiration for your own
from-scratch implementation. So long as the ultimate artifact from
the effort is a class that resembles a dojo.dnd.Source
and interacts
appropriately to update the dijit.tree.model
implementation that backs
the Tree
, you should be in good
shape. In particular, the Source
you implement should give special consideration to and implement at
least the following key methods that the Tree
's dndController
expects, listed in Table 15-13.
Table 15-13. Tree dndController interface
Name | Comment |
---|---|
| A topic event
processor for |
| A topic event
processor for |
| Used to check if the target can accept nodes from the source. This is often used to disallow dropping based on some properties of the nodes. |
| Used to check if the target can accept nodes from the source. This is often used to disallow dropping based on some properties of the target. |
| When completing a drop onto a destination that is backed by different a data source than the one where the drag started, a new item must be created for each element in nodes for the data source receiving the drop. This method provides the means of creating those items if the source and destination are backed by different data sources. |
Warning
A subtle point about the dndController
functions is that if they
are referenced in markup, they must be defined as global variables
when the parser parses the Tree
in the page; thus, they cannot be declared in the dojo.addOnLoad
block because it runs
after the parser finishes. You can, however, decide not to
reference the dndController
function at all in markup and defer wiring them up until the
dojo.addOnLoad
block. This is
the approach that the upcoming example takes.
An incredibly important realization to make is that
drag-and-drop involves DOM nodes—not _TreeNode
s; however, you'll usually need
a _TreeNode
because it's the
underlying data it provides that you're interested in, and the DOM
node does not provide that information. Whenever this need occurs,
such will be the case for any of the methods in Table 15-13. Use the dijit.getEnclosingWidget
function, which
converts the DOM node into a _TreeNode
for you.
Because these methods are so incredibly common, they may be
passed into the Tree
on
construction, which is especially nice because it allows you to
maximize the use of the boilerplate in dijit._tree
. Speaking of which, it's
about time for another example.
Let's update the existing working example from Example 15-9 to be
drag-and-droppable. We'll build upon the dijit._tree
boilerplate to minimize the
effort required. Also, note that we'll have to switch our store
from an ItemFileReadStore
to an
ItemFileWriteStore
as the very
nature of drag-and-drop is not a read-only operation.
Tip
Although it might look like the Tree
updates itself when you interact
with it in such as way that it changes display via a
drag-and-drop operation, it's important to remember that the
Tree
is only a view. Any
updates that occur are the result of updating the data source
and the data source triggering a view update.
To maintain a certain level of sanity with the example,
we'll need to prevent the user from dropping items on top of other
items, as items are inherently different from categories of items
based upon the category
of the
item from our dojo.data
store.
Example 15-11 shows the goods,
and Figure 15-5
illustrates.
Example 15-11. Simple drag-and-droppable Tree
<html> <head> <title>Drag and Droppable Tree Fun!</title> <link rel="stylesheet" type="text/css" href="http://o.aolcdn.com/dojo/1.1/dojo/resources/dojo.css" /> <link rel="stylesheet" type="text/css" href="http://o.aolcdn.com/dojo/1.1/dijit/themes/tundra/tundra.css" /> <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js" djConfig="parseOnLoad:true,isDebug:true"> </script> <script type="text/javascript"> dojo.require("dijit.Tree"); dojo.require("dojo.data.ItemFileWriteStore"); dojo.require("dijit._tree.dndSource"); dojo.require("dojo.parser"); dojo.addOnLoad(function( ) { //wire up the checkItemAcceptance handler... dijit.byId("tree").checkItemAcceptance = function(target, source) { //convert the target (DOM node) to a tree node and //then get the item out of it var item = dijit.getEnclosingWidget(target).item; //do not allow dropping onto the top (fabricated) level and //do not allow dropping onto items, only categories return (item.id != "root" && item.type == "category"); } }); </script> </head> <body class="tundra"> <div dojoType="dojo.data.ItemFileWriteStore" jsId="dataStore" url="./programmingLanguages.json"></div> <div dojoType="dijit.tree.ForestStoreModel" jsId="model" store="dataStore" query="{type:'category'}" rootId="root" rootLabel="Programming Languages" ></div> <div id="tree" dojoType="dijit.Tree" model="model" dndController="dijit._tree.dndSource"></div> </body> </html>
When you find that you need a drag-and-droppable Tree
implementation, it's well worth the
time to carefully study the boilerplate code provided in dijit._tree
. Each situation with
drag-and-drop is usually specialized, so finding an out-of-the-box
solution that requires virtually no custom implementation is
somewhat unlikely.
Get Dojo: The Definitive Guide now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.