Introducing the Dojo Tree Widget

Update: I've added a new page with information about how to customize the menu.

Here I will present some example code for working with the dojo tree widget. I'll start with a basic example and then get into programatic creation and manipulation of the tree widget.

The examples presented here were created in Eclipse using the WTP. However, since dojo is really just a javascript framework, you could use any web development tools and server, PHP, ASP, Rails, whatever.

Contents

  1. Installing Dojo
  2. Trying out the Dojo Tree Widget
  3. Adding the Context Menu
  4. Make the Context Menu Work
  5. Build the Menu Programmatically
  6. Add Drag and Drop Capability
  7. Get the Code
  8. Further Reading and Resources

Installing Dojo

dojo setup

In the WebContent (your web root) folder, create a new folder, and name it scripts. Download and unzip a kitchen sink version of the Dojo toolkit. Copy the src directory from Dojo into the scripts directory. Next, copy the dojo.js file into the scripts directory. The accompanying image shows what the directory structure should look like afterwards.

Trying out the Dojo Tree Widget

Now that the installation is finished, let's get a feel for the tree widget. We will create a page with some sample data just to see the widget in action. After that, we will create a context menu for the widget and finally, add drag and drop functionality. Once finished with the sample code, we will create an empty widget and bind it to the repository.

In the WebContent folder create a file treeTest.html. At the top of the page, we will add some script elements to set up the Dojo toolkit for use. The first, will set the dojo configuration to the debug state.

  1. <script type="text/javascript">
  2.      var djConfig = { isDebug: true };
  3. </script>

Next, we will include the Dojo engine with a javascript include.

  1. <script type="text/javascript" src="scripts/dojo.js">
  2.     /* Load Dojo engine */
  3. </script>

Finally, we will declare the required elements to make the tree widget function using the dojo.require() method calls; one for the dojo language elements, and the other for the tree widget.

  1. <script type="text/javascript">
  2.     dojo.require("dojo.lang.*");
  3.     dojo.require("dojo.widget.Tree");
  4. </script>

Now, it's time to build a tree. I am going to veer away from the common Dojo way of building it and use the alternative method. The reason for this is that the standard method involves the use of custom attributes to the html elements. This prevents pages from passing validation. There are good arguments for both methods. My reason is to simply try to keep pages on the w3c standard, as the requirement is occurring at more and more organizations. For more information about the Dojo Toolkit, read the manual. The following demo code builds a little tree based on some categories, ripped off, from the CNN website.

  1. <div class="dojo-Tree">
  2.   <div class="dojo-TreeNode" title="World"></div>
  3.   <div class="dojo-TreeNode" title="Business">
  4.     <div class="dojo-TreeNode" title="News">
  5.       <div class="dojo-TreeNode" title="Main"></div>
  6.       <div class="dojo-TreeNode" title="Company News"></div>
  7.       <div class="dojo-TreeNode" title="Economy"></div>
  8.     </div>
  9.     <div class="dojo-TreeNode" title="Markets"></div>
  10.     <div class="dojo-TreeNode" title="Technology"></div>
  11.     <div class="dojo-TreeNode" title="Jobs and Economy"></div>
  12.   </div>
  13.   <div class="dojo-TreeNode" title="Sports"></div>
  14. </div>

Adding the Context Menu

Now that we've created a little demo of the tree widget, let's do something with it. For a menu editor, I think a context menu to create and delete entries would be useful. Then, let's add the ability to drag and drop nodes around. Finally, let's have each element connect to a few text boxes to edit the details of a menu entry.

First up, the context menu. Add the following code to the page.

  1. <dl class="dojo-TreeContextMenu" id="treeContextMenu">
  2.    <dt class="dojo-TreeMenuItem" id="ctxMenuAdd" caption="Add Child" >
  3.    <dt class="dojo-TreeMenuItem" id="ctxMenuDel" caption="Remove Item">
  4. </dl>

That defines the context menu. The caption attributes define the text to be displayed. Next, it's time to connect the context menu to the tree widget. To do this, just add an attribute to the div tag that defines the tree widget.

The attribute should be named menu and the value should be the same as the id of the context menu, treeContextMenu. <div class="dojo-Tree" menu="treeContextMenu">

And up where the dojo.require() elements are, add the following.

dojo.require("dojo.widget.TreeContextMenu");

