Jump to Table of Contents

Tree

The Tree component provides a generic tree data structure, which is good for efficiently representing hierarchical data.

A tree has a root node, which may contain any number of child nodes, which may themselves contain child nodes, ad infinitum. Child nodes are lightweight function instances which delegate to the tree for all significant functionality, so trees remain performant and memory-efficient even when they contain thousands and thousands of nodes.

The Tree component itself is purely a data structure and doesn't expose any UI, but it works well as a base class for a View or a Widget.

Getting Started

To include the source files for Tree and its dependencies, first load the YUI seed file if you haven't already loaded it.

<script src="http://yui.yahooapis.com/3.11.0/build/yui/yui-min.js"></script>

Next, create a new YUI instance for your application and populate it with the modules you need by specifying them as arguments to the YUI().use() method. YUI will automatically load any dependencies required by the modules you specify.

<script>
// Create a new YUI instance and populate it with the required modules.
YUI().use('tree', function (Y) {
    // Tree is available and ready for use. Add implementation
    // code here.
});
</script>

For more information on creating YUI instances and on the use() method, see the documentation for the YUI Global Object.

Using Tree

Creating a Tree

Create an empty Tree by instantiating Y.Tree without any options.

// Create a new empty Tree.
var tree = new Y.Tree();

Trees always have a single root node, so an "empty" tree is really just a tree without any child nodes.

To populate a tree with an initial set of nodes at instantiation time, pass an array of node configuration objects to Tree's constructor.

// Create a new tree with some child nodes.
var tree = new Y.Tree({
    nodes: [
        {id: 'node 1'},
        {id: 'node 2', children: [
            {id: 'node 2.1'},
            {id: 'node 2.2'}
        ]},
        {id: 'node 3'}
    ]
});

This creates a tree structure that looks like this:

        root node
       /    |    \
  node 1  node 2  node 3
           /  \
    node 2.1  node 2.2

The id property of node objects is optional. If not specified, a unique node id will be generated automatically.

// Use empty objects to create child nodes with auto-generated ids.
var tree = new Y.Tree({
    nodes: [{}, {children: [{}, {}]}, {}]
});

If you do choose to provide custom node ids, be sure that they're unique. No two nodes in a tree may share the same id.

Tree Properties

Property Type Description
children Array Reference to the children property of the Tree's rootNode. This is a convenience property to allow you to type tree.children instead of tree.rootNode.children.
nodeClass String / Tree.Node The Y.Tree.Node class or subclass that should be used for nodes created by the tree. You may specify an actual class reference or a string that resolves to a class reference at runtime. By default this is a reference to Y.Tree.Node.
nodeExtensions Array

Optional array containing one or more extension classes that should be mixed into the nodeClass when the Tree is instantiated. The resulting composed node class will be unique to the Tree instance and will not affect any other instances, nor will it modify the defined nodeClass itself.

This provides a late-binding extension mechanism for nodes that doesn't require them to extend Y.Base, which would incur a significant performance hit.

rootNode Tree.Node The root node of the tree.

Working with Tree Nodes

Tree Node Properties

Tree nodes use properties exclusively rather than using attributes as many YUI classes do. This ensures that Y.Tree.Node instances are lightweight and extremely fast to create. Using attributes would require extending Y.Attribute, which incurs significant instantiation and memory cost.

All nodes have the following built-in properties:

Property Type Description
canHaveChildren Boolean

Whether or not the node can contain child nodes.

This value is falsy by default unless child nodes are added at instantiation time, in which case it will be automatically set to true. You can also manually set it to true to indicate that a node can have children even though it might not currently have any children.

Note that regardless of the value of this property, appending, prepending, or inserting a node into this node will cause canHaveChildren to be set to true automatically.

children Array Child nodes contained within this node.
data Object Arbitrary serializable data related to the node. Use this property to store any data that should accompany a node when that node is serialized to JSON.
id String Unique id for the node. If you don't specify a custom id when creating a node, one will be generated automatically.
parent Tree.Node Parent node of the node, or undefined for an unattached node or the root node.
state Object Arbitrary serializable state information related to the node. Use this property to store state-specific info — such as whether a node is "open", "selected", or any other arbitrary state — that should accompany a node when that node is serialized to JSON.
tree Tree Reference to the Tree instance with which the node is associated.

