Login Register

dijit.Tree and dojo.data in Dojo 1.1

This post is out of date... see the new post instead.

In version 1.1 I've fixed the Tree architecture to be able to handle DnD correctly. I've had to deprecate (not remove) some version 1.0 APIs because they didn't work with DnD, and I've listed them here, along with some general discussion about what it means for a tree to connect to a data store. Hopefully some will find it useful.

Tree/data store connection

Dijit.Tree's connection to dojo.data gives the tree a lot of power, but it's also complicated the API to something more than if the tree simply displayed a read-only collapsible list, and/or had it's own custom data format. That's why various people have asked for a more direct interface to control the nodes in a tree, but at least for now I've decided not to do that, in order to minimize the surface area of our API.

The dijit.Tree widget is a view on a dojo.data store. Tree's job is to display the store data in a hierarchy. This is more complicated then it sounds because a store doesn't represent a tree per-se, but rather it just contains a bunch of items, some of which contain references to other items. For example, an item might have multiple children attributes, each of which points to other items. Multiple parent items could point to the same child item, or there could even be loops.

Consider this store:

store.png

If we represented that data in ItemFileWriteStore, it might look like the JSON below, although it's important to realize that Tree can interface to any store, so the internals of the store are irrelevant.

{
        identifier: 'id',
        label: 'name',
        items: [
                { id: '0', name: 'Fruits', top: true, children: [ {_reference: 1}, {_reference: 4} ] },
                { id: '1', name: 'Citrus', items: [ {_reference: 2}, {_reference: 3} ] },
                { id: '2', name: 'Orange'},
                { id: '3', name: 'Lemon'},
                { id: '4', name: 'Tomato'},
                { id: '5', name: 'Vegetables', top: true, children: [ {_reference: 4} ] },
                { id: '6', name: 'Lettuce'},
        ]
}

We'd like the dijit.Tree widget to display those items above in a hierarchy, like this:

  • Fruits
    • Citrus
      • Orange
      • Lemon
    • Tomato
  • Vegetables
    • Tomato

A few things to note:

  • this is technically a forest, not a tree, since there are multiple top level nodes.
  • there's only one 'Tomato' item, but it's referenced as both a Fruit and a Vegetable.

Tree doesn't support multiple-parent items yet but will in the future. The point is to note how a data store represents very general data which we are mapping into a Tree.

Dojo 1.0 Tree API

In the dojo 1.0 release, dijit.Tree featured an API where you specify a query to get the top-level item or items, and then an attribute to query to get children of those top level items. For this data you would want to display all the elements marked as top:true at the top level, and then get children via the "children" attribute.

<div dojoType="dijit.Tree" store="catStore" query="{top: true}"
		labelAttr="name" childrenAttr="children"></div>

Tree also provided an option to create a root level node to tie 'Fruits' and 'Vegetables' together without any such item existing in the data store, by specifying a label to the Tree:

<div dojoType="dijit.Tree" store="catStore" query="{top: true}" label="Food"
		labelAttr="name" childrenAttr="children"></div>
which displays a tree like:
  • Food
    • Fruits
      • Citrus
        • Orange
        • Lemon
      • Tomato
    • Vegetables
      • Tomato

This is a convenient feature, although it causes some confusion since there's no data store item associated with the top level node in the tree, and thus clicking it will produce an onClick(null) rather than onClick(item). Of course you could add a "Food" item to your data store but it may not be convenient to change your data model just to affect the view (the Tree).

The problem with DnD and data store notification

Given that a dijit.Tree is just a view onto a dojo.data store, any updates to the tree (ie, moving a node via DnD) have to be reflected to the store, and conversly any updates to the store need to be reflected in the tree.

sync.png

As a matter of fact, dragging-and-dropping (ie, moving) an item in the Tree doesn't update the tree directly, but rather just updates the data store, and then Tree responds to that update just like any update to the store (from an external source). So, there are two issues involved with a tree:

  1. notifying the data store about updates
  2. responding to changes in the store

