Tree

[inline:tree.png]

The trees we see in User Interfaces help sort out long, heirarchical lists. A file system is the classic example, with Windows using it in Explorer and Macintoshes with its folder windows. The Dijit tree widget is like that. The Tree widget itself is simple, but the real power comes in the data you pass - this represents the heirarchical structure of the tree. This data is fed by the powerful dojo.data API.

Dojo makes easy trees easy, and hard trees possible. In particular, you can:

  • Build rooted or rootless trees (forests)
  • Nest trees to an arbitrary depth ... each branch is independently expandible
  • Apply different icons to different leaf or branch classes
  • Connect your tree to any dojo.data store implementing the Identity API.
  • Attach code to events. Events fire when users expand, contract or click particular nodes.
  • Programmatically build trees. Add, remove or disable nodes programatically.
  • Manipulate the Dojo.data store directly, which manipulates the tree indirectly
  • Allow nodes to be dragged and dropped through the familiar Dojo DnD API.

A Simple Tree

To start, here's a one level tree. We split the tree data off into a separate file, poptarts.txt, in JSON format. You will need to download this and save it in the same directory as the example HTML below:

{ label: 'name',
  identifier: 'name',
  items: [
     { name:'Fruit', type:'category'},
     { name:'Cinammon', type: 'category'},
     { name:'Chocolate', type: 'category'}
  ]
}

Then here's the HTML that draws the actual tree:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <style type="text/css">
        @import "http://o.aolcdn.com/dojo/1.0.0/dijit/themes/tundra/tundra.css";
        @import "http://o.aolcdn.com/dojo/1.0.0/dojo/resources/dojo.css";
    </style>
    <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.0.0/dojo/dojo.xd.js"
            djConfig="parseOnLoad: true">
</script>
    <script>
        dojo.require("dojo.data.ItemFileReadStore");
        dojo.require("dijit.Tree");
        dojo.require("dojo.parser");
   </script>
</head>
<body class="tundra">
        <div dojoType="dojo.data.ItemFileReadStore"
             url="poptarts.txt" jsid="popStore" />

        <div dojoType="dijit.Tree" store="popStore" labelAttr="name" label="Pop Tarts"></div>
</body>
</html>

This is a good start, but there's a lot more you can do with Tree. Read on!

Trees and Dojo.Data

To desconstruct the previous example, we need some dojo.data terminology:

  • an attribute is a named data set, much like a field or a column of a database,
  • A value is the data itself. So in "name: 'Fruit'", name is the attribute, Fruit is the value.
  • an item is one set of related attributes, much like a record or row. Unlike pure relational tables, items can have items nested within.
  • an identifieris an attribute that uniquely identifies the item, like a primary key

dijit.Tree, conforming to the dojo.data spec, expects the following in its data store:

  • The store attribute must be the name of a dojo.data data store.
  • Each item in the data store must have a label (as returned by getLabel())
  • An identifier attribute must be specified, even though it's not displayed.

So now let's take a step further. There are two methods to nesting a tree, corresponding to the two heirarchical methods in dojo.data.

Heirarchical Data

The easiest method for fixed text is to nest the items in the data store. To add a subtree, you must add a children attribute to the parent item, then add the child items to the children attribute. So for our previous example, we can add nodes under the Cinammon node:

Method 1: Direct Nesting

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}
{ label: 'name',
  identifier: 'name',
   items: [
     { name:'Fruit', type:'category'},
     { name:'Cinammon', type: 'category',
       children: [
          { name:'Cinammon Roll', type:'poptart' },
          { name:'Brown Sugar Cinnamon', type:'poptart' },
          { name:'French Toast', type:'poptart' }
       ]
     },
     { name:'Chocolate', type: 'category'}
  ]
}

By downloading this file into poptarts.txt, you can use the same HTML as our previous example. And voila!

Method 2: References