When creating a node, any properties you specify in the node's config object will be applied to the created Y.Tree.Node instance. These can be built-in Y.Tree.Node properties or arbitrary properties for your own use.

// Create a tree with some nodes containing arbitrary properties.
var tree = new Y.Tree({
    nodes: [
        {foo: 'bar'},
        {baz: 'quux'}
    ]
});

console.log(tree.children[0].foo); // => 'bar'
console.log(tree.children[1].baz); // => 'quux'

Note that arbitrary properties placed on the node itself won't be serialized if you call the node's toJSON() method or pass it to JSON.stringify(). If you want to store serializable data on a node, store it in the node's data property.

Creating Unattached Nodes

An unattached node is a node that has been created, but hasn't yet been added to a tree. Unattached nodes can be created using a tree's createNode() method.

// Create an unattached node.
var node = tree.createNode();

A node created using createNode() is associated with the tree that created it, so the node's tree property is a reference to that tree, but since it isn't yet a child of a node in that tree, its parent property will be undefined.

console.log(node.tree);   // => the Y.Tree instance that created the node
console.log(node.parent); // => undefined

An unattached node may have children. Children of an unattached node have a parent, but are still considered unattached because the top-most parent node is not the rootNode of a tree.

// Create an unattached node with children.
var node = tree.createNode({
    children: [
        {id: 'unattached child 1'},
        {id: 'unattached child 2'},
        {id: 'unattached child 3'}
    ]
});

To test whether a node is attached, call the node's isInTree() method.

var node = tree.createNode();
console.log(node.isInTree()); // => false

tree.rootNode.append(node);
console.log(node.isInTree()); // => true

An unattached node that was created in one tree can be moved to another tree by passing it to the second tree's createNode() method. The node and all its children will lose their association to the original tree and become associated with the second tree, but will remain unattached.

// Create two trees.
var treeA = new Y.Tree(),
    treeB = new Y.Tree();

// Create an unattached node in Tree A.
var node = treeA.createNode();
console.log(node.tree); // => treeA

// Move the node to Tree B.
treeB.createNode(node);
console.log(node.tree); // => treeB

Adding Nodes To a Tree

Use Y.Tree.Node's append(), insert(), and prepend() methods to add nodes to other nodes as children. Each method accepts a Y.Tree.Node instance, a node config object, or an array of Node instances or config objects.

After adding the node, each method returns the node that was added.

var tree   = new Y.Tree(),
    parent = tree.rootNode;

// Append a node (it becomes the parent's last child).
parent.append({id: 'appended'});

// Prepend a node (it becomes the parent's first child).
parent.prepend({id: 'prepended'});

// Insert a node at a specific zero-based index.
parent.insert({id: 'inserted'}, {index: 1});

You may also pass a Y.Tree.Node instance instead of a config object.

// Append a previously created Tree.Node instance.
var node = tree.createNode();
parent.append(node);

To add multiple nodes at once, pass an array of nodes or config objects.

// Append multiple nodes at once.
parent.append([
    {id: 'zero'},
    {id: 'one'},
    {id: 'two'}
]);

If you add an existing node that's already a child of another node, the node will be removed from its current parent and moved under the new parent. Similarly, if you add a node that's associated with another tree, the node will be removed from that tree and associated with the new tree.

Getting Nodes From a Tree

Use Y.Tree's getNodeById() method to look up any node in the tree (including unattached nodes) by its id.

tree.rootNode.append({id: 'foo'});

// Look up a node by its id.
var node = tree.getNodeById('foo'); // returns the previously added node

Use Y.Tree.Node's next() and previous() methods to get the next and previous siblings of a node, respectively.

tree.rootNode.append([
    {id: 'zero'},
    {id: 'one'},
    {id: 'two'}
]);

// Get the next/previous siblings of a node.
tree.children[1].next();     // => node 'two'
tree.children[1].previous(); // => node 'one'

If you know the numerical index of a node, you can retrieve it directly from the parent's children array.

