Dojo Tree


Introduction

This documentation refers to new TreeV3, not to stable Tree widget. Many classes have V3 on the end for TreeV3, but that suffix can be omitted, because it will be removed in dojo 0.5.

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, quite incomplete, 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 (incomplete)

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

Currently, multiple drag'n'drop does not work with multiple selection because of dojo bugs. Hopefully will be fixed.

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.

Features

  • Flexible styling
    • All design in CSS through classes and class combinations
    • Different trees be styled with different CSS class families
    • Multiline and rich content support
  • Full set of node operations
    • expand/collapse
    • create with JS or markup
    • destroy/move/clone
    • addChild/detach/(de)folderize
    • inline editing
    • multiple selection and drag'n'drop
  • Performance
    • batch operations
    • special features
  • Lazy loading & RPC features
    • ready to be in-sync with your data
  • Rich event system
    • fluently integrates with your app
  • Customization
    • change everything through inheritance, events and css
    • out-of-the box extensions
  • Tests and demos

Todo for TreeV3

A must, easy to implement

  • doClone in RPC should get cloned nodes from server, like createChild

Optional enhancements

  • 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
  • Make DisableSelection extension to keep node selection
  • 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..
  • Add checkbox extension. Every node will have checkbox, can save/restore checked state. When parent is checked, all children are marked "grey-checked", user may uncheck grey-checked nodes. grey-checked dependant nodes removed if parent is unchecked. This will require rewrite TreeDocIconExtension ( docIcon -> checkbox ) to allow arbitrary new "thing" left from node.

Problems to solve

  • 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.
  • Drag'n'drop onDrop errors can't be handled. Need a way to hook on e.g permission denied errors.
  • When I expand a large tree and try to move its top (dnd), node starts to move after a pause - a gap in time between I start moving node with my mouse and dragClone appears. Lots of calculations?
  • wipe and fade togglers are broken
  • can't add subwidgets in title. Seems I need to put them to children, and children property will contain not only subnodes, but also title widgets.. Will need to fix a lot.

Tree events

There are many classes of events, published with dojo.event.publish mechanism. Every event has a name and message object, containing more precise information about what happened. You may use events to update your data while tree changes, and to perform additional processing of involved objects.

There is a default naming scheme for an event class. E.g for a tree with widgetId='mytree', event of class afterTreeCreate will be named "mytree/afterTreeCreate". You may provide other names in eventNames property of the tree.

afterTreeCreate

Event occurs after tree creation is complete. There is an alternative to hook on this action by putting your objects in "listeners" property of the tree. The difference is that listeners are guaranteed to hook before nodes get added, and afterTreeCreate is published after Tree widget is created.

source
references to tree

beforeTreeDestroy

Published right before actual Tree#destroy method is called. Useful for cleanups

source
references to tree

beforeNodeDestroy

Right before TreeNode#destroy is called. Node is detached after this event fired.

source
references to node

afterChangeTree

This event is tightly created with node creation process. It is fired when

  • a node is created
  • a node moves to another tree widget

oldTree
references previous tree, null if node has been just created
newTree
new(current) tree
node
target node

afterSetFolder

Fires when a node obtains "folder" state. That may happen when a first child is added to a leaf, or if a node was initially created with isFolder=true

source
references to node

afterUnsetFolder

Fires when a node obtains looses "folder" state. That may happen when a last child leaves the node, and Tree.unsetFolderOnEmptyis set, or when unsetFolder is called explicitly.

source
references to node

(before|after)Move(From|To)

These events share same arguments and fire when a node is moved. Move process is considered something special. When you move a node, no detach/addChild events get thrown. That allows to tell situations when a node leaves a tree for some time (detached then attached) from situations when a node is simply moved to another location

oldParent
previous parent
oldTree
previous tree
oldIndex
previous index among siblings
newParent
new parent
newTree
new tree
newIndex
new index among siblings
child
target node

afterAddChild

Published when a node is attached to parent. This may happen at the end of creation process, or when a node is lazily instantiated from data object.

child
references to node
index
index among siblings
parent
current parent who adopted a child
childWidgetCreated
flag is set if child was laziliy instantiated. That is: it resided as data object in children array, but user expanded its parent, so node widget came to life.

afterDetach

Occurs when a node is detached. This may happen in the process of node destruction.

child
references to node
parent
references to old parent
index
references to index among children of old parent

