Tree

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 is dijit._TreeNode ; the leading underscore in this case signals that you'd never be using a _TreeNode outside of a Tree. 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 the dojo.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 the dojo.data model and the Tree was added. These intermediating layers are dijit.tree.TreeStoreModel and dijit.tree.ForestStoreModel, respectively. Much of the motivation for the change was to make the Tree 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.

Simple Tree

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.

The Tree that renders from the data store; clicking on the expando node closes it

Figure 15-3. The Tree that renders from the data store; clicking on the expando node closes it

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>

Simple Forest

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.

Left: the Tree (with a fabricated root node) that renders from the same data store; right: the Tree (without a root node) that displays as a forest

Figure 15-4. Left: the Tree (with a fabricated root node) that renders from the same data store; right: the Tree (without a root node) that displays as a forest

Responding to Click Events

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.

Tree-Related APIs

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

model

dijit.tree.model

Interface for uniformly accessing data.

query

Object

The data store query that returns the top-level item(s) for the tree. If the query returns exactly one item, use the TreeStoreModel as the intermediating layer; otherwise, use the ForestStoreModel.

showRoot

Boolean

Whether to display the root of the Tree ; typically used to hide the root for a ForestStoreModel.

childrenAttr

Array

A collection of String s that enumerate the attributes that hold children of a Tree. Default value is ["children"].

openOnClick

Boolean

If set to true, clicking on a node's label opens it (versus calling onClick, which handles opening it as well as other actions). false by default.

persist

Boolean

Uses cookies to save state of nodes being expanded or collapsed. true by default.

onClick(/*dojo.data.Item*/item, /*TreeNode*/node)

Function

An extension point for handling a click (as well as an Enter key press) on an item. Both the item and the node are passed in and are available for processing.

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

getRoot(/*Function*/onItem,

/*Function*/onError)

Used for traversing the Tree. Calls the onItem function with the root item for the tree, which may or may not be fabricated. Runs the onError function if an error occurs.

mayHaveChildren(/*dojo.data.Item*/item)

Used for traversing the Tree. Returns information about whether an item may have children, which is useful because it is not efficient to always check if an element actually has children before the expando is clicked.

getChildren(/*dojo.data.Item*/parentItem, /*Function*/onComplete)

Used for traversing the Tree. Calls the onComplete function with all of the child items for the parentItem.

getIdentity(/*dojo.data.Item*/item)

Used for inspecting items. Returns the identity for an item.

getLabel(/*dojo.data.Item*/item)

Used for inspecting items. Returns the label for an item.

newItem(/*Object?*/args,

/*dojo.data.Item?*/parent)

Part of the Write interface. Creates a new dojo.data item in accordance with dojo.data.api.Write.

pasteItem(/*dojo.data.Item*/childItem,

/*dojo.data.Item*/oldParentItem,

/*dojo.data.Item*/newParentItem,

/*Boolean*/copy)

Part of the Write interface. Moves or copies an item from one parent item to another, which is used in drag-and-drop operations. If oldParentItem is provided and copy is false, the child item is removed from oldParentItem ; if newParentItem is provided, the childItem is attached to it.

onChange(/*dojo.data.Item*/item)

Callback used to update a label or icon. Changes to an item's children or parent(s) trigger onChildrenChange, so those changes should probably be ignored here in onChange.

onChildrenChange(/*dojo.data.Item*/ parent,

/*Array*/ newChildren)

Callback used for responding to newly added, updated, or deleted items.

destroyRecursive()

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

onAddToRoot(/*dojo.data.Item*/item)

Called when an item is added to the top level of the tree; override to modify the item so that it matches the query for top-level tree items.

onLeaveRoot(/*dojo.data.Item*/item)

Called when an item is removed from the top level of the tree; override to modify the item so that it no longer matches the query for top-level tree items.

Drag-and-Drop with the Tree

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

onDndDrop(/*Object*/source, /*Array*/nodes, /*Boolean*/copy)

A topic event processor for /dnd/drop that is called to finish the drop operation, which entails updating the data store items according to source and destination of the operation so that three can update itself.

onDndCancel( )

A topic event processor for /dnd/cancel that handles a cancellation of a drop.

checkAcceptance(/*Object*/source, /*Array*/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 nodes.

checkItemAcceptance(/*DOMNode*/target, /*Object*/source)

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.

itemCreator(/*Array*/nodes)

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.

Drag-and-droppable Tree example

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>
Moving a programming language item to a different category

Figure 15-5. Moving a programming language item to a different category

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.