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, “Commonly used CSS selectors” 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 |
|
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, Language and Browser Utilities 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, “NodeList methods” 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.
Tip
For a review of the fundamentals involving the following Array manipulations, see the section "Array Processing" in Chapter 2, Language and Browser Utilities.
Table 5.2. NodeList methods
|
Name |
Comment |
|---|---|
|
[a] | |
|
|
Returns the first
location of an item in the |
|
|
Returns the last
location of an item in the |
|
|
Returns true if the
function returns true for every item in the |
|
|
Returns true if the
function returns true for at least one item in the |
|
|
Runs each item through
a function and returns the original |
|
|
Runs each item through
a function and returns the results as a |
|
|
Runs each item through
a |
|
|
Returns a new |
|
|
Returns a new |
|
|
Returns a new |
|
|
Adds a class to every node. |
|
|
Removes a class from every node. |
|
|
Gets or sets a
particular style to every node when style is a |
|
|
Adds a text string or
node to the relative position indicated for each node. Valid
values for position include |
|
|
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 |
|
|
Returns the box objects
for all elements in the list as an Array—not as a |
|
|
Removes DOM nodes from
the list according to the filter criteria and returns them as
a new |
|
|
Inserts DOM nodes relative to the first element of the list. |
|
|
Attaches event handlers
to every item in the |
|
|
Handy for instantiating
widgets in bulk.[a] Assuming
the |
[a] Widgets are not formally introduced until Chapter 11, Dijit Overview; consequently, no examples in
this chapter demonstrate usage of | |
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
Warning
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:
vara0= new Array(node1,node2,node3,node4,...); /* Map some elements... */ vara1= dojo.map(a0, function(x) { /* ... */ } ); /* And now filter... */ vara2= dojo.filter(a1function f(x) { /* ... */ } ); /* Now do something with them... */ dojo.forEach(a2function f(x) { /* ... */ } );
Warning
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, Language and Browser Utilities. 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.
Warning
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( )Warning
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:
onmouseoveronmouseenteronmousedownonmouseuponmouseleaveonmouseoutonmousemoveonfocusonclickonkeydownonkeyuponkeypressonblur
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
Tip
You may want to skim this section and then read it again more closely after you've read Chapter 8, Animation and Special Effects, 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.
Warning
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, “NodeList extensions for animation” 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
|
|
Fades in each node in the list. |
|
|
Fades out each node in the list. |
|
|
Wipes in each element in the list. |
|
|
Wipes out each element in the list. |
|
|
Slides each element in the list to a particular position. |
|
|
Animates all elements of the list using the specified properties. |
|
|
Similar to |
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 identifierThat'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.
The recommended practice for modularizing up this kind of
extension is to create a submodule called
ext-dojo with a resource file called
NodeList.js inside of it so that you end up with
a dojo.require statement that is
crystal clear to whoever ends up reading your code. In the end, you
have a situation that's a win for everyone. Your final usage of the
extension might end up looking like the following example once you're
all finished with it:
/* ... *
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, “Commonly used CSS selectors” 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, AJAX and Server Communication.
Tip
Chapter 4, AJAX and Server Communication 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, “Behavior Object possibilities”, which provides a summary
of the possibilities.
Table 5.4. Behavior Object possibilities
|
Key |
Value |
Comment |
|---|---|---|
|
|
| The 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 |
|
|
|
For each node matching the selector, the handler is executed with each node passed in as the parameter. |
|
|
|
For each node matching the selector, the topic name is published. The node itself is passed along for the subscribe handler to receive. |
Tip
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, “Example of dojo.behavior at work”, 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.queryto universally find nodes in the pageHave a basic understanding of CSS selector syntax
Be familiar with
NodeLists and recognize the various mappings that hold to other functions such as the Array utilities that the toolkit offersBe able to chain together the results from
NodeListmethods to cleanly and rapidly process DOM elementsBe aware that it's possible to hack
NodeListand instead opt to use other utilities in the toolkitBe able to use
NodeLists to place DOM nodes, handle animations, set up connections, and manage styleUnderstand the value in extending
NodeListwith custom operations so as to minimize the effort in processing the results fromdojo.queryBe aware of the benefits from decoupling DOM events from an HTML placeholder and how you can achieve this via the
behaviormodule
A discussion of internationalization is coming up next.





Add a comment



Add a comment