Article
DHTML Utopia: Modern Web Design Using JavaScript & DOM
Chapter 2. The Document Object Model
One day someone came in and observed, on the paper sticking out of one of the Teletypes, displayed in magnificent isolation, this ominous phrase:
values of:
will give rise to dom!
...the phrase itself was just so striking! Utterly meaningless, but it looks like what... a warning? What is "dom?"
--Dennis M. Richie
A Web page is a document. To see that document, you can either display it in the browser window, or you can look at the HTML source. It's the same document in both cases. The World Wide Web Consortium's Document Object Model (DOM) provides another way to look at that same document. It describes the document content as a set of objects that a JavaScript program can see. Naturally, this is very useful for DHTML pages on which a lot of scripting occurs. (The quote above is a pure coincidence—it's from the days before the Web!)
According to the World Wide Web Consortium, "the Document Object Model is a platform- and language-neutral interface that will allow programs and scripts to dynamically access and update the content, structure and style of documents. The document can be further processed and the results of that processing can be incorporated back into the presented page." This statement basically says that the DOM is not just a novelty—it is useful for doing things. In the coming pages, we'll take a brief look at the history of the DOM before investigating more deeply what it is and how we can use it. We'll finish up with some example scripts that demonstrate the power of this critical aspect of DHTML.
The Origins of the DOM
In Netscape Navigator 2, Netscape Communications introduced JavaScript (briefly called LiveScript), which gave Web developers scripting access to elements in their Web pages—first to forms, then, later, to images, links, and other features. Microsoft implemented JavaScript in Internet Explorer 3 (although they called it JScript) in order to keep up with Netscape.
By version 4, the two browsers had diverged significantly in terms of their respective feature sets and the access they provided to page content. Each browser manufacturer implemented its own proprietary means of providing scripting access to layers. Scripts that wanted to work in both browsers needed to contain code for each method. The ill-fated "browser wars" were all about these proprietary extensions to the Web, as each manufacturer strove to attract more developers to its platform through the lure of new features. There was little regard for cross-browser compatibility, although Microsoft copied and supported most of the early innovations made by Netscape.
While all this was taking place, the W3C developed a specification for the Document Object Model Level 1, which outlined a generic and standard method to access the various parts of an XML document using script. Since HTML can be thought of as a dialect of XML, the DOM Level 1 spec applied to HTML as well.
Both major browser manufacturers implemented the DOM Level 1 specification: in Internet Explorer 5 and in Netscape 6. The previously existing proprietary specifications were retrospectively titled; since the new standard was DOM Level 1, those old and now deprecated methods were called DOM Level 0. (Since then, the W3C has also released the DOM Level 2 and DOM Level 3 specifications, which add more features and are broken into separate modules.) There's no formal DOM Level 0 standard, though.
What is the DOM?
So, you know what the DOM used to be. Now let's discuss what it is.
Essentially, the DOM provides access to the structure of an HTML page by mapping the elements in that page to a tree of nodes. Each element becomes an element node, and each bit of text becomes a text node. Take this HTML snippet, for example:
<body>
<p>
This is a paragraph, containing
<a href="#">
a link
</a>
in the middle.
</p>
<ul>
<li>
This item has
<em>
some emphasized text
</em>
in it.
</li>
<li>
This is another list item.
</li>
</ul>
</body>
I added lots of extra indenting so that you can compare this snippet with the matching DOM tree. Don't do that in real life—I'm just trying to make things clearer in this case. The matching DOM tree is shown in Figure 2.1.
As you can see, the a element, which is located inside the p element in the HTML, becomes a child node, or just child, of the p node in the DOM tree. (Symmetrically, the p node is the parent of the a node. The two li nodes, children of the same parent, are called sibling nodes or just siblings.)
Notice that the nesting level of each tag in the HTML markup matches the number of lines it takes to reach the same item in the DOM tree. For example, the <a> tag is nested twice inside other tags (the <p> and <body> tags), so the a node in the tree is located two lines from the top.
Figure 2.1. An example of a DOM tree.

The Importance of Valid HTML
From this last example, we can see more clearly why valid HTML, including properly nested elements, is important. If elements are improperly nested, problems arise. Take the following line:
<strong>These <em>elements are</strong> badly nested</em>.
The DOM tree that results from this incorrectly nested code won't be a tree at all: it would need to be malformed in order to express the invalid element layout that this HTML requests. Each browser fixes malformed content in a different way, which can generate such horrors as an element that is its own parent node. Keeping your HTML valid avoids all these problems.
Walking DOM Trees
Trees of nodes turn up a lot in computing, because, among other things, they have a very useful property: it's easy to "walk the tree" (that is, to iterate through every one of the tree's nodes in order) with very little code. Walking a tree is easy because any element node can be considered as the top of its own little tree. Therefore, to walk through a tree, you can use a series of steps, for example:
- Do something with the node we're looking at
- Does this node have children? If so:
- For each of the child nodes, go to step 1
This process is known as recursion, and is defined as the use of a function that calls itself. Each child is the same type of thing as the parent and can therefore be handled in the same way. We don't do much with recursion ourselves, but we rely quite heavily on the browser recursing through the page's tree. It's especially useful when it comes time to work with events, as we'll see in Chapter 3, Handling DOM Events.
Finding the Top of the Tree
In order to walk the DOM tree, you need a reference to the node at its top: the root node. That "reference" will be a variable that points to the root node. The root node should be available to JavaScript as document.documentElement. Not all browsers support this approach, but fortunately it doesn't matter, because you'll rarely need to walk through an entire document's DOM tree starting from the root. Instead, the approach taken is to use one of the getElementsByWhatever methods to grab a particular part of the tree directly. Those methods start from the window.document object—or document for short.
Getting an Element from the Tree
There are two principal methods that can be used to get a particular element or set of elements. The first method, which is used all the time in DHTML programming, is getElementById. The second is getElementsByTagName. Another method, getElementsByName, is rarely used, so we'll look at the first two only for now.
getElementById
In HTML, any element can have a unique ID. The ID must be specified with the HTML id attribute:
<div id="codesection">
<p id="codepara">
</p>
<ul>
<li><a href="http://www.sitepoint.com/" id="splink"
>SitePoint</a></li>
<li><a href="http://www.yahoo.com/" id="yalink"
>Yahoo!</a></li>
</ul>
</div>
Each non-list element in that snippet has been given an ID. You should be able to spot four of them. IDs must be unique within your document—each element must have a different ID (or no ID at all)—so you can know that a specific ID identifies a given element alone. To get a reference to that element in JavaScript code, use document.getElementById(elementId):
var sitepoint_link = document.getElementById('splink')
Now the variable sitepoint_link contains a reference to the first <a> tag in the above HTML snippet. We'll see a little later what you can do with that element reference. The DOM tree for this snippet of HTML is depicted in Figure 2.2.
Figure 2.2. The snippet's DOM tree.