If you move an item (for example, deciding that a lettuce is a fruit rather than a vegetable), the DnD code tells the data store that Vegetable's children attribute changed from [Tomato, Lettuce] to just [Tomato], and that 'Fruits' children attribute changed from [Citrus, Apple, Banana] to [Citrus, Apple, Banana, Lettuce] (or perhaps a different order). The data store then notifies tree of the updates to 'Fruits' and 'Vegetables' children attributes, and tree updates it's display.

If, however, you decided that a 'Tomato' was neither a 'Fruit' nor a 'Vegetable', but rather just a type of food, you would want to somehow make it a top level item in the tree so that myStore.fetch({ query: {top: true} }) returns 'Tomato' in it's query. We have two issues here, namely that:

  1. dijit.Tree doesn't know how to mark the element as "top level". In this case, it's simply setting the top attribute to true, but there may be more complicated cases.
  2. The data store notifies the Tree that an item has changed (namely that the item's "top" attribute has been set), but there's no way for Tree to know whether that item now matches the "top level items" query specified to Tree without rerunning the query. And even if it new an item was top level, it wouldn't know the order of the new item relative to the other items. This is because Tree can connect to any data store and thus the query format is a black box.

ForestDecorator

In short, all of these problems are caused by allowing a query which lists a set of top level children, and whose results can change over time. If we assume that there's a single root item, specified when the Tree is constructed, everything is simplified. All changes to/from the tree are specified as changes to some item's children attribute.

However, in order to support data stores which don't match this pattern, I've created dijit._tree.ForestStoreDecorator, which wraps another store and just adds a fake root item.

var baseStore = new dojo.data.ItemFileWriteStore({
                url: ...});
        var decoratedStore = new dijit._tree.ForestStoreDecorator({
                store: baseStore,
                query: {top:true},
                rootId: "food",
                rootLabel: "Foods",
                childrenAttr: "children"
        });

The decorated store just has a root item, but is otherwise the same:

decorated.png

Then you simply create the tree off of the decorated store:

var tree  new dijit.Tree({
                store: decoratedStore,
                labelAttr: "name",
                childrenAttr: "children"
        });

It also has a number of hooks to handle changes to elements when they are added/removed from the root node, such as setting top: true. But the important thing is that it presents a consistent interface to dijit.Tree such that every time an item is moved, it's reflected by changes to two other items' children attributes.

Note that the top level item doesn't actually have to be displayed. The showRoot parameter controls whether the tree shows up as a true tree, like:

  • Food
    • Fruits
      • Citrus
        • Orange
        • Lemon
      • Tomato
    • Vegetables
      • Tomato

Or just as a forest:

  • Fruits
    • Citrus
      • Orange
      • Lemon
    • Tomato
  • Vegetables
    • Tomato

So, in summary, dijit.Tree still takes a query parameter, but that query should only return one item (the root item). And if (as for the case of ForestStoreDecorator) store.fetch() only returns a single item, then no query is needed.

Other deprecated APIs

getItemChildren() was a convenient abstraction for non-standard ways of mapping parents items to child items, but it doesn't handle updates to the data store. The user can write decorator store like dijit._tree.ForestStoreDecorator.

getItemParentIdentity() falls into the same category.

Nested ItemFileReadStore problems

Let's look at ItemFileWriteStore with nested elements since that's often used. Consider a data file like this:

{ 
	identifier: 'id',
	label: 'name',
	items: [
		{ id: '0', name: 'Fruits', children: [
			{ id: '1', name: 'Citrus', items: [
				{ id: '2', name: 'Orange'}
				{ id: '3', name: 'Lemon'}
				...
			]},
			{ id: '4', name: 'Apple'},
			{ id: '5', name: 'Banana'}
			...
		]},
		{ id: '6', name:'Vegetables', children:[
			{ id: '7', name: 'Tomato'},
			{ id: '8', name: 'Lettuce'}
			...
		]},
		...
	]
}

How do we mark 'Tomato' as a "top level" item, so that ItemFileReadStore.fetch() will return that item? Short of actually deleting and recreating the item, which is bad for a number of reasons, there's no way to do that with ItemFileReadStore and nested data. The best we can do is switch to a referenced layout like at the beginning of this article. Although there's a special API to create an item as a child of another item, there's no API to move it to be top level.

Actually, the nested ItemFileReadStore has another limitation that you can't multi-parent an item, such as marking 'Tomato' as both a fruit and a vegatable.

AttachmentSize
store.png15.29 KB
sync.png6.43 KB
decorated.png17.36 KB

Tree Path

Firstly, just thought I'd let you know that someone read your doc seeing you went to the trouble of explaining everything in detail. Nice Work! I'm looking forward to playing with the DnD.

Secondly, just wondering if there is a way to get the full path returned for a node e.g "Fruits;Citrus;Orange" or "Fruits\Citrus\Orange" as that is what our users want to see (after selecting from a hierarchical selection list - i.e. Department tree) even though we would only store the node value (id)? Is this possible of is it a feature request?

Tree Path

Hi DDuncan, glad you read it.

There's no built in way to get the path but since onClick(item, node) gets a pointer to the node in addition to the item, couldn't you just trace up the tree by calling node.getParent() until you hit the Tree widget itself (or maybe it returns null at that point, not sure)?

Confused about getItemChildren

Interesting post - I'm currently using a tree that works against a QueryReadStore (a customized one that has a slightly modified fetch). It loads the first-level child nodes first, and when a user expands a node that has children it uses getItemChildren to populate only that node's first-level child nodes. (FWIW, I tried with an ItemFileReadStore, but it was impractical in my situation.)

Anyway - here's a snippet that does that:

<script type="dojo/method" event="getItemChildren" args="parentItem, onComplete">
   if (parentItem == null) {
      // get top level nodes for this plugin id
      treeStore.fetch({ plugin_id: topLevelId, onComplete: onComplete});
   }
   else {
      treeStore.fetch({ plugin_id: treeStore.getValue(parentItem, 'plugin_id'), onComplete: onComplete});
   }
</script>

But if you are deprecating getItemChildren will I have some other way to do what I'm doing?

Another thing I would love to be able to do with a tree is DnD a tree node onto a textarea to insert some text. Any thoughts on that?

Thanks for the info,

Josh

More info on my tree implementation

I answered a couple forum posts for people looking to do similar stuff (hook into the Expand Node event) - it has more of the tree code I used if curious:

http://www.dojotoolkit.org/forum/dijit-dijit-0-9/dijit-support/dijit-tre...

will check

Hi Josh,

You definitely shouldn't feel tied down to ItemFileReadStore.... QueryReadStore *should* be fine.

I hoped and assumed that QueryReadStore would support the standard interface where an item has a "children" attribute (or the name of your choice) and when you call store.getValues("children") it returns an array of child items. Or perhaps, child item stubs... which Tree loads by calling store. isItemLoaded(childItem) and store.loadItem(). Not so?

Or maybe QueryReadStore supports that but you aren't using it that way.

unfortunately about QueryReadStore

Checked QueryReadStore.js. Unfortunately it doesn't support _reference slike ItemFileReadStore.

So, I can see why you were overriding getItemChildren() as a way to workaround that limitation in QueryReadStore.

Hmm, I guess someone just needs to enhance QueryReadStore to support this functionality. I filed ticket #5876 for this enhancement.

worth noting

tree.getChildren no longer works either... You have to use tree.rootNode.getChildren now.... No deprecation warnings explain that one that I saw.
-Karl

that's true

Unfortunately can't put deprecation warnings on attributes, only on methods (and not on callback methods either).

wort nothing !!!

hi all
As noted above, this is a purely illustrative calculation of the jeneratör jeneratör energy required to keep an avatar "alive" for a 24-hour period in order to make a comparison with the energy consumption of a person over a 24-hour period. For that
nikah şekeri reason, I use the "average saç ekimi population" of Second Life as a proxy for the population of an actual country or region. There are, of course, other ways to calculate SL energy use, nakliyat and I would encourage you and others to explore them all.