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

Tip

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]

Warning

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]

Tip

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 []

Warning

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.

Get Dojo: The Definitive Guide now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.