// Look up a child node by numerical index.
parent.children[0]; // returns the first child of `parent`

Removing Nodes From a Tree

Use Y.Tree.Node's empty() and remove() methods to remove nodes from a tree.

// Remove all of this node's children.
node.empty(); // returns an array of removed child nodes

// Remove this node (and its children, if any) from its parent node.
node.remove(); // chainable

Removing a node causes it to become unattached, but doesn't destroy it entirely. A removed node can still be re-added to the tree later.

To both remove a node and ensure that it can't be reused (freeing up memory in the process), set the destroy option to true when calling empty() or remove().

// Remove and destroy all of this node's children.
node.empty({destroy: true});

// Remove and destroy this node and all of its children.
node.remove({destroy: true});

Use Y.Tree's clear() method to completely clear a tree by destroying all its nodes (including the root node) and then creating a new root node.

// Remove and destroy all the tree's nodes, including the root node.
tree.clear();

Note that while it's possible to manually remove a tree's root node by calling its remove() method, this will just cause another root node to be created automatically, since a tree must always have a root node.

Tree Events

Y.Tree instances expose the following events:

Event When Payload
add A node is added to the tree.
index (Number)
Index at which the node will be added.
node (Tree.Node)
Node being added.
parent (Tree.Node)
Parent node to which the node will be added.
src (String)
Source of the event ("append", "prepend", "insert", etc.)
clear The tree is cleared.
rootNode (Tree.Node)
The tree's new root node.
remove A node is removed from the tree.
destroy (Boolean)
Whether or not the node will be destroyed after being removed.
node (Tree.Node)
Node being removed.
parent (Tree.Node)
Parent node from which the node will be removed.

All events exposed by Y.Tree are preventable, which means that the "on" phase of the event occurs before the event's default action takes place. You can prevent the default action from taking place by calling the preventDefault() method on the event façade.

If you're only interested in being notified of an event after its default action has occurred, subscribe to the event's "after" phase.

Plugins & Extensions

While the base functionality of Tree is kept intentionally simple and generic, extensions and plugins can be used to provide additional features. This makes it easy to adapt the Tree component to a variety of use cases.

Each extension is described here individually, but a custom Tree class can mix in multiple extensions to compose a class with the perfect set of features to meet your needs.

Labelable Extension

The Labelable extension adds support for a serializable label property on Y.Tree.Node instances. This can be useful when a tree is the backing data structure for a widget with labeled nodes, such as a treeview or menu.

To use the Labelable extension, include the tree-labelable module, then create a class that extends Y.Tree and mixes in Y.Tree.Labelable.

// Load the tree-labelable module.
YUI().use('tree-labelable', function (Y) {
    // Create a custom Tree class that mixes in the Labelable extension.
    Y.PieTree = Y.Base.create('pieTree', Y.Tree, [Y.Tree.Labelable]);

    // ... additional implementation code here ...
});

Tree nodes created by this custom class can now take advantage of the label property.

// Create a new tree with some labeled nodes.
var tree = new Y.PieTree({
    nodes: [
        {label: 'fruit pies', children: [
            {label: 'apple'},
            {label: 'peach'},
            {label: 'marionberry'}
        ]},

        {label: 'custard pies', children: [
            {label: 'maple custard'},
            {label: 'pumpkin'}
        ]}
    ]
});

Openable Extension

The Openable extension adds the concept of an "open" and "closed" state for tree nodes, along with related methods and events.

To use the Openable extension, include the tree-openable module, then create a class that extends Y.Tree and mixes in Y.Tree.Openable.

// Load the tree-openable module.
YUI().use('tree-openable', function (Y) {
    // Create a custom Tree class that mixes in the Openable extension.
    Y.MenuTree = Y.Base.create('menuTree', Y.Tree, [Y.Tree.Openable]);

    // ... additional implementation code here ...
});

Tree nodes created by this custom class are now considered closed by default, but can be opened either by setting the state.open property to true at creation time or by calling the node's open() method.