Direct Nesting is a little inconvenient for relational table data. So Tree supports references, where all the data nodes are at the same level, but nesting occurs with psuedo pointers to child nodes. So our example above is written: /* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}

{ label: 'name',
  identifier: 'name',
   items: [
     { name:'Fruit', type:'category'},
     { name:'Cinammon', type: 'category',
        children: [
           {_reference: 'Cinammon Roll'},
           {_reference:'Brown Sugar Cinnamon'},
           {_reference:'French Toast'}
        ]
     },
     { name:'Cinammon Roll', type:'poptart' },
     { name:'Brown Sugar Cinnamon', type:'poptart' },
     { name:'French Toast', type:'poptart' },
     { name:'Chocolate', type: 'category'}
    ]
}

Like our nested children example, the parent node requires a children attribute. But instead of actual items, you place reference objects linking to the identifier of another object. This requires a small change to the Tree tag:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<div dojoType="dijit.Tree" store="popStore2"
     labelAttr="name" label="Pop Tarts"
     query="{type:'category'}">
</div>

The query is necessary to choose only the top level items from the store. The menu produced is exactly the same:

Icon Classes

User Actions

The problem is our tree does nothing but stand around looking beautiful. Nothing wrong with that. Normally, though, you'd want some kind of actions.

Events

At the very least, you probably want to do something when a user clicks or press [ENTER] on a node. To do this, you can use the onClick event.

Pick a Pop Tart Please
/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <style type="text/css">
        @import "http://o.aolcdn.com/dojo/1.0.0/dijit/themes/tundra/tundra.css";
        @import "http://o.aolcdn.com/dojo/1.0.0/dojo/dojo.css"
    </style>
    <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.0.0/dojo/dojo.xd.js"
            djConfig="parseOnLoad: true">
</script>
    <script>
        dojo.require("dojo.data.ItemFileReadStore");
        dojo.require("dijit.Tree");
        dojo.require("dojo.parser");
   </script>
</head>
<body class="tundra">
        <div id="response">Pick a Pop Tart Please</div>
        <div dojoType="dojo.data.ItemFileReadStore"
             url="poptarts_direct.txt" jsid="popStore">
</div>
        <div dojoType="dijit.Tree" store="popStore" labelAttr="name"
             label="Pop Tarts">

             <script type="dojo/method" event="onClick" args="item">
                dojo.byId("response").innerHTML =
                   "You're a " + popStore.getLabel(item) + " fan, eh?";
            </script>
        </div>
</body>
</html>

Alternatively, you can use Dojo's publish/subscribe event system. When a node is clicked, the tree id is sent as the topic along with the message:

  • tree: the actual tree widget
  • event: "execute"
  • item: the dojo.data item selected
  • node: the DOM node selected

Drag and Drop

Dojo trees are great, and so is Dojo Drag And Drop (DnD). But together, they're unstoppable! Not being satisfied with our selection of Pop Tarts, we'll create a pool of Drag and Drop sources to add:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<ul dojoType="dojo.dnd.Source">
    <li class="dojoDndItem" id="Hot Chocolate">Hot Chocolate</li>
    <li class="dojoDndItem" id="Blueberry">Blueberry</li>
</ul>

The user should only be allowed to drop a Pop Tart on its home category - for example, Blueberry should only go under Fruit. To handle this, we use JavaScript regular expressions to make an intelligent guess. We add these to the data store:

{ label: 'name',
  identifier: 'name',
   items: [
     { name:'Fruit', type:'category', regexp:'.*erry$'},
     { name:'Cinammon', type: 'category', regexp:'[Cc]innamon',
       children: [
          { name:'Cinammon Roll', type:'poptart' }, 
          { name:'Brown Sugar Cinnamon', type:'poptart' }, 
          { name:'French Toast', type:'poptart' }
       ]
     },
     { name:'Chocolate', type: 'category', regexp:'([Cc]hocolate|Fudge)'}
  ]
}

You can download this file below. Next we wire in DnD to the Tree. This is as simple as specifying two attributes: the controller and the acceptance checker extension point, which we'll write shortly.

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<div dojoType="dijit.Tree" store="popStore" labelAttr="name"
     label="Pop Tarts" jsid="ptTree"
     dndController="dijit._tree.dndSource"
     checkItemAcceptance="poptartCheckItemAcceptance">

The checkItemAcceptance extension point function is called each time a drop target is entered. In Tree's case, every node is a drop target. How do we know it's valid? The function must check the dragged node to the regular expression of the drop node:

function poptartCheckItemAcceptance(node,source) {
     // Get the associated dojo.data item for the target
     item = dijit.getEnclosingWidget(node).item;

     // Need to check for item because when dropping on a root node,
     // item === null
     if (! item) return false;
            
     ptType = ptTree.store.getValue(item,"type"); 
            
     if (ptType == 'category') {

         // We make intelligent guesses about the correct folder
         re = new RegExp(ptTree.store.getValue(item,"regexp"));
         okToMove = true;
         for (var itemId in source.selection) {
              console.debug(itemId+" tested against "+re.toString());
              okToMove &= re.test(itemId);
         }
         return okToMove;
     }
     else 
         return false;
}

So now all the pieces are in place, yielding:

Drag a Pop Tart to Its Category
Hot Chocolate
Blueberry

And here's the full source code, which is downloadable below:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <style type="text/css">
        @import "http://o.aolcdn.com/dojo/1.0.0/dijit/themes/tundra/tundra.css";
        @import "http://o.aolcdn.com/dojo/1.0.0/dojo/dojo.css"
    </style>
    <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.0.0/dojo/dojo.xd.js"
            djConfig="parseOnLoad: true">
</script>
    <script>
        dojo.require("dojo.data.ItemFileWriteStore");
        dojo.require("dijit.Tree");
        dojo.require("dojo.parser");
        dojo.require("dojo.dnd.Source");
        dojo.require("dijit._tree.dndSource");
       
        function poptartCheckItemAcceptance(node,source) {
            // Get the associated dojo.data item for the target
            item = dijit.getEnclosingWidget(node).item;
            // Need to check for item because when dropping on a root node,
            // item === null
            if (! item) return false;
           
            ptType = ptTree.store.getValue(item,"type");
           
            if (ptType == 'category') {
                // We make intelligent guesses about the correct folder
                re = new RegExp(ptTree.store.getValue(item,"regexp"));
                okToMove = true;
                for (var itemId in source.selection) {
                   console.debug(itemId+" tested against "+re.toString());
                   okToMove &= re.test(itemId);
                }
               return okToMove;
            }
            else
                return false;
        }
   </script>
   
</head>
<body class="tundra">
        <div>Drag a Pop Tart to Its Category</div>
        <div dojoType="dojo.dnd.Source">
           <div class="dojoDndItem" id="Hot Chocolate">Hot Chocolate</div>
           <div class="dojoDndItem" id="Blueberry">Blueberry</div>
        </div>
        <div dojoType="dojo.data.ItemFileWriteStore"
             url="poptarts_dnd.txt" jsid="popStore" />

        <div dojoType="dijit.Tree" store="popStore" labelAttr="name"
             label="Pop Tarts" jsid="ptTree"
             dndController="dijit._tree.dndSource"
             checkItemAcceptance="poptartCheckItemAcceptance">

        </div>
</body>
</html>

Scripting Trees

In Actions we saw how to hook a piece of code into onClick. That's not all. With JavaScript and a Tree, you can style and manipulate trees in all sorts of combinations.

Adding an Icon or Class to a Node

OnClick is an extension point, which we'll cover in detail in Part 3. Three other extension points are used in drawing the tree nodes:

  • String getIconClass(/* dojo.data.Item */ item) takes in an item and returns a String specifiying a CSS class for the icon. The class should have a CSS style specifying a background url, pointing at the image. We saw this in Example 2 for styling mail icons.
  • String getLabelClass(/*dojo.data.Item*/ item) returns a CSS class applied to a label.
  • String getLabel(/*dojo.data.Item*/ item) returns an actual label. This is useful when the data source label is not sufficient for display - e.g. displaying "First Name Last Name" when the data source label is the SSN.

