Lazy loading data in dojo tree

In dojo tree, the data to be shown will normally be fetched from a json file. Though the data can be in any format: an array, a txt file, or a http request. Dojo tree just needs a dojo's data store object to display the data.
Anyone who has worked in dojo for a while, will know how to create a store & use it in tree.
How to load data lazily, i.e., how to load data only when a node is expanded.

In this article, I will show an simple way of loading data when a node is expanded, and data will be loaded from ajax call to server. (Using DWR to get data from server).

A new widget "TreeCustom" be created, extending from default dojo tree, and make changes to expandNode function to allow an option of loading the child items lazily. This is done by checking the state of the node being expanded & if it's UNCHECKED, then call "lazyloadItems" method, which will be overriden by the user to add new child items to that node.

Here is the extended Tree source code:

dojo.provide("dijit.TreeCustom");

dojo.require("dijit.Tree");

dojo.declare("dijit.TreeCustom", dijit.Tree, {
 
 _expandNode: function (node) {
  if(node.state == "UNCHECKED") {
   this.lazyLoadItems(node , function() {
    node.unmarkProcessing();
   });
  }
  this.inherited(arguments); 
 },
  
 lazyLoadItems: function(node, callback_function) {
  // Default implementation. User overridable function.
  callback_function();
 }
});


That's it for custom tree widget code.
The real job of lazy loading items is done in the function which overrides lazyLoadItems method. As shown below.

1) Create a tree (Custom tree)

 var myTree_Data = {
    identifier: 'name',
    label: 'name',
    items: [
    {name: 'World', type:'world',
    children:[{_reference:'India'}, {_reference:'Australia'} , {_reference:'United States'} ]},
    {name: 'India', type:'country', isStub:'true'},
    {name: 'Australia', type:'country', isStub:'true'},
    {name: 'United States', type:'country', isStub:'true'}
    ] 
 };


 var myTree_Store = null;

 dojo.addOnLoad(function(){
  myTree_Store = new dojo.data.ItemFileWriteStore({data: myTree_Data});
  createTree();
 });

 function createTree () {
  var newModel  = new dijit.tree.TreeStoreModel({
   store: myTree_Store,
   query: {type:'world'},
   childrenAttrs: ["children"]
  });

  if(dijit.byId("myTree") != null) dijit.byId("myTree").destroy();
  myTree = new dijit.TreeCustom({
   id: "myTree",
   model: newModel,
   lazyLoadItems: loadCities
  });

  dojo.byId("treecontainer").appendChild(myTree.domNode);
 }
 function loadCities(node, callback_function) {
  // Load cities for this 'node'
 }

Notice isStub property in the data for country items. Only the nodes with isStub set to true requires the data be still loaded from server.
And the data is loaded only when user wants it, i.e., when user expands that particular node.

2) Code for loadCities method (lazy load data for this node):
 function loadCities(node, callback_function) {
  // Load cities for this 'node'
  var store = this.tree.model.store;
  
  var isStub = store.getValue(node.item, "isStub");
  var country = store.getValue(node.item, "name");
  if(isStub) {
   store.setValue(node.item, "isStub", false); // Make stub as false
   
   // make DWR call to server to the cities for this country.
   CitiesHandler.getCities(country, { callback:function(datafromServer) {
    dwrCallbackMethod(datafromServer, node, callback_function); }
   });
  }

 }
 
 // Assuming the DWR method from server returns list of city names.
 function dwrCallbackMethod(data, parentNode, callback_function) {
  if(data == null) {
   callback_function();
   return;
  }

  // add the cities to the parentNode.
  var parentItem = parentNode.item;
  var store = this.tree.model.store;

  for(i=0; i < data.cities.length; i++) {
   var cityInfo = {name: data.cities[i], type:'city'};
   var cityItem = store.newItem(relationshipNode, {parent: parentItem, attribute:"children"} );
  }
  callback_function();
 }

In loadCities method, isStub property of the node is checked if its true/false. If true, a request to server is made to get the data from server. DWR callback method gets the list of city names as array. All the items in the array is added to the store, which directly reflects in the tree view.

This example assumes that user is aware of configuring DWR in server side, and how to use dwr. If not, check directwebremoting.org to download DWR & also for instructions on setting up DWR.
Any suggestions/queries, i will be happy to help. :)

5 comments:

  1. Hi,
    I try to apply the example but it gives me this error :
    Window index : error loading root: TypeError: this._expandNode(rn) is undefined message=this._expandNode(rn) is undefined

    ReplyDelete
  2. Probably you are missing somethere. May be missing to include the required js file.

    Send me your code files, i can try to help.
    send it to harish.bn@gmail.com

    ReplyDelete
  3. I am getting the same error when the _expandNode kicks off for the root node.

    ReplyDelete
  4. I had to fix this example (using dojo 1.5)

    In TreeCustom:_expandNode() the inherited call has to be returned, since it will be called recursively and a deferred object is expected. So just write:

    return this.inherited(arguments);

    When adding a new item to the store, it has to be checked if the parent node is root. In this case no second parameter must be used.

    Oh ... and in the example "relationshipNode" should be cityInfo of course.

    Thanks for the helpful post!

    ReplyDelete
  5. Hi Andre,

    Thank you for the insight :)

    Yes. When i worked on this, the available version was dojo 1.4, so it does require a some changes to work with 1.5.


    Thanks
    Harish

    ReplyDelete