// Create a new tree with some openable nodes.
var tree = new Y.MenuTree({
    nodes: [
        {id: 'file', children: [
            {id: 'new'},
            {id: 'open'},
            {id: 'save'}
        ]},

        {id: 'edit', state: {open: true}, children: [
            {id: 'copy'},
            {id: 'cut'},
            {id: 'paste'}
        ]}
    ]
});

// Close the "edit" node.
tree.getNodeById('edit').close();

// Open the "file" node.
tree.getNodeById('file').open();

Tree instances that mix in the Openable extension receive two new events: open and close. These events fire when a node is opened or closed, respectively.

See the API docs for more details on the methods and events added by the Openable extension.

Lazy Tree Plugin

The Lazy Tree plugin is a companion for the Openable extension that makes it easy to load and populate a node's children on demand the first time that node is opened. This can help improve performance in very large trees by avoiding populating the children of closed nodes until they're needed.

To use the Lazy Tree plugin, include the tree-lazy and tree-openable modules and create a custom tree class that mixes in the Openable extension, as described above.

// Load the tree-lazy and tree-openable modules. In this example we'll also
// load the jsonp module to demonstrate how to load node data via JSONP.
YUI().use('jsonp', 'tree-lazy', 'tree-openable', function (Y) {
    // Create a custom Tree class that mixes in the Openable extension.
    Y.LazyTree = Y.Base.create('lazyTree', Y.Tree, [Y.Tree.Openable]);

    // ... additional implementation code here ...
});

Next, create an instance of your tree class, and plug Y.Plugin.Tree.Lazy into it. Provide a custom load() function that will be called the first time a node is opened. This callback is responsible for populating the node with children if necessary.

// Create a new tree instance.
var tree = new Y.LazyTree();

// Plug in the Lazy Tree plugin and provide a load() callback that will
// populate child nodes on demand.
tree.plug(Y.Plugin.Tree.Lazy, {

    // Custom function that Y.Plugin.Tree.Lazy will call when it needs to
    // load the children for a node.
    load: function (node, callback) {
        // Request child nodes via JSONP.
        Y.jsonp('http://example.com/data?callback={callback}', function (data) {
            // If we didn't get any data back, treat this as an error.
            if (!data) {
                callback(new Error('No data!'));
                return;
            }

            // Append the loaded children to the node (for the sake of this
            // example, assume that data.children is an array of node config
            // objects).
            node.append(data.children);

            // Call the callback function to tell Y.Plugin.Tree.Lazy that
            // we're done loading data.
            callback();
        });
    },

    // Handle events.
    on: {
        // Called before the load() function is executed for a node.
        beforeLoad: function () { /* ... */ },

        // Called if the load() method passes an error to its callback.
        error: function () { /* ... */ },

        // Called when the load() method executes its callback without an
        // error.
        load: function () { /* ... */ }
    }

});

The first time any node with a truthy canHaveChildren property is opened, the Lazy Tree plugin will fire a beforeLoad event and then call your custom load() function, passing in the node being opened and a callback that you should call once you've finished populating the node with children.

How you load your node data is entirely up to you. You could use JSONP, XHR, pull it out of localStorage, or use any number of other techniques. All the Lazy Tree plugin cares about is that you populate the node and call the provided callback when you're done.

If you pass an error to the callback, the plugin will fire an error event.

If you call the callback without an error, the plugin will fire a load event to indicate that the node's children were loaded successfully.

Selectable Extension

The Selectable extension adds the concept of a "selected" state for tree nodes, along with related methods, events, and tree attributes.

To use the Selectable extension, include the tree-selectable module, then create a class that extends Y.Tree and mixes in Y.Tree.Selectable.

// Load the tree-selectable module.
YUI().use('tree-selectable', function (Y) {
    // Create a custom Tree class that mixes in the Selectable extension.
    Y.OptionTree = Y.Base.create('optionTree', Y.Tree, [Y.Tree.Selectable]);

    // ... additional implementation code here ...
});

Tree nodes created by this custom class are now considered unselected by default, but can be selected either by setting the state.selected property to true at creation time or by calling the node's select() method.