Here is an example of coloring our Pop Tarts labels. Notice the loose coupling here. If another category of Pop Tarts are introduced in the data source, you only need to add a corresponding class to poptarts.css: /* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000000; font-weight: bold;} .geshifilter .kw2 {color: #993333;} .geshifilter .co1 {color: #a1a100;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #933;} .geshifilter .re0 {color: #cc00cc;} .geshifilter .re1 {color: #6666ff;} .geshifilter .re2 {color: #3333ff;} .geshifilter .re3 {color: #933;} .geshifilter .re4 {color: #933;}

.Cinammon {
    color:red;
}
.Chocolate {
   color:brown;
}
.Fruit {
   color: blue;
}

And the getLabelClass is straightforward:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <style type="text/css">
        @import "http://o.aolcdn.com/dojo/1.0.0/dijit/themes/tundra/tundra.css";
        @import "http://o.aolcdn.com/dojo/1.0.0/dojo/dojo.css";
        @import "poptarts.css";
    </style>
    <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.0.0/dojo/dojo.xd.js"
            djConfig="parseOnLoad: true">
</script>
    <script>
        dojo.require("dojo.data.ItemFileReadStore");
        dojo.require("dijit.Tree");
        dojo.require("dojo.parser");
   </script>
</head>
<body class="tundra">
        <div dojoType="dojo.data.ItemFileReadStore"
             url="poptarts_direct.txt" jsid="popStore" />

        <div dojoType="dijit.Tree" store="popStore" labelAttr="name"
             label="Pop Tarts">

                <script type="dojo/method" event="getLabelClass" args="item">
                if (item != null && popStore.getValue(item, "type") == 'category') {
                    // For name=Chocolate, return class Chocolate etc.
                        return popStore.getValue(item, "name");
                }
            </script>
        </div>
</body>
</html>

Adding, Removing and Disabling Nodes

Tree Tag

dijit.Tree, dijit._TreeNode
dijit.Tree is a container for a hierarchical list with expandable and collapsible items. dijit._TreeNode's are the items themselves. _TreeNodes are almost never created with markup, and in general you don't deal with them.
Attributes
childrenAttr String name of attribute that holds children of a tree node consider this "root node" to be always expanded
label String New in 1.0 label for the top node of the tree, if desired. Note there is no actual item associated with this node.
query String get top level node(s) of tree (ex: {type:'continent'})
store dojo.data.Store The store to get data to display in the tree
Methods
isExpanded if expandible, returns true if children are displayed
isExpandible returns true if node can be expanded (has an expando icon next to it)
Extension Points
getIconClass user overridable class to return CSS class name to display icon
getItemChildren User overridable function that return array of child items of given parent item, or if parentItem==null then return top items in tree
getItemParentIdentity User overridable function, to return id of parent (or null if top level). It's called with args from dojo.store.onNew
getLabel user overridable function to get the label for a tree node (given the item)
mayHaveChildren New in 1.0 User overridable function to tell if an item has or may have children. Controls whether or not +/- expando icon is shown. (For efficiency reasons we may not want to check if an element has children until user clicks the expando node)
onClick(item, node) Called when someone clicks a tree item

Accessibility

Keyboard

ActionKey
Navigate to first tree item*Tab
Navigate to the next siblingDown arrow
Navigate to the previous siblingUp arrow
Open a subtreeRight arrow
Close a subtreeLeft arrow
Navigate to open subtreeRight arrow
Navigate to parentLeft arrow
Activate a tree itemEnter

* Note: The last tree item focused will be in the Tab order.