dijit.Tree - server fetch on node expansion
by Josh Trutwin

Here’s an example of a Tree that pulls data from the server on child node expansion (clicking the “+” next to a node that has children) - dojo 1.1 or better required:

My application for this tree is to list the fields I have in objects I call plugins in my CMS. These fields are similar but not exactly the same as database fields. I’m using a tree because plugins can relate to other plugins and I want to show these relationships in a top-down heirarchical fashion. Eventually users can select these fields to do many interesting things, but that’s for later. This example is for a plugin called a Company that has fields like name, city, state, zip, etc.

First - the JSON that’s returned for the company plugin (id = 5):

{
   label: 'name',
   identifier: 'id',
   items: [
      { id:'1', name:'company_id', type:'item' },
      { id:'2', name:'name', type:'item' },
      { id:'3', name:'phone', type:'item' },
      { id:'4', name:'address', type:'item' },
      { id:'5', name:'city', type:'item' },
      { id:'5', name:'state', type:'item' },
      { id:'6', name:'zip', type:'item' },
      { id:'7', name:'company_contacts', type:'item', plugin_id:'12', children: [
         { id:'7_1', name:'fieldid', type:'sub'}
         ]
      },
      { id:'8', name:'company_products', type:'item', plugin_id:'6', children: [
         { id:'8_1', name:'fieldid', type:'sub'}
         ]
      }
   ]
}

This basically lists the different fields for my Company plugin object in my CMS app. It starts with basic fields such as name, city, state, etc. At the bottom though there are two fields that associate/relate this plugin to other plugins, in this case a company is related to contacts and products.

The fact that the last two fields have a children attribute in the JSON will put the expando +/- node next to these items in the tree. The actual children values in the JSON do not matter, they will be replaced on clicking the expando node. The important part of the JSON for these last two is the piece called “plugin_id”. This tells me which plugin in my CMS these fields relates to (so I have a Contacts and Products plugin in the CMS as well with their own set of fields).

The JSON for plugin_id 6 (Products) looks like this:

{
   label: 'name',
   identifier: 'id',
   items: [
      { id:'1', name:'product_id', type:'item' },
      { id:'2', name:'name', type:'item' },
      { id:'3', name:'description', type:'item' },
      { id:'4', name:'product_company', type:'item', plugin_id:'5', children: [
         { id:'4_1', name:'fieldid', type:'sub'}
         ]
      },
      { id:'5', name:'product_keys', type:'item', plugin_id:'7', children: [
         { id:'5_1', name:'fieldid', type:'sub'}
         ]
      },
      { id:'6', name:'serial_number', type:'item' }
   ]
}

Same exact kind of data as before - I just want these items to appear under the “company_products” node when I expand. One note: I have a link BACK to the original company plugin via product_company field. This is precisely why I cannot build the full JSON for this tree as I would get in a situation where I have an infinite loop. I don’t care if the user wants to expand the products node, then that node’s company node, then that node’s products node, etc etc. I basically have a tree with an infinite possible number of child nodes.

Moving on - how to make this thing work. Let’s just show the code and I’ll explain.

<script type="text/javascript" language="Javascript">
	dojo.provide("my.ForestStoreModel");
	dojo.declare("my.ForestStoreModel", dijit.tree.ForestStoreModel, {
		getChildren: function(parentItem, complete_cb, error_cb) {
			if (parentItem.root == true) {
				// get top level nodes for this plugin id 
				pid = 5;
			}
			else {
				pid = this.store.getValue(parentItem, 'plugin_id');
			}
			this.store.fetch({ query: {plugin_id: pid}, 
							   onComplete: complete_cb, 
							   onError: error_cb});
 
			// Call superclasses' getChildren
			return this.inherited(arguments);
		}
	});
</script>
 
<div dojoType="dojox.data.QueryReadStore"
  	url="/get_plugin_fields.php" 
  	jsId="treeStore"></div>
 
 
<div dojoType="my.ForestStoreModel"
	store="treeStore"
	rootId="company"
	rootLabel="Company"
	query="{ id: '*' }"
	childrenAttrs="children"
	jsId="treeModel"></div>
 
 
<div dojoType="dijit.Tree" 
	  model="treeModel" 
	  jsId="itemTree" 
	  persist="false">
	<script type="dojo/method" event="onClick" args="item">
		alert('you clicked: ' + treeStore.getLabel(item));
	</script>
</div>

The first thing I’m doing is creating a customized ForestStoreModel which is provided by the Tree class. There is also a TreeStoreModel but my JSON essentially has no root data in the items array. ForestStoreModel is made for this situation where there is no apparent root or there are multiple root nodes. Bill has a nice description of these store models in this blog entry. Using ForestStoreModel means that I basically need to provide a fake root node and treat all the items in my JSON as children of this psuedo-root. Other than disliking this on principle it has no adverse affect.

Within my custom ForestStoreModel I override the getChildren method so I can define how my child data is fetched from the server. By checking if parentNode.root is null I can determine that this is the top-level fetch, in which case I use 5 as my hard-coded plugin id for the company plugin object. Otherwise this is an expansion of a child node and I pull the value of the plugin_id for the node being expanded. This plugin_id is grabbed from this piece of JSON:

      { id:'8', name:'company_products', type:'item', plugin_id:'6', children: [
         { id:'8_1', name:'fieldid', type:'sub'}
         ]
      }

So, if I click the node labeled “company_products” it will recognize that this node has children (because of the “children” in the JSON) which will call my custom getChildren method. This is not the root node so it will query this node’s value for plugin_id, which will be 6 for this item. Then it will call a new fetch from the server with plugin_id=6 on the query string. This will get me the JSON for this part of the tree which will magically be inserted under the node I expanded. In actuality, it will replace the current children I have from my JSON (the fieldid row above). Pretty cool.