after(Expand|Collapse)

Fire when a node is expanded/collapsed. Some togglers do nice animation hiding/showing node. This event fires when animation finishes.

source
target node

afterSetTitle

When a node is edited, or explicit setTitle method is called, this event helps to inform interested parts about changes.

source
target node
title
new node title

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

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

The results described here refer to operations without any lazy features involved. Most of time you will use lazy creation or lazy loading, or both, and operate with 10000 "virtual" nodes with ease.

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 ?

Every data object you send is created with Tree#createNode call.

You want to pass a special object, specify widgetName in widgetName property of your object.

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

Extensions

Extensions are also called plugins, they can be hooked onto widgets in various combinations and provide wanted options.

Currently there is a couple of extensions

TreeDisableWrapExtension
Tree extension, disables wrapping for tree nodes. Also it fixes IE bug when an 'unwrappable' node (e.g single word) will move to next line if no space left.
TreeDocIconExtension
Tree extension, places icon to the left of a node, depening on nodeType property
TreeEmphasizeOnSelect
Selector extension, highlights currently selected nodes
TreeDeselectOnDblselect
Selector extension, deselects a selected node when it is clicked. Usually, one should ctrl-click, or click another node.
TreeLinkExtension
Tree extension, turns labels into links, merges object property into <a> tag

Node creation

There are few code paths that lead to same purpose: to create a tree node. They differ in effeciency and use patterns

Markup creation

You specity a tree and its nodes in HTML, relying upon dojo to parse it and turn into widgets. That is a slowest way, but nice for small trees or if only tree top is specified and the rest is created later.

dojo widget parser walks DOM and creates a special structure. The next pass creates widgets from the structure.

widget.create

The generic widget creation routine. It basically runs the operations in order:

  1. Mix in widget properties from parameters/markup
  2. Register widget in widget.Manager
  3. Call buildRendering to make fill template and create domNode
  4. Call initialize
  5. Call postInitialize. registers widget as a child of its parent and after it creates all subwidgets
  6. Call postCreate

Note that initialize is called in pre-order: parent is initialized before children, postInitialize is called in post-order: a child is postCreated before its parent.

Manual creation

If you create nodes with javascript, then you run create calls manyally. So parents are naturally created (and postCreated) before children

There seem to be no good way to distinguish betwen markup creation and manual creation. From the one hand its seems good, because allows reuse of generic creation code. From the other hand code paths going through this code are subtly different.

The reliable thing is that initialize will process widget after its domNode is built, BUT it should not assume anything about children.

nodeCreate event is fired on initialization also. If you want to know anything about children and do something at this point - check addChild, but not node creation.

Input parameters

children array may be

  • empty
  • contain widgets, e.g if created from markup, or someone created them before parent and pushed in
  • contain data objects, that will be turned into widgets when parent expands.

isFolder comes into play only when there are no children. It allows creation of empty folders, with UNCHECKED state that can be filled later.

Faq

How to make tree unselectable?

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.

How to bind an object to tree node?

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

How to 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.

How to evade a situation where all nodes are (re)moved and tree is empty without a way to add new child (no nodes) ?

Make a single root node with actionsDisabled="DETACH;MOVE". User will be unable to remove it, so interface will stay sane.

How to make pages open when a user clicks on node?

There are 2 ways. The first one is to attach TreeSelector and hook on "select" event. So when a user clicks, event handler will change url to node.object.href. Of course, you should fill hrefs.

A probably more convinient path would be to employ TreeLinkExtension, which will turn your labelNodes into real links, and apply attrbutes from node object to them.

I open very large tree. But navigation away to another page from the tree takes time. What's up?

Dojo performs actions not only when a node is created, but also cleanup when a node is destroyed. Lazy features allow node creation be distributed in time, but when you navigate away from a large tree, cleanup need some time. I don't know a way to evade that.

How to add icons to nodes ?

TreeDocIconExtension handles that. You should declare nodeType for your nodes, so they'll get nodeIcon[Your type] CSS class. Default type is Document for leaves and Folder for folders.

Contributors

Thanks to all dojo contributors, Tree widgets wouldn't be possible without such a nice background.

Also, there are people who contributed their skills to make Tree widget better.

  • Ilia Kantor
  • Slava Ivanyuk
  • Nachoman

In case if someone is left out, message me, please.