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.
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 |
|
| Elements of type E |
|
| Elements with class C |
|
| Elements of type E having class C |
|
| Element with ID
|
|
| Element of type E with
ID |
|
| Elements with attribute A |
|
| Elements of type E with attribute A |
|
| Elements with attribute A having value "V" |
|
| Elements of type E having a list of space separated attributes, one of which is exactly equal to "V" |
|
| Elements of type E having an attribute that begins with "V" |
|
| Elements of type E having an attribute that ends with "V" |
|
| Elements of type E having an attribute that contains the substring "V" |
|
| Boolean OR |
|
| Element F is a child of element E |
|
| Element F is an arbitrary descendant of element E |
|
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.
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>
. ...
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.
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.