Dojo Tree Tutorial


Introduction

This documentation refers to new TreeV3, not to stable Tree widget. Feel free to suggest features and test/critisize existing solutions.

Basic things are shown in tests (dojo/tests/widget/treeV3), so you might want to check them first and copy-paste exactly the things you need. The tutorial is meant to provide deeper understanding for those who want to extend/modify tree behavior.

This is just a draft, but I'd be grateful for your replies. The contacts are

  • IRC: Freenode, #dojo by nick [algo]
  • ICQ: 820317
  • Dojo mailing lists: "Ilia Kantor" ilia @ dojotoolkit.org

What's new in TreeV3

New HTML/CSS structure

Nested divs

Previous tree used a list of divs, each of them was indented with grid and spacers to right level. The new tree uses natural nested divs structure (children' divs inside parent's div). Grid is contigous and structure is displayed correctly for any node/font size

All design in CSS through classes and class combinations

All image and size information was removed from JS code. There is a bunch of classes applied to nodes, that may denote node folder state, node type, show if there are children, etc. CSS moves this logical classes into style

Different trees be styled with different CSS class families

Want to put 2 differently styled trees on a page? Give them different classPrefix.

Multiline content support

Rich content support was incomplete, because list-of-divs model could not handle arbitrary-sized nodes. Now you may have <br>, <p> and any other width/height modifiers.

Event system modified

nodeDOMCreated event was removed. That's because listeners are bound to tree and may want to modify the new node, but that's only possible when the node is being bound to the tree, not when it was created and hanging around. afterTreeChange was introduced to help listeners to (un)bind nodes the right moment

All events were renamed to better reflect the moment of their publishing

afterExpand, afterCollapse events now fire when the animation (e.g fading in or out) finishes, not when the actual expand/collapse is called.

Lazy widget creation

Before TreeV3, all nodes must be widgets. A node is added - hence graphical widget is created. For performance reasons that behavior was altered. Now when you add a node, you may actually add a "data object", containing node data, e.g {title:"new node"}. You may want to add a large nested branch of such data objects, like {title:"new", children:[...data objects..]}.

Data objects will become real members of children array (you may recursively search them, modify etc), but graphical widgets will be created only when visitor expands them.

The compatibility drawback of such behavior is that old code may erroneously call *widget* methods on *data objects* while recursively traversing a tree, e.g with Widget#getDescendants. You should change such code to use TreeCommon#processDescendants, or handle data objects in special way.

There are no special mechanisms to add laziliy instantiated "data objects". You may manipulate them simply modifying children array, but no events are thrown until a real widget appears on the scene. In most cases that is fine, but you are free to "disable" lazy widget creation - do not modify children directly and enable tree.eagerWidgetInstantiation

Tree extensions

Many features were moved from core into extensions

  • Added TreeDocIconExtension instead of builtin childIcon support
  • Selector now only throws events, not doing anything with nodes
  • Out-of-the box extensions introduced to be examples and handle well-known requirements

Implicit helpers removed

The Tree is actually a pack of loosely coupled components, connected through events. To keep things simple and also for compatibility reasons, such components(controller,selector...) were created implicitly, if not declared. But actually this proved to be a source of questions and misunderstandings. So now nothing is created implicitly, read how-to and declare things.

RPC has both sync/async modes

Old callbacks code was removed in favor to dojo.Deferred. Now all operations may be async and run your callbacks at the end.

Drag'n'drop changes

Multiple selection and multiple drag'n'drop (not ready yet)

Sounds simple enough.. Select multiple nodes with ctrl and get them with selector.selectedNodes. instead of removed selectorNode call.

Drop of any source, not just tree node

If treeNode property is empty, tree will create a new node from the data returned by source.getTreeNode, then source.onDrop will be called to remove old node.

Inline node editing

It became possible to edit nodes inline, using TreeEditor. Base variant uses RichText widget, you can make another wrapper though. Remote calls can be made on save only, or on start/cancel too e.g for locking purposes.

TODO for TreeV3