The rest of the code is pretty straight-forward. I need to use a QueryReadStore as my backend as ItemFileReadStore will not re-fetch from the server unless I create a new store on each fetch. My URL doesn’t have a plugin_id param because getChildren is called to show the root items as well.

The definition for the tree store uses my custom forest model, the values for rootId, rootLabel are arbitrary, rootLabel will be the label for the top-level node. The rootId and rootLabel setup that psuedo-root node I referred to earlier.

The tree itself is pretty basic. I have a connect script that alerts the value of the node that was clicked for testing purposes.

Hope this helps.

Tags: , ,

This entry was posted on Saturday, June 7th, 2008 at 9:50 pm and is filed under Intermediate, Tutorials. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

12 Responses to “dijit.Tree - server fetch on node expansion”

  1. younjung Says:

    Thanks! This is what I have been looking for.

  2. kchiu Says:

    How to refresh a node in the tree?

  3. trutwijd Says:

    Re: kchiu

    Unfortunately, dijit.Tree has some built-in caching mechanisms that make “refreshing” nodes rather difficult. See comments here: http://dojocampus.org/content/2008/06/04/refreshing-a-data-store/

  4. younjung Says:

    Isn’t there any way to refresh a node so far? Or is that possible to use ItemFileReadStore instead of using QueryReadStore in this example?

  5. Nathan Says:

    Here’s a question: Why is this so difficult to do using Dojo??????

    Lazily loading data in a tree should be a straightforward task. It shouldn’t involve workarounds and hacks (e.g. including a bunk “children” array on the data). But what’s worst of all is that this functionality USED to be included in Dojo — I first used a lazy loading tree somewhere around the .4 Dojo version 2 years ago and it worked GREAT. That tree is still in a production system today; it even included an out-of-box little animated hourglass icon that would display while data was being dynamically loaded. The clients loved it.

    Then somewhere after that .4 Dojo version the tree was dropped from core functionality. One had to hack the old one to use it on a newer Dojo, or use some beta version which was barely functional. Then that beta (I think it was called the V3 tree?) version was forgotten about… and there was nothing for awhile. Then a somewhat busted tree came along, and finally the one we have today.

    We have chosen not to use Dojo for some new projects because of the sordid history of the tree widget (and used YUI’s instead, which is great). This is a widget that many modern systems require, and it should really be given more attention. Please stop making crazy video player and toaster widgets and focus on the important ones first.

  6. nonken Says:

    Hi Nathan,

    thanks for your post.

    I understand your frustration with some of the functionality of the Tree.
    The Dojo Toolkit is an effort by many people who work on it for the “love” of it. It is difficult sometimes to control what gets done first and then we get things like fancy widgets but not a well documented tree widget. This is not an excuse, but should give you some insight in why some things seem to take longer than others.

    If you have issues I can only suggest that you file a ticket, or even a patch which adds the functionality you require, or ask in the #dojo channel on irc.freenode.net

    The Toolkit is on a very good path, we are currently working hard on entire new documentation (the sandbox is here on docs.dojocampus.org) and hopefully can be released soon. Hopefully the issues you have with the Tree will be addressed by then as well.

  7. Nathan Says:

    nonken -

    I do understand the perils of an open source project. And I’m a big fan of Dojo, but this stupid tree thing is literally my biggest gripe against it.

    And not to rant too much, but one more obstacle to our mass adoption of the tree — one can’t use the XML datasource with it.

    What’s up with that??

  8. dante Says:

    Nathan - If I’m not mistaken, the reason one cannot use XML datasource with it is because the XmlDataStore doesn’t currently support the Identity API (?) — this is in progress and hopefully will be part of Dojo 1.3 — I’ve been wrong before though.

    Regards,
    Peter Higgins

  9. atalay Says:

    Thank you kardes saolasin

  10. bill Says:

    Hi Nathan,

    > Lazily loading data in a tree should be a straightforward task. It shouldn’t involve workarounds and hacks (e.g. including a bunk “children” array on the data).

    It doesn’t involve any workarounds/hacks/bunk children array. All you need is to define a model that implements getChildren() correctly.

    LIke dante said above, the XML data source should work with 1.3, or you could use the latest code in SVN.

  11. bill Says:

    Josh - BTW, I’m not sure that extending from ForestStoreModel is getting you much. It is possible to just write a model for the Tree from scratch, see for example the code in http://bugs.dojotoolkit.org/browser/demos/trunk/i18n/model.js (although that’s a rather complicated example).

  12. jnorris Says:

    Josh,

    This is exactly what I need, but I can’t get it to work. Firebug tells me that dijit.tree is undefined. Yet it I use dijit.tree.ForestStoreModel for my dijit.Tree, everything works as expected. Anyway, here’s what I have. Firebug complains about dijit.tree in the dojo.declare() line. I’m using Dojo 1.1.1.

    dojo.require(”dijit.Tree”);

    dojo.provide(”my.ForestStoreModel”);
    dojo.declare(”my.ForestStoreModel”, dijit.tree.ForestStoreModel, {
    getChildren: function(parentItem, complete_cb, error_cb) {
    var pid = “”;
    if (parentItem.root == true) {
    // get top level nodes (the sims).
    pid = “sim*”;
    } else {
    pid = this.store.getValue(parentItem, “id”);
    }
    this.store.fetch({ query: { id: “sim*” },
    onComplete: complete_cb,
    onError: error_cb });

    // Call superclasses’ getChildren
    return this.inherited(arguments);
    }
    });

Leave a Reply

You must be logged in to post a comment.