Now, if you right click on an item the context menu will show up (like in the demo above). It should also have text entries that match the captions in the tags. Now, you must be thinking, I just had to read a bunch of stuff about XHTML and he's already breaking the rule. Ah, I knew you would catch me. Well, I just wanted to get it up as quickly as possible and show that there is more than one way o do something. So now, delete the code just added. Now add the following javascript to create the context menu programmatically.

  1. <script type="text/javascript">
  2. function addTreeContextMenu(){
  3.   var djWdgt = dojo.widget;
  4.   var ctxMenu = djWdgt.createWidget("TreeContextMenu",{});
  5.   ctxMenu.addChild(djWdgt.createWidget(
  6.     "TreeMenuItem",{caption:"Add Child Menu Item"}));
  7.   ctxMenu.addChild(djWdgt.createWidget(
  8.     "TreeMenuItem",{caption:"Delete This Menu Item"}));
  9.   document.body.appendChild(ctxMenu.domNode);
  10.   var myTree = dojo.widget.manager.getWidgetById("myTreeWidget");
  11.   /* Bind the context menu to the tree */
  12.   ctxMenu.listenTree(myTree);
  13. }
  14.  
  15. dojo.addOnLoad( function(){
  16.   addTreeContextMenu();
  17.   });
  18. </script>

So, what does this do? Let's start at the bottom. On lines 39-41, we tell dojo what to do on startup. The addOnLoad function takes a function as an argument. Here we create an anonymous inline function that calls the function declared on line 25. We are doing this (instead of passing a direct reference) because we plan to add a little more, later.

Moving back up, line 26 just creates a short reference variable to reduce typing later. On line 27, the dojo.widget object is invoked to create the TreeContextMenu widget. This is analogous to the html version before when the class, dojo-TreeContextMenu, was used. The second argument is used to pass parameters used to create the object. It is left blank for the moment.

Lines 28 and 29 create an item to show in the context menu. The caption parameter is for the text to display in the context menu. The item is then added as a child node of the context menu. On line 32, the context menu is added to the web page. Line 33, retrieves a reference to the tree widget and line 35 binds the context menu to the tree.

Make the Context Menu Work