// Create a new tree with selectable nodes.
var tree = new Y.OptionTree({
    nodes: [
        {id: 'kittens', children: [
            {id: 'chartreux', state: {selected: true}},
            {id: 'maine coon'},
            {id: 'british shorthair'}
        ]},

        {id: 'puppies', children: [
            {id: 'pug'},
            {id: 'dachshund'},
            {id: 'miniature schnauzer'}
        ]}
    ]
});

// Select a puppy.
tree.getNodeById('pug').select();

By default, only one node in the tree may be selected at a time. Selecting a node when another node is already selected will cause the original node to be unselected. To allow multiple selection, set the tree's multiSelect attribute to true.

When a node is selected, the Selectable extension fires a select event. When a node is unselected, it fires an unselect event.

See the API docs for more details.

Sortable Extension

The Sortable extension makes it possible to sort the children of any node using custom sorting logic, and also ensures that inserted nodes are added at the appropriate index to maintain the current sort order.

To use the Sortable extension, include the tree-sortable module, then create a class that extends Y.Tree and mixes in Y.Tree.Sortable.

// Load the tree-sortable module.
YUI().use('tree-sortable', function (Y) {
    // Create a custom Tree class that mixes in the Sortable extension.
    Y.SortableTree = Y.Base.create('sortableTree', Y.Tree, [Y.Tree.Sortable]);

    // ... additional implementation code here ...
});

Nodes will now be sorted automatically as they're inserted in this tree, or you can manually re-sort all children of a specific node by calling that node's sort() method.

By default, nodes are sorted in insertion order, meaning that the first node you insert gets index 0, the second node inserted gets index 1, and so on. To customize the sort criteria, pass a custom sortComparator function to the tree's constructor, or set it on the tree's prototype. This function will receive a node as an argument, and should return a value by which that node should be sorted.

Here's a sortComparator function that sorts nodes by id:

var tree = new Y.SortableTree({
    sortComparator: function (node) {
        return node.id;
    }
});

To sort nodes in descending order instead of ascending order, set the tree's sortReverse property to true.

Each node in a tree may optionally have its own custom sortComparator and/or sortReverse properties to govern the sort order of its children. This makes it possible to use different sort criteria for different nodes in the tree. Setting these properties on a node will override the tree's sortComparator and sortReverse properties for that node's children (but not for its children's children).

Tree instances that mix in the Sortable extension receive a sort event that fires when a node's children are manually re-sorted by calling the sort() method.

See the API docs for more details on the methods and events added by the Sortable extension.

Creating Custom Tree Extensions

Y.Tree extends Y.Base, so a Tree extension begins just like any other Base extension class. However, since Y.Tree.Node doesn't extend Y.Base for performance reasons, a special composition mechanism is used to allow for lightweight Y.Tree.Node extensions.

For a simple example, let's look at the implementation of the Labelable extension.

The Y.Tree.Labelable class, which will be mixed into a Tree as a Base extension, looks like this:

// Y.Tree.Labelable extension class.
function Labelable() {}

Labelable.prototype = {
    initializer: function () {
        this.nodeExtensions = this.nodeExtensions.concat(Y.Tree.Node.Labelable);
    }
};

Y.Tree.Labelable = Labelable;

In the initializer() method, the Labelable extension creates a copy of the tree's nodeExtensions array, then adds the Y.Tree.Node.Labelable class to it.

The Y.Tree.Node.Labelable class looks like this:

// Y.Tree.Node.Labelable class.
function NodeLabelable(tree, config) {
    this._serializable = this._serializable.concat('label');

    if ('label' in config) {
        this.label = config.label;
    }
}

NodeLabelable.prototype = {
    label: ''
};

Y.Tree.Node.Labelable = NodeLabelable;

The specific implementation here isn't important, but it illustrates how node extensions work.

When a Tree instance is created, Y.Tree extensions have a chance to add their custom Y.Tree.Node extension classes to the nodeExtensions array. Once all the tree extension initializers have run, a "composed" Tree Node class is created.

This composed Tree Node class mixes in all the prototype properties of every class in nodeExtensions and automatically chains their constructor functions. This is similar in some ways to how Y.Base extensions work, but much lighter and faster, so composed nodes remain very efficient.

For more detailed examples of Tree and Tree Node extensions, take a look at the source code for the Openable and Selectable extensions.