This tutorial is for Dojo 1.6 and may be out of date.
Up to date tutorials are available.
Connecting a Store to a Tree
The Dojo Tree component is a powerful tool for visual presentation of hierarchical data. In this tutorial we will look at how to connect the tree to a store for quick and efficient data drill-down into nested data.
Tree and Stores
The Dojo Tree component provides a comprehensive, familiar, intuitive drill-down presentation of hierarchical data. The Tree supports lazy loading of branches, making it highly scalable for large data sets. The Tree is a great widget to use when data has parent-child relationships.
Here we will learn how to use the new Dojo object store interface with the tree, to quickly build data-driven tree structures. In this tutorial, we will be using a data source that provides information on the US government structure and displaying the information in a Tree in order to easily descend into sections and collapse what we're not working with. We are going to start from scratch, create a simple object store, and end up with a data-driven Tree with lazy loading, drag-n-drop, and real-time response to data changes.
Start with a Store
We will begin by creating our data source. This will be the store that drives the Tree. Here we will use the JsonRest store. This store makes it easy to utilize lazy loading of data. In this example we will present the hierarchy of the US government. Here is the basic instantiation of the JsonRest store for connecting to our server so that data can be retrieved RESTfully:
dojo.require("dojo.store.JsonRest"); usGov = new dojo.store.JsonRest({ target:"data/" });
Add Basic Data Model Methods
We are going to use our store as the data model for the Tree. In order to do this, we also need to define the model logic that describes the hierarchy within our data. The Tree requires five model methods to render data as a tree:
- getIdentity(object) - Already provided by the store, and doesn't usually need to be reimplemented.
- mayHaveChildren(object) - Indicates whether or not an object may have children (prior to actually loading the children). In this example, we will treat the presence of a "children" property as the indication of having children.
- getChildren(parent, onComplete, onError) - Called to retrieve the children. This may execute asynchronously and should call the onComplete callback when finished. In this example, we will do a get() to retrieve the full representation of the parent object to get the children. Once the parent is fully loaded, we return the "children" array from the parent.
- getRoot(onItem) - Called to retrieve the root node. The onItem callback should be called with the root object. In this example, we get() the object with the id/URL of "root" for the root object.
- getLabel(object) - Returns the label for the object (this is the text that is displayed next to the node in the tree). In this example, the label is just the "name" property of the object.
Now, let's look at how to implement our definition of hierarchy with our data structure. We can most easily do this by defining the methods in the JsonRest instantiation:
usGov = new dojo.store.JsonRest({ target:"data/", mayHaveChildren: function(object){ // see if it has a children property return "children" in object; }, getChildren: function(object, onComplete, onError){ // retrieve the full copy of the object this.get(object.id).then(function(fullObject){ // copy to the original object so it has the children array as well. object.children = fullObject.children; // now that full object, we should have an array of children onComplete(fullObject.children); }, onError); }, getRoot: function(onItem, onError){ // get the root object, we will do a get() and callback the result this.get("root").then(onItem, onError); }, getLabel: function(object){ // just get the name return object.name; } });
Create Tree with our Store as Data Model
Now we can easily plug this store into our tree:
dojo.require("dijit.Tree"); tree = new dijit.Tree({ // create a tree model: usGov // give it the model }, "tree"); // target HTML element's id tree.startup();When the Tree starts up it will query our model/store for the root object. It will then ask the store for the label (via getLabel()) and get the children (via getChildren()). For each child it will render the label and add an expander icon if the object might have children (via mayHaveChildren()). Our getChildren() and getRoot() functions delegate to get() calls which trigger requests to the server (using the store's target + the get(id) argument as the URL for a GET). The server responds with JSON to these requests to satisfy the model and Tree. Here is how it looks:
Lazy Loading
To take advantage of lazy loading, when loading an object with its children, our server provides each child of the object, but only includes enough data in the children to render it. The requested object is a "full" representation of the object. However, for each child only the "name" property (for the label), the "id" property (to identify the object), and a boolean for the "children" property (indicating if it may have children) are included. These child objects are effectively "partial" representations. This approach to lazy loading ensures that only one request is needed each time a node is expanded (rather than a request for each child node of the expanded node). Here is what our server returns for the "root" object (GET data/root):
{ "name": "US Government", "id": "root", "children": [ { "name": "Congress", "id": "congress", "children": true }, { "name": "Executive", "id": "exec", "children": true }, { "name": "Judicial", "id": "judicial" } ] }
Then, when we click to expand a node, the Tree will request the target object's children. This is translated to a request for the parent object's full representation. If we click on the Executive node, the store will use the target object's id ("exec") and request the full "exec" object, triggering the request GET data/exec. The server then responds with:
{ "name": "Executive", "id": "exec", "children": [ { "name": "President", "id": "pres" }, { "name": "Vice President", "id": "vice-pres" }, { "name": "Secretary of State", "id": "state" }, { "name": "Cabinet", "id": "cabinet", "children": true } ] }
In this response, you can see that only the Cabinet object may have children.
User Modification of the Tree
The Tree widget has excellent support for drag-n-drop based modifications of the structural hierarchy of the tree. If we want to allow modifications to our data via drag-n-drop, we can implement the pasteItem() method and set the drag-n-drop controller for the tree. First, let's implement pasteItem(). This method is called when a drag-n-drop operation takes place. The pasteItem() method is called with several arguments:
- child - The child object that is being pasted.
- oldParent - The parent object where the child was dragged from.
- newParent - The new parent of the child object, where the child was dragged to.
- copy - Indicates if the child should be copied (instead of moved).
- insertIndex - The index of where the child should be placed in the lists of children for the new parent (if the store supports ordering of children).
The basic approach of implementing pasteItem() is straightforward. In our example, we simply want to remove the child object from the oldParent's children array and add the child object to the newParent's children array. We can do this by finding the index of the child in the oldParent's children array, use splice() to remove it, and then use splice() to place it in the newParent's children array at the correct index. We then call put() for each of these parent objects to save the modification. However, there are a couple of complications that we also need to consider. First, the parent objects may or may not be fully downloaded objects. With our lazy loading scheme, only full object's have the children array. Therefore, we will do a get() on each of the parent's to ensure we have the full object. Next, because there may be alternate copies of objects, we can't do a direct indexOf() call to find the child object in the children, so we need to scan the children to find an object with a matching id. With these considerations in mind, we can craft our pasteItem() implementation:
usGov = new dojo.store.JsonRest({ pasteItem: function(child, oldParent, newParent, bCopy, insertIndex){ // make the this store available in all the inner functions var store = this; // get the full oldParent object store.get(oldParent.id).then(function(oldParent){ // get the full newParent object store.get(newParent.id).then(function(newParent){ // get the oldParent's children and scan through it find the child object var oldChildren = oldParent.children; dojo.some(oldChildren, function(oldChild, i){ // it matches if the id's match if(oldChild.id == child.id){ // found the child, now remove it from the children array oldChildren.splice(i, 1); return true; // done, break out of the some() loop } }); // do a put to save the oldParent with the modified children's array store.put(oldParent); // now insert the child object into the new parent, //using the insertIndex if available newParent.children.splice(insertIndex || 0, 0, child); // save changes to the newParent store.put(newParent); }); }); }, ...
Configure Drag-n-Drop for Tree
We then need to define the drag-n-drop controller for the Tree as well. We will use the standard dijit.tree.dndSource
as the controller:
dojo.require("dijit.tree.dndSource"); tree = new dijit.Tree({ model: usGov, // define the drag-n-drop controller dndController: dijit.tree.dndSource }, "tree");
Now drag-n-drop operations should trigger our pasteItem() implementation and cause children arrays to be modified and saved. With the JsonRest store, the modifications that are saved via put() will trigger HTTP PUT requests to save the data back to the server.
Notifications
We aren't quite done yet. We need to also notify the Tree of the changes in the children. The Tree follows standard MVC principles of responding to data model changes rather than controller actions. This is extremely powerful because the view of the data can respond to changes regardless of what triggered the change (direct programmatic changes, drag-n-drop, etc.). The tree listens for the "onChildrenChange
" and "onChange
" events. The Store API dictates that all data changes happen via its put()
method. So we can extend put()
to call these model methods (triggering the Tree events), and then call the original put()
method to complete the action on the store:
usGov = new dojo.store.JsonRest({ put: function(object, options){ // fire the onChildrenChange event this.onChildrenChange(object, object.children); // fire the onChange event this.onChange(object); // execute the default action return dojo.store.JsonRest.prototype.put.apply(this, arguments); }, // we can also put event stubs so these methods can be // called before the listeners are applied onChildrenChange: function(parent, children){ // fired when the set of children for an object change }, onChange: function(object){ // fired when the properties of an object change }, ...
We now have defined our data model methods so our store can be used with the Tree for drag-n-drop. We can view the tutorial demo, but be aware that this demo does not implement any response to the HTTP PUT requests. The demo is just static files, so nothing is actually changed. If you do multiple drag-n-drop operations you will see objects reappear in old places due to the fact that the server is continually responding with the same static data.
Programmatic Data Changes
As we mentioned before, the Tree/model interface is designed so that the Tree responds to changes regardless of the trigger. Consequently to add a new child, we can simply insert a child object into a parent's children
array, save it with a put()
, and the Tree will automatically respond. In the demo, a button triggers the addition of a child object using the following code:
// get the selected object from the tree var selectedObject = tree.get("selectedItems")[0]; // get the full copy of the object usGov.get(selectedObject.id).then(function(selectedObject){ // add a new child selectedObject.children.push({ name: "New child", id: "new-child-id" }); // save it with a put(). The tree will automatically update the UI usGov.put(selectedObject); });
Of course, we could remove children with the same approach. We could also change properties of objects, such as the name (the label of the nodes). In the demo, we listen for double-clicks to prompt for a new name for objects:
dojo.connect(tree, "onDblClick", function(object){ // node was double clicked, prompt for a new name object.name = prompt("Enter a new name for the object"); // save the change, again the tree auto-updates usGov.put(object); });
Conclusion
The Tree is designed to properly separate the data model concerns from presentation, and the new object store can easily be extended with application hierarchical logic to drive the Tree. The Tree provides important features such as keyboard navigation and accessibility. Also, the Tree and object store combination leverages the additional powerful functionality of the Tree including scalable lazy loading, drag-n-drop, and real-time response to data model changes. We encourage you to explore the Tree documentation in more depth to learn more about the Tree capabilities such styling, icon customization, and it's API.