Now that we have a context menu, let's make it actually add and remove items. To do this we've got a little work to do. We'll need to add ids to the context menu elements, create a controller for the tree widget, create the functions to add and delete, and finally, subscribe to the events triggered by the items of the context menu. To begin with, I'm going to refactor the code a little also so that we can control the name space of the functions and eliminate redundancy.

  1. <script type="text/javascript">
  2. var DemoTreeManager = {
  3.   djWdgt: null,
  4.   myTreeWidget: null,
  5.   addTreeContextMenu: function(){
  6.     var ctxMenu = this.djWdgt.createWidget("TreeContextMenu",{});
  7.     ctxMenu.addChild(this.djWdgt.createWidget(
  8.       "TreeMenuItem",{caption:"Add Child Menu Item",
  9.         widgetId:"ctxAdd"}));
  10.     ctxMenu.addChild(this.djWdgt.createWidget(
  11.       "TreeMenuItem",{caption:"Delete this Menu Item",
  12.         widgetId:"ctxDelete"`}));
  13.     document.body.appendChild(ctxMenu.domNode);
  14.     /* Bind the context menu to the tree */
  15.     ctxMenu.listenTree(this.myTreeWidget);
  16.   },
  17.   addController: function(){
  18.     this.djWdgt.createWidget(
  19.       "TreeBasicController",
  20.       {widgetId:"myTreeController",DNDController:"create"}
  21.     );
  22.   },
  23.   bindEvents: function(){
  24.     /* Bind the functions in the TreeActions object to the
  25.        context menu entries */
  26.     dojo.event.topic.subscribe("ctxAdd/engage",
  27.       function (menuItem) { TreeActions.addNewNode(menuItem.getTreeNode(),
  28.         "myTreeController"); }
  29.     );
  30.     dojo.event.topic.subscribe("ctxDelete/engage",
  31.       function (menuItem) { TreeActions.removeNode(menuItem.getTreeNode(),
  32.         "myTreeController"); }
  33.     );
  34.   },
  35.   init: function(){
  36.     /* Initialize this object */
  37.     this.djWdgt = dojo.widget;
  38.     this.myTreeWidget = this.djWdgt.manager.
  39.       getWidgetById("myTreeWidget");
  40.     this.addTreeContextMenu();
  41.     this.addController();
  42.     this.bindEvents();
  43.   }
  44. };
  45.  
  46. var TreeActions = {
  47.   addNewNode: function(parent,controllerId){
  48.     this.controller = dojo.widget.manager.getWidgetById(controllerId);
  49.     if (!parent.isFolder) {
  50.       parent.setFolder();
  51.     }
  52.     var res = this.controller.createChild(parent, 0, { title: "New node" });
  53.   },
  54.   removeNode: function(node,controllerId){
  55.     if (!node) {
  56.       alert("Nothing selected to delete",);
  57.       return false;
  58.     }
  59.     this.controller = dojo.widget.manager.getWidgetById(controllerId);
  60.     var res = this.controller.removeNode(node, dojo.lang.hitch(this));
  61.   }
  62. };
  63.  
  64. dojo.addOnLoad(function(){
  65.   DemoTreeManager.init()
  66.   });
  67.  
  68. </script>

The code explained

Ok, I admit it. That was a lot of code to throw up all at once. So let's attack it piece by piece. The first thing done is to create an object DemoTreeManager that is used to contain the functions for setting up the widget.

  1. var DemoTreeManager = {

Som properties (lines 25 and 26) are defined for convenience djWdgt and myTreeWidget. These will be assigned values in the init function (lines 59-61).

  1.     this.djWdgt = dojo.widget;
  2.     this.myTreeWidget = this.djWdgt.manager.
  3.       getWidgetById("myTreeWidget");

Next, we add a function addTreeContextMenu to the object (lines 27-38). This should look pretty familiar. It's the same function from before. Only now, it is defined as part of the object DemoTreeManager instead of as a stand alone function. To call it use would use the syntax DemoTreeManager.addTreeContextMenu(). The only major difference you should see now is that the context menu items now also have widgetId's assigned to them (lines 31 and 34). These will be used later when we subscribe to the menu's events.

  1.     ctxMenu.addChild(this.djWdgt.createWidget(
  2.       "TreeMenuItem",{caption:"Add Child Menu Item",
  3.         widgetId:"ctxAdd"}));
  4.     ctxMenu.addChild(this.djWdgt.createWidget(
  5.       "TreeMenuItem",{caption:"Delete this Menu Item",
  6.         widgetId:"ctxDelete"`}));

On line 39, we create a function, addController, to add a controller to manage the tree widget. The controller will be required to add and remove nodes from the tree.

  1.   addController: function(){
  2.     this.djWdgt.createWidget(
  3.       "TreeBasicController",
  4.       {widgetId:"myTreeController",DNDController:"create"}
  5.     );
  6.   },

Next, a function, bindEvents, is created (line 45-56) to subscribe to the action of a user selecting an entry in the context menu. The event name ctxAdd/engage basically means that the item with the id ctxAdd is engaged the function will be called. This is the same as the id we gave the menu item on line 31. The subscription for the delete action works the same way. The functions called TreeActions.addNewNode and TreeActions.removeNode will be covered in a moment.

  1.   bindEvents: function(){
  2.     /* Bind the functions in the TreeActions object to the
  3.        context menu entries */
  4.     dojo.event.topic.subscribe("ctxAdd/engage",
  5.       function (menuItem) { TreeActions.addNewNode(menuItem.getTreeNode(),
  6.         "myTreeController"); }
  7.     );
  8.     dojo.event.topic.subscribe("ctxDelete/engage",
  9.       function (menuItem) { TreeActions.removeNode(menuItem.getTreeNode(),
  10.         "myTreeController"); }
  11.     );
  12.   },

Finally, or initially ;-), the init function (lines 57-65). It just sets up the properties, described before, and calls the functions in order to set up the menu and controller for the tree.

Next up, we create the TreeActions object (line 68). This object just contains the two functions to add and remove nodes from the tree. The function addnewNode (remember the call in the event binding above?) checks to see if the node selected is a folder type. If not, it is made into one. Next, the controller is used to create a new child node for the node selected when the context menu was pulled up. We give the new node a default caption "New node", just so something is displayed.

  1. var TreeActions = {
  2.   addNewNode: function(parent,controllerId){
  3.     this.controller = dojo.widget.manager.getWidgetById(controllerId);
  4.     if (!parent.isFolder) {
  5.       parent.setFolder();
  6.     }
  7.     var res = this.controller.createChild(parent, 0, { title: "New node" });
  8.   },

The removeNode function, ensures that a node has been selected and then uses the controller to remove it. Pretty straightforward.

  1.   removeNode: function(node,controllerId){
  2.     if (!node) {
  3.       alert("Nothing selected to delete",);
  4.       return false;
  5.     }
  6.     this.controller = dojo.widget.manager.getWidgetById(controllerId);
  7.     var res = this.controller.removeNode(node, dojo.lang.hitch(this));
  8.   }

Next up, we'll add bindings for menu entries and make a little form to edit them with.

Build the Menu Programmatically

Ok, that was fun. Now since most uses of a tree widget will be in some kind of AJAX application, lets go over building the tree programmatically. To do this we'll create a new HTML file, progTree.html.

For this, lets start with an onject definition for the data in the tree. This will be in the same format as a JSON string, only, not in one line in quotes. This format is easy to generate using a JSON library (available for just about every server type), and is easy to generate even without a library. It is also easy to parse through so we can focus on the dojo tree widget, its why you're here after all. I am going to use the same data as in the HTML example.

First off, go ahead and add the script elements to set the debug mode, get the dojo.js file and to define the required dojo libraries. Then add the following scipt for the tree data.

  1. <script type="text/javascript">
  2. var treeDat = {
  3.   treeNodes: [
  4.     { title:"World" },
  5.     { title:"Business",
  6.       children:[
  7.         { title:"News",
  8.           children:[
  9.             { title:"Main"},
  10.             { title:"Company News" },
  11.             { title:"Economy" }
  12.           ]
  13.         },
  14.         { title:"Markets" },
  15.         { title:"Technology" },
  16.         { title:"Jobs and Economy" }
  17.       ]
  18.     },
  19.     { title:"Sports" }
  20.   ]
  21. };
  22. </script>

Next, lets create a little HTML to act a placeholder for where the tree widget will be added. I like the idea of a little 'loading' message, just so a user knows that something is happening.

  1. <div id="myWidgetContainer"
  2.    style="width: 17em; border: solid #888 1px;">
  3.   <span id="treePlaceHolder"
  4.      style="background-color:#F00; color:#FFF;">
  5.     Loading tree widget...
  6.   </span>
  7. </div>

That was easy, now lets add a little bit of javascript to parse through the treeDat object, build our tree, and replace the placeholder with the tree in the page.

  1. <script type="text/javascript">
  2. var TreeBuilder = {
  3.   buildTreeNodes:function (dataObjs, treeParentNode){
  4.     for(var i=0; i<dataObjs.length;i++){
  5.       var node = dojo.widget.createWidget("TreeNode",{
  6.         title:dataObjs[i].title
  7.       });
  8.       treeParentNode.addChild(node);
  9.       treeParentNode.registerChild(node,i);
  10.       if(dataObjs[i].children){
  11.         this.buildTreeNodes(dataObjs[i].children, node);
  12.       }
  13.     }
  14.   },
  15.   buildTree:function (){
  16.     myTreeWidget = dojo.widget.createWidget("Tree",{
  17.       widgetId:"myNewTreeWidget"
  18.     });
  19.     this.buildTreeNodes(treeDat.treeNodes,myTreeWidget);
  20.     var treeContainer = document.getElementById("myWidgetContainer");
  21.     var placeHolder = document.getElementById("treePlaceHolder");
  22.     treeContainer.replaceChild(myTreeWidget.domNode,placeHolder);
  23.   }
  24. }
  25. dojo.addOnLoad(function(){
  26.   TreeBuilder.buildTree()
  27.   });
  28. </script>

Lets briefly go over that. Lines 71-73 should look familiar, it is just telling dojo to call the function to build the tree after it loads.

  1. dojo.addOnLoad(function(){
  2.   TreeBuilder.buildTree()
  3.   });

The buildTree function (lines 61-69) just creates a tree widget object, collects the TreeNodes (using the treeDat data object), and swaps the Tree widget with the placeholder.

  1.   buildTree:function (){
  2.     myTreeWidget = dojo.widget.createWidget("Tree",{
  3.       widgetId:"myNewTreeWidget"
  4.     });
  5.     this.buildTreeNodes(treeDat.treeNodes,myTreeWidget);
  6.     var treeContainer = document.getElementById("myWidgetContainer");
  7.     var placeHolder = document.getElementById("treePlaceHolder");
  8.     treeContainer.replaceChild(myTreeWidget.domNode,placeHolder);
  9.   }

The buildTreeNodes function is a recursive function used to translate the data objects into TreeNode objects.

If you start up the page, you will see the same menu as the HTML one we wrote before. You can also just copy over the context menu code from before to this page (or just pull it into its own javascript file).

Add Drag and Drop Capability

Now, lets add the ability to drag and drop nodes around the tree. First off, lets add some more properties to the tree definition code (lines 62-64).

  1.     myTreeWidget = dojo.widget.createWidget("Tree",{
  2.       widgetId:"myNewTreeWidget",
  3.       DNDMode:"both",
  4.       DNDAcceptTypes:["myNewTreeWidget"]
  5.     });

Now, if you pull up the page in a browser, you should be able to drag and drop the nodes of the table around. After a little bit of playing, you'll notice that the little icons and lines for the tree get out of whack. Lets fix that.

Fixing Drag and Drop

This one is a pain, I'm off to work on other things. In the meantime, I've got a post on the mailing list to recruit some help. I'll come back to this once I finish my other project. sorry

Follow-up

Further Reading and Resources

The following links provide a little more information for working with Dojo and the Tree widget.

Although the following books do not cover the Dojo Toolkit, they do have some good information for working with javascript and DHTML. I highly recommend Dynamic HTML by Danny Goodman. Its funny that it took forever for the 2nd edition to come out and then, the 3rd edition came very quickly afterwards. If you get it, be sure to get the latest.

If you know of some web sites with some informative information about the Dojo tree widget, please send a link to my email: feedback@willcode4beer.com, thanks.


Feel free to send comments to: feedback@willcode4beer.com or leave a reply on my blog. thanks


Sponsors:

About willCode4Beer