This tutorial is for Dojo 1.6 and may be out of date.

Up to date tutorials are available.

NodeList Extensions

Dojo includes a range of extensions to the dojo.NodeList collection that is used by dojo.query. In this tutorial, we’ll look at what extended functionality is available and how to put it to good use.

Getting Started

In the earlier tutorial on dojo.query, we saw how to get a collection of nodes matching a query or selector, and how to use the methods on dojo.NodeList to work with those nodes. Let’s quickly recap. Here’s the markup we’ll be using for most of the demos (we’re going with a fruity theme for this tutorial):

<h3>Fresh Fruits</h3>
<ul id="freshList"></ul>

<h3>Fruits</h3>
<ul>
	<li class="fresh">Apples</li>
	<li class="fresh">Persimmons</li>
	<li class="fresh">Grapes</li>
	<li class="fresh">Fresh Figs</li>
	<li class="dried">Dates</li>
	<li class="dried">Raisins</li>
	<li class="dried">Prunes</li>
	<li class="fresh dried">Apricots</li>
	<li class="fresh">Peaches</li>
	<li class="fresh">Bananas</li>
	<li class="fresh">Cherries</li>
</ul>

To demonstrate dojo.query, a click handler has been attached to a button which executes the following code:

var nodes = dojo.query("li.fresh");
nodes.addClass("highlight");
nodes.onclick(function(evt){
	alert("I love fresh " + this.innerHTML);
});
nodes.place(dojo.byId("freshList"));

The call to dojo.query returns a dojo.NodeList, which is a standard JavaScript array decorated with additional methods that let us work more easily with a collection of DOM nodes. Because each NodeList call returns a NodeList, we can make this even simpler by chaining method calls (instead of typing nodes over and over again):

dojo.query("li.fresh")
	.addClass("highlight")
	.onclick(function(evt){
		alert("I love fresh " + this.innerHTML);
	})
	.place(dojo.byId("freshList"));
View Recap Demo

Troubleshooting chains of method calls can be difficult, as there’s nowhere to add logging statements or breakpoints in the debugger. Break apart the chain into discrete steps to inspect what each method returns.

Doing More with dojo.NodeList

This pattern of getting some nodes and doing something with them is pervasive enough that many potential features of dojo.NodeList end up conflicting with the goal of Dojo Base to provide only the common, foundational functionality that most people need most of the time. As a result, in Dojo Core and DojoX, there are a variety of NodeList extension modules that can be dojo.require’d to add new functionality to dojo.NodeList as necessary. Let’s take a look at them now.

A Note on Documentation

In the API browser, NodeList extension methods wind up being listed alongside the default NodeList methods regardless of where they are actually defined. In the reference docs, though, each extension module has its own page (such as dojo.NodeList-data), which makes it clearer which module provides which methods.

Animating Elements

The dojo.NodeList-fx module augments dojo.NodeList with a series of methods that allow you to apply effects from Dojo’s effects system to a collection of nodes. These methods function identically to their non-NodeList counterparts, so take a look at the Dojo Effects and Animation tutorials if you aren’t familiar with them.

In this demo, we’ll use the same fruity list, and a button that executes the following code when clicked:

dojo.require("dojo.NodeList-fx");

dojo.query("li.fresh")
	.slideTo({
		left: 200, auto: true
	})
	.animateProperty({
		properties: {
			backgroundColor: { start: "#fff", end: "#ffc" }
		}
	}).play();
View Fx Demo

Unlike most NodeList methods, NodeList-fx methods return an animation object by default, which conflicts with the normal chaining behavior of NodeList. This is because Dojo’s animation functions normally return an animation object, and it’s up to you to call play on that object to start the animation. To cause a NodeList-fx method to automatically play the animation and return a NodeList instead, set auto: true in the object passed to the function, as demonstrated above in the slideTo call.

Associating Data with Elements

The dojo.NodeList-data module adds a mechanism for attaching arbitrary data to elements via the data method. Here’s an example that stashes a Date object on an element each time it is clicked:

dojo.require("dojo.NodeList-data");

function mark(evt){
	var nodeList = new dojo.NodeList(this); // make a new NodeList from the clicked element
	nodeList.data("updated", new Date());   // update the 'updated' key for this element via the NodeList
}

dojo.ready(function(){
	dojo.query("li")                 // get all list items
		.data("updated", new Date()) // set the initial data for each matching element
		.onclick(mark);              // add the event handler

	dojo.connect(dojo.byId("btn"), "onclick", function(){
		dojo.query("li").data("updated").forEach(function(date){
			console.log(date.getTime());
		});
	});
});
View Data Demo

Here, we’re doing three things: associating an initial Date object with each element, setting up an onclick handler to call the mark function, and setting up a button that allows us to view the data for each item. Inside the onclick handler, we still need a NodeList to get at the data properties for the clicked element, so we create a new one from the element that was clicked. The existing Date object on the clicked element is then replaced with a new one.

With NodeList-data, it is extremely important that you call removeData on the NodeList (or dojo._removeNodeData on the element itself) when removing elements from the DOM. If this is not done, your application will leak memory, since the data is not actually stored on the element itself and will not be garbage collected even if the node itself is.

Moving Around the DOM

The dojo.NodeList-traverse module adds methods to NodeList that allow you to easily move around the DOM to find parents, siblings, and children of reference nodes.