getElementsByTagName
The document.getElementsByTagName method is used to retrieve all elements of a particular type. The method returns an array that contains all matching elements (Technically, it returns a node collection, but this works just like an array.):
var all_links = document.getElementsByTagName('a');
var sitepoint_link = all_links[0];
The all_links variable contains an array, which contains two elements: a reference to the SitePoint link, and a reference to the Yahoo! link. The elements are returned in the order in which they are found in the HTML, so all_links[0] is the SitePoint link and all_links[1] is the Yahoo! link.
Note that document.getElementsByTagName always returns an array, even if only one matching element was found. Imagine we use the method as follows:
var body_list = document.getElementsByTagName('body');
To get a reference to the sole body element in this case, we would need to use the following:
var body = body_list[0];
We would be very surprised if body_list.length (the array's size) was anything other than 1, since there should be only one <body> tag! We could also shorten the process slightly by replacing the previous two lines with this one:
var body = document.getElementsByTagName('body')[0];
JavaScript allows you to collapse expressions together like this. It can make your code a lot more compact, and save you from declaring a lot of variables which aren't really used for anything.
There is another useful feature; getElementsByTagName is defined on any node at all, not just the document. So, to find all <a> tags in the body of the document, we could use the method like this:
var links_in_body = body.getElementsByTagName('a');
Note that "Element" is plural in this method's name, but singular for getElementById. This is a reminder that the former returns an array of elements, while the latter returns only a single element.
Walking from Parents to Children
Each node has one parent (except the root element) and may have multiple children. You can obtain a reference to a node's parent from its parentNode property; a node's children are found in the node's childNodes property, which is an array. The childNodes array may contain nothing if the node has no children (such nodes are called leaf nodes).
Suppose the variable node points to the ul element of the DOM tree. We can get the node's parent (the div element) like this:
parent = node.parentNode;
We can check if the unordered list has any list items (children) by looking at the length property of the childNodes array:
if (node.childNodes.length == 0) {
alert('no list items found!');
}
If there are any children, their numbering starts at zero. We can obtain the second child in our example HTML (an li element) as follows:
list_item = node.childNodes[1];
For the special case of the first child, located here:
list_item = node.childNodes[0];
we can also use this shorthand:
child = node.firstChild;
Similarly, the last child (in this case, the second li) has its own special property:
child = node.lastChild;
We'll see all these properties used routinely through the rest of this book.
What to do with Elements
Now you know how to get references to elements—the nodes in your HTML page. The core of DHTML—the D-for-dynamic bit—lies in our ability to change those elements, to remove them, and to add new ones. Throughout the rest of this chapter, we'll work with the following code snippet, which we saw earlier:
<div id="codesection">
<p id="codepara">
</p>
<ul>
<li><a href="http://www.sitepoint.com/" id="splink"
>SitePoint</a></li>
<li><a href="http://www.yahoo.com/" id="yalink"
>Yahoo!</a></li>
</ul>
</div>
Changing Element Attributes
Every property of an element, and every CSS style that can be applied to it, can be set from JavaScript. The attributes that can be applied to an element in HTML—for example, the href attribute of an <a> tag—can also be set and read from your scripts, as follows:
// using our sitepoint_link variable from above
sitepoint_link.href = "http://www.google.com/";
Click on that link after the script has run, and you'll be taken to Google rather than SitePoint. The new HTML content, as it exists in the browser's imagination (the HTML file itself hasn't changed), looks like this:
<div id="codesection">
<p id="codepara">
</p>
<ul>
<li><a href="http://www.google.com/" id="splink"
>SitePoint</a></li>
<li><a href="http://www.yahoo.com/" id="yalink"
>Yahoo!</a></li>
</ul>
</div>
Each element has a different set of attributes that can be changed: a elements have the href attribute, <img> elements have the src attribute, and so on. In general, an attribute that can be applied to a tag in your HTML is also gettable and settable as a property on a node from JavaScript. So, if our code contains a reference to an img element, we can change the image that's displayed by altering the img_element.src property. (One notable divergence from this rule is that an element's class attribute in HTML is available in JavaScript as node.className, not node.class. This is because "class" is a JavaScript reserved word.)
The two most useful references that document elements and their supported attributes are those provided by the two major browser makers: the Microsoft DOM reference, and the Mozilla Foundation's DOM reference.
Importantly, though, when we altered our link's href above, all we changed was the destination for the link. The text of the link, which read "SitePoint" before, has not changed; if we need to alter that, we have to do so separately. Changing the text in a page is slightly more complex than changing an attribute; to alter text, you need to understand the concept of text nodes.
Changing Text Nodes
In Figure 2.1 above, you can see how the HTML in a document can be represented as a DOM tree. One of the important things the figure illustrates is that the text inside an element is not part of that element. In fact, the text is in a different node: a child of the element node. If you have a reference to that text node, you can change the text therein using the node's nodeValue property:
myTextNode.nodeValue = "Some text to go in the text node";
How can we get a reference to that text node? We need to walk the DOM tree—after all, we have to know where the text node is before we can alter it. If we consider the sitepoint_link node above, we can see that its childNodes array should contain one node: a text node with a nodeValue of "SitePoint". We can change the value of that text node as follows:
sitepoint_link.childNodes[0].nodeValue = 'Google';
Now, the text displayed on-screen for that link will read Google, which matches the link destination that we changed earlier. We can shorten the code slightly to the following:
sitepoint_link.firstChild.nodeValue = 'Google';
You may recall that a node's firstChild property, and childNodes[0], both refer to the same node; in this case, you can substitute childNodes[0] with success. After this change, the browser will see the following document code:
<div id="codesection">
<p id="codepara">
</p>
<ul>
<li><a href="http://www.google.com/" id="splink"
>Google</a></li>
<li><a href="http://www.yahoo.com/" id="yalink"
>Yahoo!</a></li>
</ul>
</div>
Changing Style Properties
As we have seen, the attributes that are set on an HTML tag are available as properties of the corresponding DOM node. CSS style properties can also be applied to that node through the DOM, using the node's style property. Each CSS property is a property of that style property, with its name slightly transformed: a CSS property in words-and-dashes style becomes a property of style with dashes removed and all words but the first taking an initial capital letter. This is called InterCaps format. Here's an example. A CSS property that was named:
some-css-property
would appear to a script as the following JavaScript property:
someCssProperty
So, to set the CSS property font-family for our sitepoint_link element node, we'd use the following code:
sitepoint_link.style.fontFamily = 'sans-serif';
CSS values in JavaScript are almost always set as strings; some values, such as font-size, are strings because they must contain a dimension, such as "px" or "%". (Internet Explorer will let you get away without using a dimension, as it assumes that a dimensionless number is actually a pixel measurement. However, do not try to take advantage of this assumption; it will break your code in other browsers, and it's in violation of the specification.) Only entirely numeric properties, such as z-index (which is set as node.style.zIndex, as per the above rule) may be set as a number:
sitepoint_link.style.zIndex = 2;
Many designers alter style properties to make an element appear or disappear. In CSS, the display property is used for this: if it's set to none, the element doesn't display in the browser. So, to hide an element from display, we can set its display property to none:
sitepoint_link.style.display = 'none';
To show it again, we give it another valid value:
sitepoint_link.style.display = 'inline';
For a complete reference to the available CSS style properties and what each does, see SitePoint's HTML Utopia: Designing Without Tables Using CSS.
Bigger DOM Tree Changes
The next level of DOM manipulation, above and beyond changing the properties of elements that are already there, is to add and remove elements dynamically. Being able to change the display properties of existing elements, and to read and alter the attributes of those elements, puts a lot of power at your disposal, but the ability to dynamically create or remove parts of a page requires us to leverage a whole new set of techniques.
Moving Elements
To add an element, we must use the appendChild method of the node that will become the added node's parent. In other words, to add your new element as a child of an existing node in the document, we use that node's appendChild method:
// We'll add the link to the end of the paragraph
var para = document.getElementById('codepara');
para.appendChild(sitepoint_link);
After this, our page will look a little odd. Here's the updated HTML code:
<div id="codesection">
<p id="codepara">
<a href="http://www.google.com/" id="splink">Google</a>
</p>
<ul>
<li></li>
<li><a href="http://www.yahoo.com/" id="yalink"
>Yahoo!</a></li>
</ul>
</div>
Another useful thing to know is that, in order to move the node to its new place in the document, we don't have to remove it first. If you use appendChild to insert a node into the document, and that node already exists elsewhere in the document, the node will not be duplicated; instead, it will move from its previous location to the new location at which you've inserted it. We can do the same thing with the Yahoo! link:
para.appendChild(document.getElementById('yalink'));
After this, the page will again be rearranged to match the HTML:
<div id="codesection">
<p id="codepara">
<a href="http://www.google.com/" id="splink">Google</a>
<a href="http://www.yahoo.com/" id="yalink">Yahoo!</a>
</p>
<ul>
<li></li>
<li></li>
</ul>
</div>
Figure 2.3 shows the new DOM tree so far.
Figure 2.3. The DOM tree after changes.

What if you didn't want to add your new (or moved) element to the end of that paragraph? In addition to appendChild, each node has an insertBefore method, which is called with two arguments: the node to insert, and the node before which it will be inserted. To move the Yahoo! link to the beginning of the paragraph, we want to insert it as a child of the paragraph that appears before the Google link. So, to insert the Yahoo! link (the first argument) as a child of the paragraph right before the Google link (sitepoint_link, the second argument), we'd use the following:
para.insertBefore(document.getElementById('yalink'),
sitepoint_link);
Be sure that the second argument (sitepoint_link) really is an existing child node of para, or this method will fail.
Throwing Away Elements
Removing an element is very similar to the process of adding one: again, we use the removeChild method on the element's parent node. Remembering from earlier that we can access a given node's parent as node.parentNode, we can remove our sitepoint_link from the document entirely:
// never hurts to be paranoid: check that our node *has* a parent
if (sitepoint_link.parentNode) {
sitepoint_link.parentNode.removeChild(sitepoint_link);
}
That action will change the HTML code to that shown below:
<div id="codesection">
<p id="codepara">
<a href="http://www.yahoo.com/" id="yalink">Yahoo!</a>
</p>
<ul>
<li></li>
<li></li>
</ul>
</div>
Note
Even after the node's removal, sitepoint_link still constitutes a reference to that link. It still exists, it's just not in the document any more: it's floating in limbo. We can add it back to the document somewhere else if we want to. Set the variable to null to make the deleted element disappear forever.
Creating Elements
Moving existing elements around within the page is a powerful and useful technique (with which you're well on the way to implementing Space Invaders or Pac Man!). But, above and beyond that, we have the ability to create brand new elements and add them to the page, providing the capacity for truly dynamic content. The point to remember is that, as before, a page's text resides in text nodes, so if we need to create an element that contains text, we must create both the new element node and a text node to contain its text. To achieve this, we need two new methods: document.createElement and document.createTextNode.
First, we create the element itself:
var linux_link = document.createElement('a');
Even though we've created the element, it's not yet part of the document. Next, we set some of its properties in the same way that we'd set properties on an existing link:
linux_link.href = 'http://www.linux.org/';
We then create the text node for the text that will appear inside the link. We pass the text for the text node as a parameter:
var linux_tn =
document.createTextNode('The Linux operating system');
The text node is also floating around, separate from the document. We add the text node to the element's list of children, as above:
linux_link.appendChild(linux_tn);
The element and text node now form a mini-tree of two nodes (officially a document fragment), but they remain separate from the DOM. Finally, we insert the element into the page, which is the same as putting it into the DOM tree:
para.appendChild(linux_link);
Here's the resulting HTML:
<div id="codesection">
<p id="codepara">
<a href="http://www.yahoo.com/" id="yalink">Yahoo!</a>
<a href="http://www.linux.org/">The Linux operating system</a>
</p>
<ul>
<li></li>
<li></li>
</ul>
</div>
As you can see, to create elements, we use the same techniques and knowledge—text nodes are children of the element node, we append a child with node.appendChild—we use to work with nodes that are already part of the document. To the DOM, a node is a node whether it's part of the document or not: it's just a node object.
Copying Elements
Creating one element is simple, as we've seen. But what if you want to add a lot of dynamic content to a page? Having to create a whole batch of new elements and text nodes—appending the text nodes to their elements, the elements to each other, and the top element to the page—is something of a laborious process. Fortunately, if you're adding to the page a copy of something that's already there, a shortcut is available: the cloneNode method. This returns a copy of the node, including all its attributes and all its children. (You can elect to clone the node only—not its children—by passing false to the cloneNode method.) If you have a moderately complex piece of HTML that contains many elements, cloneNode is a very quick way to return a copy of that block of HTML ready for insertion into the document:
var newpara = para.cloneNode(true);
document.getElementById('codesection').appendChild(newpara);
You can't rush ahead and just do this, though: it pays to be careful with cloneNode. This method clones all attributes of the node and all its child nodes, including IDs, and IDs must be unique within your document. So, if you have elements with IDs in your cloned HTML block, you need to fix those IDs before you append the cloned block to the document.
It would be nice to be able to grab the Yahoo! link in our cloned block using the following code:
var new_yahoo_link = newpara.getElementById('yalink');
But, unfortunately, we can't. The getElementById method is defined only on a document, not on any arbitrary node. The easiest way around this is to refrain from defining IDs on elements in a block that you wish to clone. Here's a line of code that will remove the Yahoo! link's id:
newpara.firstChild.removeAttribute('id');
We still have the ID on the paragraph itself, though, which means that when we append the new paragraph to the document, we'll have two paragraphs with the ID codepara. This is bad—it's not supposed to happen. We must fix it before we append the new paragraph, revising the above code as follows:
var newpara = para.cloneNode(true);
newpara.id = 'codepara2';
newpara.firstChild.removeAttribute('id');
document.getElementById('codesection').appendChild(newpara);
This code returns the following results:
<div id="codesection">
<p id="codepara">
<a href="http://www.yahoo.com/">Yahoo!</a>
<a href="http://www.linux.org/">The Linux operating system</a>
</p>
<p id="codepara2">
<a href="http://www.yahoo.com/">Yahoo!</a>
<a href="http://www.linux.org/">The Linux operating system</a>
</p>
<ul>
<li></li>
<li></li>
</ul>
</div>
As you can see, there's a little bit of surgery involved if you choose to copy big chunks of the document. This demonstration concludes our experimentation with this particular bit of code.
Making an Expanding Form
As our first full example, we'll use the DOM's element creation methods to build a form that can grow as the user fills it. This allows users to add to the form as many entries as they like.
Let's imagine an online system through which people can sign up themselves, and any number of their friends, for free beer. (Maybe there's a mad millionaire philanthropist on the loose. No, I can't give you a URL at which this system is running for real!)
The users add their own names, then the names of all of the friends they wish to invite. Without the DOM, we'd require the form either to contain a large number of slots for friends' names (more than anyone would use), or to submit regularly back to the server to get a fresh (empty) list of name entry areas.
In our brave new world, we can add the extra name entry fields dynamically. We'll place a button on the form that says, Add another friend. Clicking that button will add a new field to the list, ready for submission to the server. Each newly-created field will need a different name attribute, so that it can be distinguished when the server eventually receives the submitted form. (Depending on the server-side language used to process the form, this isn't strictly necessary. Since our example form won't actually submit to anything, we'll implement it as a useful exercise.)
Our form will provide a text entry box for the user's name, a fieldset containing one text entry box for a friend's name, and a button to add more friends. When the button is clicked, we'll add a new text entry box for another friend's name.
Example 2.1. expandingForm.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Free beer signup form</title>
<script type="text/javascript">
var fieldCount = 1;
function addFriend() {
fieldCount++;
var newFriend = document.createElement('input');
newFriend.type = 'text';
newFriend.name = 'friend' + fieldCount;
newFriend.id = 'friend' + fieldCount;
document.getElementById('fs').appendChild(newFriend);
}
</script>
<style type="text/css">
input {
display: block;
margin-bottom: 2px;
}
button {
float: right;
}
fieldset {
border: 1px solid black;
}
</style>
</head>
<body>
<h1>Free beer signup form</h1>
<form>
<label for="you">Your name</label>
<input type="text" name="you" id="you">
<fieldset id="fs">
<legend>Friends you wish to invite</legend>
<button onclick="addFriend(); return false;">
Add another friend
</button>
<input type="text" name="friend1" id="friend1">
</fieldset>
<input type="submit" value="Save details">
</form>
</body>
</html>
Notice our fieldCount variable; this keeps track of how many friend fields there are.
Example 2.2. expandingForm.html (excerpt)
var fieldCount = 1;
When the button is clicked, we run the addFriend function (we'll discuss handling clicks—and various other kinds of events—more in the next chapter):
<button onclick="addFriend(); return false;">
The addFriend function completes a number of tasks each time it's run:
- Increments the
fieldCount:Example 2.3.
expandingForm.html(excerpt)fieldCount++; - Creates a new
inputelement:Example 2.4.
expandingForm.html(excerpt)var newFriend = document.createElement('input'); - Sets its type to
text—we want a text entry box, an element specified by<input type="text">:Example 2.5.
expandingForm.html(excerpt)newFriend.type = 'text'; - Sets a unique id and name (because the ID must be unique, and all the entry boxes must have different names so they can be distinguished when the form's submitted):
Example 2.6.
expandingForm.html(excerpt)newFriend.name = 'friend' + fieldCount;
newFriend.id = 'friend' + fieldCount; - Adds this newly-created element to the document:
Example 2.7.
expandingForm.html(excerpt)document.getElementById('fs').appendChild(newFriend);
Here's what the page looks like after the "add another friend" button has been clicked twice, and two friends' names have been added:
Figure 2.4. Signing up for free beer.

Free beer, thanks to the power of the DOM. We can't complain about that!
Making Modular Image Rollovers
Image rollover scripts, in which an image is used as a link, and that image changes when the user mouses over it, are a mainstay of JavaScript programming on the Web. Traditionally, they've required a lot of script, and a lot of customization, on the part of the developer. The introspective capability of the DOM—the ability of script to inspect the structure of the page in which it's running—gives us the power to detect rollover images automatically and set them up without any customization. This represents a more systematic approach than the old-fashioned use of onmouseover and onmouseout attributes, and keeps rollover code separate from other content.
We'll build our page so that the links on which we want to display rollover effects have a class of rollover. They'll contain one img element—nothing else. We'll also provide specially named rollover images: if an image within the page is called foo.gif, then the matching rollover image will be named foo_over.gif. When the page loads, we'll walk the DOM tree, identify all the appropriate links (by checking their class and whether they contain an img element), and set up the rollover on each. This specially-named rollover image allows us to deduce the name of any rollover image without saving that name anywhere. It reduces the amount of data we have to manage.
An alternative technique involves use of a non-HTML attribute in the image tag:
<img src="basic_image.gif" oversrc="roll_image.gif">
However, since oversrc isn't a standard attribute, this approach would cause your HTML to be invalid.
Some of the following script may seem a little opaque: we will be attaching listeners to DOM events to ensure that scripts are run at the appropriate times. If this is confusing, then feel free to revisit this example after you've read the discussion of DOM events in the next chapter.
A Sample HTML Page
First, the HTML: here we have our links, with class rollover, containing the images.
Example 2.8. rollovers.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Modular rollovers</title>
<script type="text/javascript" src="rollovers.js"></script>
<style type="text/css">
/* Remove the blue border on the rollover images */
a.rollover img {
border-width: 0;
}
</style>
</head>
<body>
<h1>Modular rollovers</h1>
<p>Below we have two links, containing images that we want
to change on mouseover.</p>
<ul>
<li>
<a href="" class="rollover" alt="Roll"
><img src="basic_image.gif" /></a>
</li>
<li>
<a href="" class="rollover" alt="Roll"
><img src="basic_image2.gif"></a>
</li>
</ul>
</body>
</html>
The page also includes the JavaScript file that does all the work:
Example 2.9. rollovers.js
function setupRollovers() {
if (!document.getElementsByTagName)
return;
var all_links = document.getElementsByTagName('a');
for (var i = 0; i < all_links.length; i++) {
var link = all_links[i];
if (link.className &&
(' ' + link.className + ' ').indexOf(' rollover ') != -1)
{
if (link.childNodes &&
link.childNodes.length == 1 &&
link.childNodes[0].nodeName.toLowerCase() == 'img') {
link.onmouseover = mouseover;
link.onmouseout = mouseout;
}
}
}
}
function findTarget(e)
{
/* Begin the DOM events part, which you */
/* can ignore for now if it's confusing */
var target;
if (window.event && window.event.srcElement)
target = window.event.srcElement;
else if (e && e.target)
target = e.target;
if (!target)
return null;
while (target != document.body &&
target.nodeName.toLowerCase() != 'a')
target = target.parentNode;
if (target.nodeName.toLowerCase() != 'a')
return null;
return target;
}
function mouseover(e) {
var target = findTarget(e);
if (!target) return;
// the only child node of the a-tag in target will be an img-tag
var img_tag = target.childNodes[0];
// Take the "src", which names an image called "something.ext",
// Make it point to "something_over.ext"
// This is done with a regular expression
img_tag.src = img_tag.src.replace(/(\.[^.]+)$/, '_over$1');
}
function mouseout(e) {
var target = findTarget(e);
if (!target) return;
// the only child node of the a-tag in target will be an img-tag
var img_tag = target.childNodes[0];
// Take the "src", which names an image as "something_over.ext",
// Make it point to "something.ext"
// This is done with a regular expression
img_tag.src = img_tag.src.replace(/_over(\.[^.]+)$/, '$1');
}
// When the page loads, set up the rollovers
window.onload = setupRollovers;
The DOM-walking parts of this code are found in setupRollovers and in findTarget, which is called from the two mouseover/mouseout functions. Let's look at each of these in turn.
The setupRollovers Function
The code for the setupRollovers function starts like this:
Example 2.10. rollovers.js (excerpt)
if (!document.getElementsByTagName)
return;
This code confirms that we're in a DOM-supporting browser. If we're not (i.e. if document.getElementsByTagName, the method, doesn't exist), we exit here and progress no further. If the method does exist, we continue:
Example 2.11. rollovers.js (excerpt)
var all_links = document.getElementsByTagName('a');
Here, we make all_links a reference to a list of all the <a> tags in the document.
Example 2.12. rollovers.js (excerpt)
for (var i = 0; i < all_links.length; i++) {
var link = all_links[i];
The above code iterates through the retrieved list of tags in standard JavaScript fashion. We assign the link variable to each link, as a way to simplify the following code.
Example 2.13. rollovers.js (excerpt)
if (link.className &&
(' ' + link.className + ' ').indexOf(' rollover ') != -1)
{
We need to know whether each link is of class rollover. However, an element may have more than one class; if this tag had two classes, rollover and hotlink, for example, it would have className="rollover hotlink". This would mean that we could not check for an element having a specific class using the following:
if (element.className == "myclass")
If the element has multiple classes, the above condition will always evaluate to false. A useful approach here is to look for the string ' myclass ' (the class name with a space before and after it) in the string ' ' + element.className + ' ' (the element's class attribute with a space before and after it). This will always find your class, as you're expecting. It also avoids a problem with a similar technique, which uses className.indexOf to look for 'myclass'. If the element in question is of class myclassroom, this technique will give a false positive. (Another option is to use a regular expression to spot the class name. In the interests of simplicity, however, we'll stick with the method already presented.)
Example 2.14. rollovers.js (excerpt)
if (link.childNodes &&
link.childNodes.length == 1 &&
link.childNodes[0].nodeName.toLowerCase() == 'img') {
We want to confirm that this link contains nothing but an img element, so we make use of a very handy property of JavaScript, called short-circuit evaluation. In an if statement of the form if (a && b && c), if a is false, then b and c are not evaluated at all. This means that b and c can be things that depend on a's trueness: if a is not true, then they are not evaluated, so it's safe to put them into the if statement.
Looking at the above code may make this clearer. We need to test if the nodeName of the link's first child node is img. We might use the following code:
if (link.childNodes[0].nodeName.toLowerCase == 'img')
However, if the current link doesn't have any child nodes, this code will cause an error because there is no link.childNodes[0]. So, we must first check that child nodes exist; second, we confirm that there is one and only one child; third, we check whether that one-and-only first child is an image. We can safely assume in the image check that link.childNodes[0] exists, because we've already confirmed that that's the case: if it didn't exist, we wouldn't have got this far.
Example 2.15. rollovers.js (excerpt)
link.onmouseover = mouseover;
This code attaches an event handler to the mouseover event on a node.
Example 2.16. rollovers.js (excerpt)
link.onmouseout = mouseout;
And this line attaches an event handler to the mouseout event on that node. That's all!
The findTarget Function
This little function is called by the mouseover and mouseout functions. As we'll see, they pass event objects to findTarget, which, in return, passes back the link tag surrounding the image that generated the event, if any such tag is to be found.
findTarget starts like this:
Example 2.17. rollovers.js (excerpt)
var target;
if (window.event && window.event.srcElement)
target = window.event.srcElement;
else if (e && e.target)
target = e.target;
if (!target)
return null;
This first part is related to DOM event handling, which is explained in the next chapter. We'll ignore its workings for now, except to say that it caters for the differences between Internet Explorer and fully DOM-supporting browsers. Once this code has run, however, we should have in our variable target the element that the browser deems to be responsible for the mouseover or mouseout event—ideally the <a> tag.
Example 2.18. rollovers.js (excerpt)
while (target != document.body &&
target.nodeName.toLowerCase() != 'a')
target = target.parentNode;
if (target.nodeName.toLowerCase() != 'a')
return null;
The variable target should be a reference to the <a> tag on which the user clicked, but it may be something inside the <a> tag (as some browsers handle events this way). In such cases, the above code keeps getting the parent node of that tag until it gets to an <a> tag (which will be the one we want). If we find the document body—a <body> tag—instead, we've gone too far. We'll give up, returning null (nothing) from the function, and going no further.
If we did find an <a> tag, however, we return that:
Example 2.19. rollovers.js (excerpt)
return target;
}
The mouseover / mouseout Functions
These functions work in similar ways and do very similar things: mouseover is called when we move the mouse over one of our rollover links, while mouseout is called when we move the mouse out again.
The code for mouseover starts like this:
Example 2.20. rollovers.js (excerpt)
var target = findTarget(e);
if (!target) return;
We call the findTarget function, described above, to get a reference to the link over which the mouse is located. If no element is returned, we give up, degrading gracefully. Otherwise, we have the moused-over <a> tag in target. Next, we dig out the image.
Example 2.21. rollovers.js (excerpt)
var img_tag = target.childNodes[0];
We also know that the <a> tag has one, and only one, child node, and that's an <img> tag. We know this because we checked that this was the case when we set up the event handler in setupRollovers.
Example 2.22. rollovers.js (excerpt)
img_tag.src = img_tag.src.replace(/(\.[^.]+)$/, '_over$1');
Images have a src attribute, which you can access through the DOM with the element's src property. In the code snippet above, we apply a regular expression substitution to that string. (Although the full details of regular expressions are beyond the scope of this book, we'll look at the basics in Chapter 6, Forms and Validation. A more detailed resource is Kevin Yank's article on sitepoint.com, Regular Expressions in JavaScript.) Changing the value of an <img> tag's src attribute causes it to reload itself with the new image; thus, making this substitution (replacing something.gif with something_over.gif) causes the original image to change to the rollover image. The mouseout function does the exact opposite: it changes the reference to something_over.gif in the image's src attribute to something.gif, causing the original image to reappear.
Something for Nothing (Almost)
If you look at the code for this modular rollover, you'll see that it's divided into parts. The setupRollovers function does nothing but install listeners. The findTarget function does nothing but find the link tag for a given event. The mouseover and mouseout functions do little other than the actual image swapping work. The tasks are neatly divided.
That means that this code is good for other applications. We can change the mouseover and mouseout functions to do something else—for example, to make popup help content appear—without needing to start from scratch to get it working. We get to reuse (or at least rip off with minimal change) the other functions in the script. This is not only convenient; it's also neat and clean. We're on the way to a better kind of scripting!
Summary
In the introduction, we referred to the DOM as a critical part of DHTML. Exploring the DOM—being able to find, change, add, and remove elements from your document—is a powerful technique all by itself, and is a fundamental aspect of modern DHTML. Once you've mastered the techniques described in this chapter, everything else will fall into place. Through the rest of the book, we'll be describing techniques and tricks with which you can do wondrous things on your sites, and in your Web applications, using DHTML. They all build upon this fundamental approach of manipulating the Document Object Model.