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