To illustrate, we’ll use a longer, categorized list of fruit. Some fruits have been marked as tasty (with the class yum), and we want to be able to 1. highlight them, and 2. indicate in the header for that list that there’s goodness inside. Using the methods provided by NodeList and NodeList-traverse, here’s one quick way to do that:

dojo.require("dojo.NodeList-traverse");

dojo.ready(function(){
	dojo.query("li.yum")        // get LI elements with the class 'yum'
		.addClass("highlight")  // add a 'highlight' class to those LI elements
		.closest(".fruitList")  // find the closest parent elements of those LIs with the class 'fruitList'
		.prev()                 // get the previous sibling (headings in this case) of each of those fruitList elements
		.addClass("happy")      // add a 'happy' class to those headings
		.style({backgroundPosition: "left", paddingLeft: "20px"}); // add some style properties to those headings
});
View Traverse Demo

The chain here starts with an initial query to find the list nodes we’re interested in, then uses traversal methods to move up and sideways to find the heading elements associated with the lists that contain those list nodes.

The important thing to understand with the traversal methods is that each call returns a new NodeList containing the results of your traversal. Methods like closest, prev, and next are essentially subqueries, with the nodes in the current NodeList being used as a reference point for the next subquery. Most of these methods function identically to traversal methods in jQuery and should feel very familiar to users of that library.

Manipulating Elements

The dojo.NodeList-manipulate extension module complements the traverse module by adding some methods for manipulating the nodes in a NodeList. This package covers DOM operations covered by dojo.place, with additional handy methods for wrapping elements and getting & setting node contents.

The following example puts some of these capabilities to use. Using the same categorized list of fruits, it builds two new lists of yummy and yucky fruits:

dojo.require("dojo.NodeList-manipulate");

dojo.ready(function(){
	dojo.query(".yum")  // get elements with the class 'yum'
		.clone()        // create a new NodeList containing cloned copies of each element
		.prepend('<span class="emoticon happy"></span>') // inject a span inside each of the cloned elements
		.appendTo("#likes"); // insert the clones into the element with id 'likes'

	dojo.query(".yuck")
		.clone()
		.append('<span class="emoticon sad"></span>')
		.appendTo("#dontLikes");
});
View Manipulate Demo

The key to this demo is the use of the clone method to create duplicates of the original elements. As with the NodeList-traverse methods, clone returns a new NodeList containing all newly cloned elements which are then modified and appended to the DOM. If clones were not created, the original elements would have been modified and moved instead.

Advanced Content Injection

The dojo.NodeList-html module brings the advanced capabilities of dojo.html.set to dojo.NodeList. Here’s a simple example of its use, in which we turn a simple list into a checkbox list using dijit.form.CheckBox widgets:

dojo.require("dojo.NodeList-html");

dojo.ready(function(){
	dojo.query(query)
		.html('<input name="fruit" value="" data-dojo-type="dijit.form.CheckBox">', {
			onBegin: function() {
				var label = dojo.trim(this.node.innerHTML),
					cont = this.content + label;
				cont = cont.replace('value=""', 'value="' + dojo.trim(this.node.innerHTML) + '"');

				this.content = cont;
				return this.inherited("onBegin", arguments);
			},
			parseContent: true
	});
});
View html Demo

With the rich capabilities offered by other NodeList methods, especially those in NodeList-manipulate, the NodeList-html module is probably not one you will use very often, if at all. It is mentioned here nonetheless because it can still be useful as a specialized tool to solve a certain class of problems that would be much more difficult to solve in other ways.

Event Delegation

The final extension module we’ll look at is the dojox.NodeList-delegate module. This module brings event delegation to NodeList. Event delegation is a mechanism that exploits the way that events bubble up the DOM in order to reduce the number of event listeners on a page. A simple example illustrates its use and advantage:

dojo.require("dojox.NodeList-delegate");

dojo.ready(function(){
	dojo.query(".fruitList")                      // on each '.fruitList'
		.delegate("li", "onclick", function(evt){ // add an event listener that listens for clicks on child 'li' elements
			console.log("clicked: ", evt.target, " target element: ", this, " bound element: ", evt.currentTarget);
			dojo.toggleClass(this, "yum");
		});
});
View Delegate Demo

In our demo, this code attaches only four event handlers. Compare that to this similar-looking code, which offers the same functionality but works less efficiently:

dojo.ready(function(){
	dojo.query(".fruitList li") // on each 'li' in each '.fruitList'
		.onclick(evt){          // add an event listener that listens for clicks
			dojo.toggleClass(this, "yum");
		});
});

In our demo, this code would end up attaching 64 event handlers.

Since attaching event listeners is an expensive operation, using delegation can easily turn a sluggish application into a speedy one. It also removes the worry of needing to tear down and create event listeners when you add or remove elements from the DOM. In this example, we can simply add and remove fruit from lists, and event handling is automatically ensured by virtue of the element’s position in the DOM.

Note that delegate—like all other NodeList event-related methods—currently provides no way of disconnecting the event listeners it creates; the disconnect handle (returned by calls to dojo.connect under the hood) is discarded. If you need to unhook event listeners, use dojo.query to return a NodeList and call dojo.connect on each element in the list yourself, storing the disconnect handles as they are returned.

Conclusion

NodeList modules extend the existing NodeList API without bloating your code with features you won’t use. By using some of these extensions in your code, you will be able to work with the DOM much more effectively and efficiently.