Optional

  • Automated unit-testing system based on Selenium
  • Optimize all gifs with AGO (advanced gif optimizer) carefully. AGO spoils transparent gifs. It may make single layer LESS than reported image size, that'd lead to background-repeat effect under OS X or Opera.
  • Make SortChildren extension to keep nodes sorted
  • On Drag'n'drop tree 'wobbles', because node size increases/decreases when it is bordered. Could be nice to evade it, either placing another transparent div with border onto the node or decreasing their size, or using padding divs, or..

BUGS

  • Multiple dnd of nodes selected by multiple select work incorrectly. Although all is fine if dragManager uses shift for multiple dnd and TreeSelector uses ctrl. No idea what the reason is.

Features

Important

We will have that all, but not everything in place right now
  • Flexible styling
    • All design in CSS through classes and class combinations
    • Different trees be styled with different CSS class families
    • Multiline content support
  • Full set of node operations
    • expand/collapse
    • create with JS or markup
    • destroy/move/clone/edit
    • addChild/detach/(de)folderize
    • batch operations
    • drag'n'drop
  • Written with performance in mind
  • Integrated lazy loading & RPC features
  • Rich event system
  • Extension system with out-of-the box working examples
  • Tests and documentation

Important

The rest of the document is draft and may be outdated

Performance

Tree was coded with performance in mind. Although, JavaScript itself is a slow language. Flexible model requires some code that slows it down. It's not DOM manipulations, but actually javascript that I couldn't make lighter. Being a part of dojo/widget structure also implies some overhead, but also power.

Almost all operations require small constant time when single node is involved. Depending on your application you may notice slowdown when (most common) creating lots of nodes or performing other batch operations.

In my tests 1000 nodes required 0.7-0.8 sec, growth is linear, depth does not matter, children are created with createSimple and added to parent all at once with setChildren.

Creation from markup or with standard create/addChild routines is 2-3 times slower, because these routines are generic.

Memory footprint (IE,FF) is about 1M per 100 nodes, that were not postponed by lazy creation feature

Comparison

Fast node creation with dojo tree is 2-3 times slower than xtree 1.7, another tree widget, not so featured, but nicely optimized for performance.

Important

These tests are in-dev tests carried on early stages. The results may change, but not much.

Performance tricks

When talking about performance, one should understand, that there are single-node operations that operate on single node... These ones are fast. The examples are: create a node, delete a node, move a node along the tree.

... And there are batch operations that touch a lot of nodes. The examples are: initial tree creation, moving a node from one tree to another which has different listeners, etc.

That performance issues become noticeable at 100-300 tree nodes depending on your trees. All algorithms are linear in worst case, but JS is slow language, DOM is also not that fast.

There is a number of features one could use to get a speedup.

Lazy loading

A node can be created with isFolder=true flag, but without children. Any node has a state, initially UNCHECKED for empty folder, and used by TreeLoadingController.

When a user presses expand, tree controller (supporting lazy loading) will send a request to server asking for nodes, and parse the answer creating children.

The benefit is obvious: you don't have to load/process whole tree at once. You can only load a single node and user will load the rest clicking "expand"

Lazy creation

Node/tree keeps array of its children in children property. Lazy creation is somewhat a half-way approach to lazy loading. It allows you to put data objects into this array and tree will create widgets of them later, when they are expanded.

For instance, one can call node.children = [{title:'node1'},{title:'node2'}]. The objects will be set, but no widgets are created. You can also set children to nested array: node.children = [{title:'node1', children:[{title:'node2'}] }].

You can create tree on server, JSON-serialize it and put to HTML, that is gzip-compressed. Compression will be 6 times or more, so it is not that space hungry.

The benefit comes from postponing almost all real job: widget creation and attaching it to tree will happen in expansion-time.

Comparison between lazy creation and lazy loading
  • You need web-service for lazy loading, not for lazy creation
  • No network waits for lazy creation
  • Lazy creation gives you the tree right here. You can search data objects and modify them without spending time and memory on graphical widgets

Sometimes, lazy creation and loading may work together nicely, providing seamless increase in speed and decrease in memory footprint. For instance, server may pass a whole tree branch in JSON to lazy loading controller. Top nodes will be created right along, because user needs them, but the rest of the branch will be postponed relying on lazy creation feature.

There are operations, like "expandAll" where such lazy tricks don't help, because all graphical widgets must be processed. That is why widget creation process is well-optimized itself. createSimple is a hacky program-only way to create TreeNodes fast. setChildren is a method to assign (and create if needed) all children at once. It helps to evade some extra work happening when children are added one by one.

