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.
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.18.1/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.
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.
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
This provides a late-binding extension mechanism for nodes that doesn't require them to extend |
rootNode |
Tree.Node | The root node of the tree. |
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 Note that regardless of the value of this property, appending, prepending, or inserting a node into this node will cause |
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.
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
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.
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`
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.
Y.Tree
instances expose the following events:
Event | When | Payload |
---|---|---|
add |
A node is added to the tree. |
|
clear |
The tree is cleared. |
|
remove |
A node is removed from the tree. |
|
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.
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.
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'} ]} ] });
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.
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.
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.
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.
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.