Chapter 5. Node Manipulation

This chapter provides an overview of query, behavior, and NodeList. These constructs provide concise and highly efficient mechanisms for manipulating DOM nodes. Querying the DOM using query 's CSS selector syntax, decoupling events and manipulations from an HTML placeholder with Core's behavior module, and chaining operations together with the syntactic sugar offered by NodeList are among the fun topics coming up.

Query: One Size Fits All

If you've done much JavaScripting, you've no doubt needed to query against the DOM to look up some nodes based on some set of criteria. If you only needed to look them up by tag name, then you probably used document.getElementsByTagName and called it a day. However, if you needed to look up a set of nodes by class, a specific attribute value, or some combination thereof, you may have scratched your head and wondered why there wasn't a built-in getElementsByClass function. Apparently, everyone wondered that very same thing, and then set out to write their own version—some more successful than others.

Although earlier versions of Dojo included specialized implementations of functions like getElementsByClass, the toolkit now includes a function that universally allows you to query the DOM with CSS query syntax. To illustrate the use for a DOM querying Swiss army knife, consider a heroic attempt at implementing a getElementsByClass function (a very common need) yourself:

// Lookup elements from a class name, optionally starting at a particular parent node
function getElementsByClassName(/*String*/className, /*DOMNode?*/node) {
  var regex = new RegExp('(^| )' + className + '( |$)');
  var node = node||document.body;
  var elements = node.getElementsByTagName("*");
  var results = [];

  for (var i=0; i < elements.length; i++) {
    if (regex.test(elements[i].className)) {
      results.push(elements[i]);
    }
  }
  return results;

While this function is only 12 lines of code, that's still 12 lines that you have to write, debug, and maintain. If you wanted to query by tags and classes, you'd have to add in an additional parameter to provide the tag name and pass it into the getElementsByTagName function. If you wanted to do anything else, you'd get to write and maintain that logic, too. That's all in addition to the fact that there's probably a corner case or two in which the above function might not work all of the time on all browsers, and that regular expression that may not be intuitively obvious.

Fortunately, dojo.query makes rolling your own query functions a thing of the past. Here's the API that provides universal querying:

dojo.query(/*String*/ query, /*String?|DOMNode?*/ root) //Returns NodeList

Although you won't be formally introduced to NodeList for a few more pages, all you really need to know at the moment is that a NodeList is a subclass of Array that has some specialized extensions for manipulating nodes.

To accomplish the previous getElementsByClassName example via query, just pass in a CSS selector for a class name, like so:

dojo.query(".someClassName")

Querying for a tag type like a DIV and a class name is just as easy; you just update the selector with the additional CSS syntax:

dojo.query("div.someClass")

Starting to see the beauty in using a quick one liner to query the DOM using a uniform syntax? You'll like it even better as you keep reading. First, however, take a look at Table 5-1 to get a feel for the wide range of common operations you can accomplish with query. See http://www.w3.org/TR/css3-selectors/ for the definitive reference on CSS selectors.

Table 5-1. Commonly used CSS selectors

Syntax

Meaning

Example

*

Any element

dojo.query("*")

E

Elements of type E

dojo.query("div")

.C

Elements with class C

dojo.query(".baz")

E.C

Elements of type E having class C

dojo.query("div.baz")

#ID

Element with ID ID

dojo.query("#quux")

E#ID

Element of type E with ID ID

dojo.query("div#quux)

[A]

Elements with attribute A

dojo.query("[foo]")

E[A]

Elements of type E with attribute A

dojo.query("div[foo]")

[A="V"]

Elements with attribute A having value "V"

dojo.query("[foo='bar']")

E[A˜='V']

Elements of type E having a list of space separated attributes, one of which is exactly equal to "V"

dojo.query("div[foo˜='bar']")

E[A^='V']

Elements of type E having an attribute that begins with "V"

dojo.query("div[foo^='bar']")

E[A$='V']

Elements of type E having an attribute that ends with "V"

dojo.query("div[foo$='bar']")

E[A*='V']

Elements of type E having an attribute that contains the substring "V"

dojo.query("div[foo*='bar']")

,

Boolean OR

dojo.query("div,span.baz")

E > F

Element F is a child of element E

dojo.query("div > span")

E F

Element F is an arbitrary descendant of element E

dojo.query("E F")

Warm Up

Let's warm up to dojo.query with a page containing some simple markup as part of a storybook structure. For brevity, only one full scene is included:

<div id="introduction" class="intro">
    <p>
        Once upon a time, long ago...
    </p>
</div>

<div id="scene1" class="scene">...</div>

<div id="scene2" class="scene">
    <p>
        At the table in the <span class="place">kitchen</span>, there were three
bowls of <span class="food">porridge</span>. <span class="person">Goldilocks</span>
was hungry. She tasted the <span class="food">porridge</span> from the first bowl.
    </p>
    <p>
        "This <span class="food">porridge</span> is too hot!" she exclaimed.
    </p>

    <p>
        So, she tasted the <span class="food">porridge</span> from the second bowl.
    </p>

    <p>
        "This <span class="food">porridge</span> is too cold," she said
    </p>
    <p>
        So, she tasted the last bowl of <span class="food">porridge</span>.
    </p>

    <p>
        "Ahhh, this <span class="food">porridge</span> is just right," she said
happily and she ate it all up.
    </p>
</div>

<div id="scene3" class="scene">...</div>

As was demonstrated in our earlier example, getElementsByTagName returns an array of DOM nodes for a given type. The dojo.query equivalent is to simply provide the tag name as the argument string; so, in order to query a page for all of the div elements, you'd simply use dojo.query("div"), like so:

dojo.query("div")
//Returns [div#introduction.intro, div#scene1.scene, div#scene2.scene,
//div#scene3.scene]

Note that if you want to query against only the children of a particular node instead of the entire page, you can specify a second argument to query using that second argument as the root of the tree. For example, to query only scene2 for paragraph elements instead of the entire page for paragraph elements, provide the second parameter as a node or the id of a node, like so:

dojo.query("p", "scene2")
//Returns [p, p, p, p, p, p]

Querying a page for elements of a specific class is just as simple; just indicate the class you're looking for using CSS query syntax, which, according to the specification, means prefixing the class name with a leading dot. For example, you could query all of the elements that currently have the food class applied to them, like so:

dojo.query(".food")
//Returns [span.food, span.food, span.food, span.food, span.food,
//span.food, span.food]

Base's addClass and removeClass functions do not expect a leading dot to identify class names and won't return the correct results if you include it. This can be easy to forget when you're just starting out with the toolkit.

Combining the ability to query by tag and class is just as easy: combine the two constructs. Consider the case of wanting to query for span elements that have the place class applied:

dojo.query("span.place")
//Returns [span.place]

Selecting a class is handy and all, but there are plenty of times when you'll want to select more than one class. Fortunately, you can accomplish this task using the same simple approach that you've already grown to love. For example, you could select all of the elements having food and place applied thusly:

dojo.query(".food,.place")
//Returns [span.food, span.food, span.food, span.food, span.food, span.food,
//span.food, span.place]

Parts of a CSS expression that are separated by a comma all stand on their own. They are not left-associative like some mathematical operators or parts of grammar.

As a final example of the versatility of query, consider the case of finding descendants of a particular node. For our story, let's say that you want to find all of the nodes with the food class applied that are a descendant of scene2 :

dojo.query("#scene2 .food")
//Returns [span.food, span.food, span.food, span.food, span.food, span.food,
//span.food]

Note that the child combinator using the > operator would have returned an empty list because there are no nodes reflecting the food class that are direct children of scene2 :

dojo.query("#scene2 > .food")
//Returns []

A common problem is confusing the child combinator (>) with the descendant combinator (a space). The combinator operator returns immediate child nodes while the descendant operator returns descendants that appear anywhere in the DOM hierarchy.

Although this example was necessarily brief, a final word worth mentioning is that reducing the search space as much as possible by providing the most specific query that you can has a significant impact on performance.

State Tracking Example

In addition to the obvious case of finding nodes in the DOM, a powerful facility like dojo.query tends to change the way you solve a lot of common problems because it expands the creative possibilities. As a simple illustration, consider the problem of tracking state in an application, a very common piece of any reasonably complex application's design. Perhaps it involves determining whether a particular section of text is highlighted or not, or perhaps it involves knowing whether some action has already been triggered. While you could introduce explicit variables to track every facet of state, using CSS classes to track state often provides a much more elegant solution to the problem.

For example, suppose that you're developing a cutting-edge new search engine for the web that is capable of tagging entities in the document, and that you've indicated that you'd like to explicitly view people in your search results. Let's assume that your search results contained Shakespeare's play Macbeth, and that you had requested that "people" be tagged in it. You might get the following results:

...
<a rel="person">First Witch</a>
When shall we three meet again
In thunder, lightning, or in rain?

<a rel="person">Second Witch</a>
When the hurlyburly's done,
When the battle's lost and won.

<a rel="person">Third Witch</a>
That will be ere the set of sun.

<a rel="person">First Witch</a>
Where the place?

<a rel="person">Second Witch</a>
Upon the heath.

<a rel="person">Third Witch</a>
There to meet with <a rel="person">Macbeth</a>.

...

The long, brittle way

As a developer who has a soft spot for usability, you might want to include a small control panel on the side of the page that toggles highlighting particular entity types in the search results. A low-level JavaScript approach in which you directly manipulate the DOM yourself might look something like the following:

function addHighlighting(entityType) {
  var nodes  = document.getElementsByTagName("a");
  for (var i=0; i < nodes.length; i++) {
    if (nodes[i].getAttribute('rel')==entityType) {
      nodes[i].className="highlighted";
    }
  }
}

function removeHighlighting(entityType) {
  var nodes = document.getElementByTagName("a");
  for (var i=0; i < nodes.length; i++) {
    if (nodes[i].getAttribute('rel')==entityType) {
      nodes[i].className="";
    }
  }
}

That sort of gets the job done, but it's still a little bit naïve to assume the search results won't ever have any other class associated with them than the highlighted class—because if they did, we'd be directly clobbering it in each of our functions. Thus, we'd also need to engineer some functions for adding and removing classes from nodes that may have multiple classes applied, which would involve a more robust effort requiring us to search over the string value for className and optionally add or remove a class's name. You could use Base's addClass and removeClass functions that you learned about in Chapter 2 to prevent any more cruft from appearing, but that still doesn't minimize the existing cruft.

The short, robust way

Here's the way you could safely attack the problem with query, cruft-free:

function addHighlighting(entityType) {
  dojo.query("span[type="+entityType+"]").addClass("highlighted");
}

function removeHighlighting(entityType) {
  dojo.query("span[type="+entityType+"]").removeClass("highlighted");
}

For this particular example, you rid yourself of low-level DOM manipulation, writing a for loop, and introducing a conditional logic block in exchange for some elegant CSS syntax—and that's not to overlook the assumption about there not being more than one class applied to the entities in the search results document.

While there isn't anything dojo.query can do for you that you can't do the long way around, hopefully the previous discussion illustrated that dojo.query does provide a single, uniform interface for finding and manipulating elements in the DOM at a very high level and that the additional complexity lies in the query string versus additional conditional logic statements. Not to mention that it's a little less awkward than manipulating the DOM at such a low level in the first place.

If you think there are a lot of cool things you can do with query, just wait until you see the flexibility that NodeList offers. It's the return type from a call to query and is coming up next.

NodeList

A NodeList is a specialized subclass of Array that is expressly designed with some fantastic extensions for manipulating collections of DOM nodes with ease. One of the more seductive features of a NodeList is its ability to provide chaining via the dot operator, although many specialized capabilities such as mapping, filtering, and looking up the index of a node exist as well.

Table 5-2 provides an overview of the NodeList methods available. These methods are named according to the very same convention as Base's Array functions. The only caveats are that they return NodeLists instead of Arrays.

For a review of the fundamentals involving the following Array manipulations, see the section "Array Processing" in Chapter 2.

Table 5-2. NodeList methods

Name

Comment

[a]

indexOf(/*DOMNode*/n)

Returns the first location of an item in the NodeList.

lastIndexOf(/*DOMNode*/n)

Returns the last location of an item in the NodeList.

every(/*Function*/f)

Returns true if the function returns true for every item in the NodeList.

some(/*Function*/f)

Returns true if the function returns true for at least one item in the NodeList.

forEach(/*Function*/f)

Runs each item through a function and returns the original NodeList.

map(/*Function*/f)

Runs each item through a function and returns the results as a NodeList.

filter(/*Function*/f)

Runs each item through a NodeList, returning only the items that meet the function criteria, or applies CSS query filtering to the list of nodes.

concat(/*Any*/item, ...)

Returns a new NodeList with the new items appended, behaving just like the Array.concat method except that it returns a NodeList.

splice(/*Integer*/index, /*Integer*/howManyToDelete, /*Any*/item, ...)

Returns a new NodeList with the new items inserted or deleted, behaving just like the Array.splice method except that it returns a NodeList.

slice(/*Integer*/begin, /*Integer*/end)

Returns a new NodeList with the new items sliced out, behaving just like the Array.slice method except that it returns a NodeList.

addClass(/*String*/class)

Adds a class to every node.

removeClass(/*String*/class)

Removes a class from every node.

style(/*String|Object*/style)

Gets or sets a particular style to every node when style is a String. Works just like dojo.style to set multiple style values if style is an Object.

addContent(/*String*/ content, /*String?|Integer?*/ position)

Adds a text string or node to the relative position indicated for each node. Valid values for position include first, last, before, and after. Position values first and last are a function of the node's parent, while before and after are relative to the node itself.

place(/*String|Node*/ queryOrNode, /*String*/ position)

Places each item in the list relative to node, or to the first item matched by the query criteria. Valid values for position are the same as with method addContent (see above).

coords( )

Returns the box objects for all elements in the list as an Array—not as a NodeList. Box objects are of the form { l: 50, t: 200, w: 300: h: 150, x: 100, y: 300 }, where l specifies the offset from the left of the screen, t specifies an offset from the top of the screen, w and h correspond to the width and height of the box, and x and y provide the absolute position of the cords.

orphan/*String?*/ filter

Removes DOM nodes from the list according to the filter criteria and returns them as a new NodeList.

adopt(/*String|Array|DomNode*/ queryOrListOrNode, /*String?*/ position)

Inserts DOM nodes relative to the first element of the list.

connect(/*String*/ methodNameOrDomEvent, /*Object*/ context, /*String*/ funcName)

Attaches event handlers to every item in the NodeList, using dojo.connect so event properties are normalized internally. The signature is just like dojo.connect in that you provide a method name or DOM event for connecting along with an optional context and function name. DOM event names should be normalized to all lowercase. For most use cases, you will instead use the shortcuts discussed later in this chapter in "Dom Event Shortcuts."

instantiate(/*String|Object*/declaredClass, /*Object?*/properties)

Handy for instantiating widgets in bulk.[a] Assuming the NodeList contains a number of arbitrary source nodes, this method tries to parse them into the widget class defined as declaredClass, passing in any widget properties provided in properties.

[a] Widgets are not formally introduced until Chapter 11; consequently, no examples in this chapter demonstrate usage of instantiate.

Array-Like Methods

As you may recall, there are several functions available for manipulating arrays that are included in Base. You'll be pleased to know that many of these same methods are available to NodeList. In particular, indexOf, lastIndexOf, every, some, forEach, map, and filter work just like the corresponding functions for an array—although NodeList 's filter function offers some additional features depending on the parameter passed. (More on that shortly.)

To get started, we'll need to create ourselves a NodeList. You can use the same syntax as you would with an array, which explicitly provides some elements to the NodeList, or you can also use a NodeList 's built-in concat method to create a NodeList from an existing Array object.

Here are a few of the possible ways to construct a new NodeList:

var nl = new dojo.NodeList(  ); //create an empty NodeList

var nl = new dojo.NodeList(foo, bar, baz);
//create a NodeList with some existing nodes

var a = [foo, bar, baz];
// suppose there is an existing Array object with some nodes in it

a = nl.concat(a); //turn the  Array into a NodeList

If you create a NodeList with the following approach, you may not end up with what you expect:

var nl = new dojo.NodeList([foo, bar, baz]);

The previous line of code returns a NodeList that contains an Array object with three numbers in it—this is the exact same result you'd get as a result of new Array([foo,bar,baz]).

Chaining NodeList results

While Dojo's array methods are extremely useful if you don't need to stream in the results of a previous operation into another operation, or if you need to strictly deal with an Array, you may otherwise find NodeList s to be your new data structure of choice because the syntax is quite elegant. The following example illustrates chaining together some operations:

var nl = new dojo.NodeList(node1,node2,node3,node4,...);

nl.map(
  /* Map some elements... */
  function(x) {
    /* ... */
  }
)
.filter(
  /* And now filter them... */
  function f(x) {
    /* ... */
  }
)
.forEach(
  function(x) {
    /* Now do something with them... */
  }
);

Had we used the standard Dojo functions to accomplish this same workflow, take a look at the clutter that would have been introduced by way of intermediate state variables:

var a0 = new Array(node1,node2,node3,node4,...);

/* Map some elements... */
var a1 = dojo.map(a0,
  function(x) {
    /* ... */
  }
);

/* And now filter... */
var a2 = dojo.filter(a1
  function f(x) {
    /* ... */
  }
);

/* Now do something with them... */
dojo.forEach(a2
  function f(x) {
    /* ... */
  }
);

Be advised that although chaining together the results of operations via the dot operator can produce really elegant code, the lack of intermediate state variables can also have a significant impact on your ability to debug and maintain an application. As always, use discretion.

String-as-Function style Arguments

Just like Base's methods for manipulating Array s, you can use the special index, array, and item identifiers if you choose to use String arguments as described in the section "Array Processing" in Chapter 2. To recap, consider the following example:

//Suppose you have an existing NodeList called nl...

//Use the item identifier instead of writing out the entire function wrapper
nl.forEach("console.log(item)");

Enhanced filtering

In addition to NodeList 's filter method, which provides the traditional array-like capabilities like dojo.filter, NodeList also provides CSS query-style filtering when you pass in a String parameter. For example, the previous code block illustrated passing a function into NodeList to operate on each individual piece of data. The following block of code uses CSS query syntax to filter an actual list of DOM nodes by the query string:

dojo.query("div")
.forEach(
  /* Print out all divs */
  function f(x) {
    console.log(x);
  })
.filter(".div2") //filter on a specific class and print again.
.forEach(
  /*Now, print only div.div2 divs*/
  function f(x) {
    console.log(x);
   }
  });

Style

Given that you can use CSS query syntax to fetch a list of nodes, it seems entirely possible that you may want to perform style operations on them. For this very reason, NodeList includes a few methods to help you get the job done. NodeList 's style method is especially noteworthy in that it can act as a getter or as a setter depending upon whether you provide a second parameter. This behavior is just like the dojo.style function.

As a reminder of how dojo.style works, recall that dojo.style(someNode, "margin") would return the margin value of a DOM node, while dojo.style(someNode, "margin", "10px") would set the node's margin to a value of 10 pixels.

Manipulating a NodeList is just the same except that there's no need for an explicit first parameter that denotes a particular node anymore. Like any other NodeList function that processed nodes, the method is applied to each node in the list:

// dojo.style approach...
var a = [];

/* load the Array with some nodes */

// iterate over the nodes and apply style
dojo.forEach(a, function(x) {
  dojo.style(x, "margin", "10px");
});

//NodeList approach...
dojo.query( /* some query */ )
.style("margin", "10px");

NodeList also includes methods for adding and removing classes via addClass and removeClass —again, just like the corresponding dojo.addClass and dojo.removeClass functions. That is, you can manually set style properties for elements via style, or explicitly add or remove classes via addClass and removeClass. Note that the style method is especially useful when you don't actually have an existing class that accomplishes the purpose, whereas the addClass and removeClass methods are useful for those times when you already have classes that you want to toggle on or off. Just like style, the syntax is for these methods is predictable:

dojo.query("span.foo", someDomNode).addClass("foo").removeClass("bar");
dojo.query("#bar").style("color","green");

Placement

Not surprisingly, a few methods for manipulating the placement of nodes on the page are included as methods of NodeList. You may recognize the coords method, which, like its dojo counterpart, returns an Array containing the coordinate objects for each node in the list. Likewise, NodeList 's place method is similar to dojo.place in that it provides a way to insert the entire NodeList into the DOM in a sequential fashion based on a specific position.

The addContent method, however, is a method that doesn't have a corresponding counterpart elsewhere in the toolkit; it provides a way to add a node or text string to a relative position for each item in a NodeList.

Here's an example of using addContent to insert a text string (which gets wrapped as an inline span) after each page container. This particular example might be useful a method for an application in which you have various displays involving tab and stack containers:

/* Add a footer message after each container identifed by the pageContainer class*/
var nl = dojo.query("div.pageContainer").addContent("footer goes here!", "after");

Recalling that the place method functions by inserting the entire NodeList into the page relative to another node, you might do the following to insert the entire list inside of a container node identified by an id value of debugPane :

var nl = dojo.query("div.someDebugNodes").place("#debugPane", "last");

dojo.coords, like its counterpart, returns an object of key/value pairs that represent the coordinates for each item in the NodeList. Recall that the coords object includes keys for top and left offsets, length and height, and absolute x and y positions, which can be transformed to be relative to a viewport.

The result of coords is an Array, not a NodeList. Inspect the output of the following blurb in the Firebug console and see for yourself:

dojo.forEach(
  dojo.query("div").coords(  ),
  function(x) { console.log(x); }
);

A somewhat unique method provided by NodeList for placement that does not have a dojo counterpart is its orphan method, which applies a simple filter (single CSS selector—no commas allowed) to each of its elements, and each child element involved in a relationship that matches the filter criteria is removed from the DOM. These child elements that have been removed—or orphaned—are then returned as a new NodeList. The orphan method is often used to remove nodes from the DOM in a much less kludgy manner than the DOM accessor functions otherwise dictate, which is the following pattern for a node called foo : foo.parentNode.removeChild(foo).

For example, to remove all hyperlink elements that are children of a span from the DOM and return them as a new NodeList, you'd do the following:

var nl = dojo.query("span > a").orphan(  )

The > selector is whitespace-sensitive; you must include a whitespace on each side of the selector.

The adopt method is essentially the inverse of the orphan operator in that it allows you to insert elements back into the DOM. The function is quite flexible, allowing you to pass in a particular DOM node, a query string, or a NodeList. The nodes that will be inserted are positioned relative to the first element in the NodeList that provides the adopt method. The second parameter providing positional information allows for the usual positional information (first, last, after, and before ):

var n = document.createElement("div");
n.innerHTML="foo";
dojo.query("#bar").adopt(n, "last");

DOM Event Shortcuts

Given that you can do just about everything else with a NodeList, you probably won't be too surprised to find out that you can also batch process nodes to respond to particular DOM events such as blurs, mouse movements, and key presses. Firing custom actions in response to one or more DOM events is such a common occurrence that NodeList provides a built-in method for accomplishing this task with ease.

The following DOM events are offered as events for batch processing with NodeList s:

  • onmouseover

  • onmouseenter

  • onmousedown

  • onmouseup

  • onmouseleave

  • onmouseout

  • onmousemove

  • onfocus

  • onclick

  • onkeydown

  • onkeyup

  • onkeypress

  • onblur

As an example, consider the use case of capturing mouse movement events over a particular element. You'd simply fill in the function for the onmouseover function like so:

dojo.query("#foobar").onmousemove(
  function(evt) {
    console.log(evt); // you should really do something more interesting!
  }
);

The event objects that are available via the DOM Event methods are standardized, because internally dojo.connect is being used. The event model as provided via dojo.connect is standardized in accordance with the W3C specification.

There is no direct way to manage and disconnect the connections you create with NodeList 's connect method, although a future 1.x dot release may provide that ability. If it's not enough to have these connections automatically torn down when the page unloads, you can opt to use the normal dojo.connect method inside of a NodeList 's forEach method if you have a really good reason to manage the connections yourself.

For example, if you needed to manually manage the connections from the previous example, you might do it like so:

var handles =
  dojo.query("a").map(function(x) {
    return dojo.connect(x, "onclick",
      function(evt) { /* ... */ });
  });

/* Sometime later... */
dojo.forEach(handles, function(x) {
  dojo.disconnect(x);
});

Animation

You may want to skim this section and then read it again more closely after you've read Chapter 8, which provides complete coverage of animating content.

Producing animations with DHTML has often been perceived as a bit cumbersome—and it certainly can be. NodeList, however, makes this task just as simple as anything else you can do with a NodeList. From an application development standpoint, that means that you can perform fades trivially, and can even perform more complex operations via the _Animation.animateProperty method.

The _Animation that is operated upon has a leading underscore. In this particular context, the leading underscore signifies that the API is not final and, in general, _Animation objects should be treated somewhat opaquely. While the information presented in this section is current as of Dojo 1.1 and the _Animation API is fairly stable, future versions of Dojo could change it.

The methods listed in Table 5-3 involving animation are currently available, but must be explicitly retrieved via a call to dojo.require("dojo.NodeList-fx"). Each of these methods takes an associative array of key/value pairs that provide properties such as the animation duration, position information, colors, etc.

Table 5-3. NodeList extensions for animation

fadeIn

Fades in each node in the list.

fadeout

Fades out each node in the list.

wipeIn

Wipes in each element in the list.

wipeOut

Wipes out each element in the list.

slideTo

Slides each element in the list to a particular position.

animateProperties

Animates all elements of the list using the specified properties.

anim

Similar to animateProperties except that it returns an animation that is already playing. See dojo.anim for more details.

As you might already be thinking, animations are fun to toy around with. Dojo makes this so simple to do. Like anything else in the toolkit, you can just open up the Firebug console and start experimenting. You might start out with simple fades, like so:

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

//Once NodeList-fx has loaded...
dojo.query("p").fadeOut(  ).play(  )

Then, when you're ready to begin trying more advanced animations, add some key/value pairs to the associative array and see what happens:

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

//Once NodeList-fx has loaded...
dojo.query("div").animateProperty({
  duration: 5000,
  properties: {
    color: {start: "black", end: "green"}
  }
}).play(  );

Note that the actual result of the various effects method is an _Animation object, and that its play method is the standard mechanism for activating it.

Creating NodeList Extensions

While the built-in methods for NodeList are quite useful, it's not going to be long before you'll find that there's this one method that you could really benefit from having on hand. Fortunately, it takes very little effort to inject your own functionality into NodeList. Consider the following use case accomplished via query that returns the innerHTML for each element of a NodeList :

dojo.query("p").map(function(x) {return x.innerHTML;});

Compared to working up that solution from scratch, you already have a really concise solution, but you could go even further to simplifying matters by using the even more concise String-as-Function syntax with the following improvement:

dojo.query("p").map("return item.innerHTML;"); //Used the special item identifier

That's definitely an improvement—would you believe that your code could still be even more readable and concise ? Consider the following extension to NodeList, in which you embed the mapping inside of a more readable and elegant function call that is very intuitively named so that it's completely obvious exactly what is happening:

//Extend NodeList's prototype with a new function
dojo.extend(dojo.NodeList, {
    innerHTML : function(  ) {
        return this.map("return item.innerHTML");
    }
});


//Call the new function
dojo.query("p").innerHTML(  );

What's great about extending NodeList is that for a very little bit of planning up front, you can significantly declutter your design and make it a lot more maintainable at the same time.

/* ... *
dojo.require("dtdg.ext-dojo.NodeList");

/* ...*/

dojo.query("p").innerHTML(  );

Clearly, you could go as far as to name the resource file NodeList-innerHTML.js if you wanted to be pedantic; do whatever makes you most comfortable, so long as you are consistent.

Behavior

Core contains a lightweight extension that builds on top of query to provide a great way for decoupling events and DOM manipulations from an HTML placeholder via the behavior module. It may not be intuitively obvious at first, but the ability to define behavior for nodes irrespective of the markup itself can lend an immense of flexibility to a design. For example, it allows you to concisely accomplish tasks such as assigning click handlers to all anchor elements without knowing where or how many anchor elements there will be. You use the same CSS selectors you learned about in Table 5-1 to find the nodes for attaching behavior to, so the possibilities are almost endless.

The behavior module currently provides two API calls; the add method allows you to queue up a collection of behaviors, and the apply method actually triggers those behaviors:

dojo.behavior.add(/*Object*/ behaviorObject)
dojo.behavior.apply(  )

Basically, you use add to assign a new behavior to a collection of DOM nodes, but the behavior isn't actually reflected until you call apply. One of the reasons that it's a two-step process is because the pattern of performing multiple add operations before a final apply occurs lends itself to a lot of asynchronous communication patterns, described in Chapter 4.

Chapter 4 introduced a data structure called Deferred that is a staple in Dojo's IO subsystem. Deferred s provide the façade of having a thread available to operate on and lend themselves to successively applying multiple callback and error handling functions. After reading about Deferred patterns, the utility in providing separate functions for add and apply should be apparent.

The Object that you pass into add and apply is quite flexible and can accept a number of variations. In short, the behavior Object contains key/value pairs that map CSS selectors to Object s that supply DOM event handlers. The DOM event handlers themselves come as key/value pairs. Before the example, though, skim over Table 5-4, which provides a summary of the possibilities.

Table 5-4. Behavior Object possibilities

Key

Value

Comment

Selector (String)

Object

The Object should contain key/value pairs that map either DOM event names or the special "found" identifier to event handlers or topic names.

For example:

{
    onclick : function(evt) {/*...*/},
    onmouseover : "/dtdg/foo/moveover",
    found : function(node) {/*...*/},
    found : "/dtdg/bar/found"
}

In the case of a topic being published, the standardized event object is passed along for the subscribe handler to receive.

In the case of an event handler, the standardized event object is passed into the function.

In the case of the special "found" identifier, the matching node itself is either passed into the handler or passed along with the topic that is published.

Selector (String)

Function

For each node matching the selector, the handler is executed with each node passed in as the parameter.

Selector (String)

String

For each node matching the selector, the topic name is published. The node itself is passed along for the subscribe handler to receive.

Remember to provide the keys to the behavior Objects as actual String values whenever the CSS selector requires it. For example, a behavior object of {div : function(evt) {/*...*/} is fine whereas {#foo : "/dtdg/foo/topic"} would not be valid because #foo is not a valid identifier.

Take a moment to read through Example 5-1, which illustrates some of the possibilities as a working example.

Example 5-1. Example of dojo.behavior at work
<html>
     <head>
         <title>Fun with Behavior!</title>

         <link rel="stylesheet" type="text/css"
             href="http://o.aolcdn.com/dojo/1.1/dojo/resources/dojo.css" />
         <script
             type="text/javascript"
             src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js"
             djConfig="isDebug:true"
         ></script>

         <script type="text/javascript">
             dojo.require("dojo.behavior");

             dojo.addOnLoad(function(  ) {
                     /* Pass a behavior Object into dojo.behavior.
                    This object is automatically added once the page loads*/
                     dojo.behavior.add({

                         /* The behavior Object is keyed by any combination of CSS
                        selectors, which can map to a single behavior or a
collection of
                        behaviors */

                         /* Mapping a key to a function is equivalent to mapping
to {found
                         : function(node) { ... } } */
                         ".container" : function(node) {
                            //apply some generic styling

                            dojo.style(node, {
                                border : "solid 1px",
                                background : "#eee"
                            });
                         },

                        /* Map the key to a collection of behaviors */
                        "#list > li" : {
                            /* DOM events work just like dojo.connect, allowing
you to act
                            on the event */
                            onmouseover : function(evt) {dojo.style(evt.target,
                            "background", "yellow");},
                            onmouseout : function(evt) {dojo.style(evt.target,
                            "background", "");},

                            /* String values are published as topics */
                            onclick : "/dtdg/behavior/example/click",

                            /* "found" is a general purpose handler that allows
                             manipulation of the node*/
                            found : function(node) {dojo.style(node, "cursor",
"pointer")}
                        }
                    });

                    /* Somewhere, out there...a subscription is set up... */
                    dojo.subscribe("/dtdg/behavior/example/click", function(evt) {
                        console.log(evt.target.innerHTML, "was clicked");
                    });
            });
         </script>
     <head>

         <body>
             <div class="container" style="width:300px">
                 Grocery List:
                 <ul id="list">
                    <li>Bananas</li>
                    <li>Milk</li>
                    <li>Eggs</li>
                    <li>Orange Juice</li>
                    <li>Frozen Pizzas</li>
                </ul>
            </div>
        </body>
 </html>

As the example demonstrates, any behavior you set up before the page loads is set up automatically. After the page loads, however, you need to first add the behavior and then apply it. The following update adds another click handler to list elements:

dojo.behavior.add({
  "#list > li" : {
    onclick : "/dtdg/behavior/example/another/click"
  }

});
dojo.behavior.apply(  );

dojo.subscribe("/dtdg/behavior/example/another/click", function(evt) {
  console.log("an additional event handler...");

});

Although one of the key observations you should be making is how decoupled the actual behavior of the nodes are from the markup itself, you hopefully just made the connection that behavior 's apply function provides you with a great benefit: any behavior you supply on top of existing behavior is added along with the existing behavior. In other words, new behavior doesn't just blow away what was there before; you are able to add behavior in layers and the book keeping is handled without any additional intervention on your behalf.

Summary

After reading this chapter, you should:

  • Be able to use dojo.query to universally find nodes in the page

  • Have a basic understanding of CSS selector syntax

  • Be familiar with NodeList s and recognize the various mappings that hold to other functions such as the Array utilities that the toolkit offers

  • Be able to chain together the results from NodeList methods to cleanly and rapidly process DOM elements

  • Be aware that it's possible to hack NodeList and instead opt to use other utilities in the toolkit

  • Be able to use NodeList s to place DOM nodes, handle animations, set up connections, and manage style

  • Understand the value in extending NodeList with custom operations so as to minimize the effort in processing the results from dojo.query

  • Be aware of the benefits from decoupling DOM events from an HTML placeholder and how you can achieve this via the behavior module

A discussion of internationalization is coming up next.