IE image-reloading fixup

IE has a well-known bug. If an image was loaded dynamically - with a new Image(), or img.src= assignment, or even as a background of a new node, it will not be cached. So every time when you create a node, all needed icons get loaded from server (or requested at least). A possible solution is to put a special div into HTML (adjust src to your path):

<div style="display:none">
  <!-- IE has a bug: it reloads all dynamically resolved images, no matter, is it 
  new Image() or CSS background. If you don't specify images like that,
  it will reload them every time a new node is created -->
  <img src="../../../src/widget/templates/images/TreeV3/i.gif"/>
  <img src="../../../src/widget/templates/images/TreeV3/i_half.gif"/>
  <img src="../../../src/widget/templates/images/TreeV3/expand_minus.gif"/>
  <img src="../../../src/widget/templates/images/TreeV3/expand_plus.gif"/>
  <img src="../../../src/widget/templates/images/TreeV3/expand_leaf.gif"/>
  <img src="../../../src/widget/templates/images/TreeV3/i_long.gif"/>
  <img src="../../../src/widget/templates/images/TreeV3/document.gif"/>
  <img src="../../../src/widget/templates/images/TreeV3/open.gif"/>
  <img src="../../../src/widget/templates/images/TreeV3/closed.gif"/>
</div>

Components

Tree
Tree and TreeNode classes handle both data and view
Controllers
TreeBasicController
Controller that provides all capabilities but no server calls
TreeLoadingController
Added dynamic node loading from server
TreeRPCController
All actions call server
Selector
TreeSelector handles node selection. Currently, only single node can be selected
Drag'n'Drop
TreeDragSource, TreeDropTarget and TreeDNDController are classes that allow nodes to be dragged, to be dropped on and mechanism binding that together. Sources are located under src/dnd folder, not among widgets.
Context menu
TreeContextMenu inherits dojo context menu to provide a lightweight right-click menu. There is a single menu object for all nodes, although it always knows its target
Style & icons
Tree.css and icons are located under src/widget/templates and provide basic style used by default.
Extensions
TreeControllerExtension contains additional functions for controller that you might need

Lazy loading

How to load *not* TreeNodes ?

You can pass objects as element of children array, but you can also pass arrays. If you pass objects, they are treated as data objects for TreeNodes, but arrays are treated as arguments for createWidget call. E.g if you want to createWidget("MyWidgetNodeType", {title:'somedata'}), then you pass ["MyWidgetNodeType, {title:'somedata'}] instead of just {title:'somedata'} from server (or in setChildren call).

Another option is to change defaultChildWidgetType property of the tree. Helpful in case when you have your own widgets and use them elsewhere instead of TreeNode

Tree model

Two classes handle the model: TreeV3 is "tree" itself and somewhat a root node, while TreeNodeV3 is any other node, linked by parent and children[] properties. There is a common mixin TreeWithNode.

Tree parameters

listeners specifies a list of widgetIds. They will be called AFTER tree itself is initialized, but BEFORE any nodes get added. This allows them to hook on node creation events, mostly useful for markup creation.

Extensions

Extensions provide additional functionality by either adding new methods or hooking on events

Additional aim of writing them is to demonstrate how one can do certain things.

You can declare extensions in markup by listing them in extension property.

An extension is declared just like any other widget and it has special loadExtension method that applies it.

How-to

Make tree unselectable

TODO: write extension Sometimes people want tree nodes to be unselectable, sometimes selection is a good way to copy (part of) content from the node.

To make tree (or its elements) unselectable use dojo.html.disableSelection in nodeCreate and treeCreate hooks. Apply disableSelection to every node you want to make unselectable.

Bind an object to tree node

There is an "objectId" property and "object" property ready to be filled in from markup or program-way.

Walk all node descendants

You may use dojo.lang.forEach(nodeOrTree.getDescendants(),function(elem) { ... }) to process all descendants, it will walk children property recursively.

The safer way would be to call TreeCommon.prototype.processDescendants(nodeOrTree, filter, func), it will process all children with func, but will not descend into nodes if filter(node) returns false. E.g see collapseAll controller method uses it to collapse all widgets, but skip non-folders and data objects.