The contentious experience of the W3C's DOM task force illustrates many of the problems with designing a critical industrywide infrastructure by committee. DOM syntax went through several major changes in less than a year, to the consternation of web coders trying to author new pages according to the standard. Even the most powerful companies represented on the committee, such as Microsoft, found it hard to keep up with the various drafts. But now that their work on DOM Level 1 is done, the result can be found at the W3C site.
Frankly, the standards specification is not intended to be clear to outsiders, so some interpretation is essential. Think of a document as a fractal object, like a tree with several "nodes." In botany, a node corresponds to a root, a trunk, a branch, or a twig, terminating in a leaf or a nut. (The leaf and nut might correspond to a tag's attribute and text-data in the document model. These can't have children.) Each sort of node is an object with its own set of properties and methods.
The node is therefore the atomic unit of a document. Like a trunk or a branch, each node can have children nodes (except terminal nodes such as text-data), and each node has a parent node (except for the root node). By getting a reference to the child or parent of any node, one can recursively climb the tree and reach any part of the document.
Here's a list of nodes in XML and HTML documents.
One useful thing about a document fragment is that it allows one to move around large chunks of a document as a single object. Equally useful is the fact that unlike a document, a document fragment need not be "well formed" (that is, it need not have a perfect set of nested tags).
Authors who are familiar with object models in DHTML should find the DOM reasonably easy to useonce they master the scripts required to navigate recursively through the document. For instance, consider a simplified version of Dan Steinman's cross-browser DHTML function for writing new content shown in Listing 4.
Understanding the Document Object Model
name="value"
pair that represents a tag's property (for instance, src
is an attribute for the img
element).EntityReference
.DocumentType
object to access a list of entities within that document. DHTML vs. DOM
Listing 4. Writing new content in Dan Steinman's "DynLayer" object.
ns4 = (document.layers)? true:false
ie4 = (document.all)? true:false
function simpleLayerWrite(id,text) {
if (ns4) {
var lyr = document.layers[id].document
lyr.open()
lyr.write(text)
lyr.close()
}
else if (ie4) document.all[id].innerHTML = text
}
This DHTML function can change the content of a div
tag, given its ID value and the string for new content. Note that while the commands are straightforward, they must take very different forms to accomodate two incompatible object models. The ns4
flag is true when the browser is Navigator 4 and the ie4
flag is true when the browser is Internet Explorer 4.0. Navigator uses an object called Layer for dynamic content, while Internet Explorer addresses the div
more directly.
By contrast, when using the DOM, there is only one set of code, for instance, the example shown in Listing 5.
// C. DOM tree-navigation and utilities. /* One must climb the trunk, branches, and twigs to get (or set!) the fruit. The recursive nature of this algorithm reflects an essential fractal nature of documents. */ function replaceAllText(startelem) { // Climb the object tree, replacing its text nodes with // new data. for (var i=0; i < startelem.childNodes.length; i = i + 1) { switch (startelem.childNodes.item(i).nodeType) { case 1: // Element nodetype replaceAllText(startelem.childNodes.item(i)) break; case 3: // Text nodetype if (datacount < model.length) { setText(startelem.childNodes.item(i), model[datacount]) datacount = datacount + 1 } else { setText(startelem.childNodes.item(i)," - ? - ") } break; } //endswitch } //endfor } //endfunction /* Many operations on dynamic documents require one to start from the document's body element. */ function getBody() { if(navigator.appName != "Netscape") { resultElement = document.body; } else { resultElement = document.getElementsByTagName("body").item(0); } return resultElement; } // Utility function to overwrite text nodes. function setText(tagToSet, valueToSet) { tagToSet.nodeValue = valueToSet } |
Listing 5 illustrates the tree-climbing algorithm that is used when it's not feasible to locate the desired item by an ID attribute. This function accepts a branch node and recursively travels it, replacing each text node that it finds with the items in an array called "model." This is a common practice that often requires one to locate the document rootits body element, as shown in the getBody()
function.
Using the DOM, an author can:
- Create elements and attach them to any point in the document tree.
- Manipulate tree branches in a document fragment and insert the changed fragment back into the tree.
- Destroy elements.
- Move one part of the document tree to another without destroying and recreating the content.
- Harvest the entire text content of a document.
- Search the document for a string.
- Search the tags for a given attribute.
- And do more.
Members of the model
The DOM interface exposes objects, collections, properties, and methods. Table 1 outlines a few of the most important methods.
Selected Document Methods | |
createElement(tagName) </td> <td valign="top">creates a new tag. </td> </tr> <tr> <td valign="top"><pre> createDocumentFragment() </td> <td valign="top">creates a new DocumentFragment. </td> </tr> <tr> <td valign="top"><pre> createTextNode(data) </td> <td valign="top">creates new content for the document. </td> </tr> <tr><td><pre>createAttribute(name) </td> <td valign="top"> creates an attribute for a tag. </td> </tr> <tr><td valign="top"><pre>getElementsByTagName(tagname) </td> <td> returns a list of matching nodes (NodeList). </td> </tr> <tr><td colspan="2"><b>Selected HTMLDocument Methods</b><br /> <font face="verdana, helvetica, sans-serif" size="1"><i>An HTMLDocument object has these methods in addition to the methods of Document.</i></font></td> </tr> <tr><td valign="top"><pre>open() </td> <td>Opens a document for new content. </td> </tr> <tr><td valign="top"><pre>close() </td> <td> Closes a document.</td> </tr> <tr><td valign="top"><pre>write(text) </td> <td> Appends the text to the open document. </td> </tr> <tr><td valign="top"><pre>writeln(text)</td> <td> Appends text and a new line to the open document. </td> </tr> <tr><td valign="top"><pre>getElementById(elementId) </td> <td> returns the element with the given ID. </td> </tr> <tr><td colspan="2"><b>Selected Node Methods</b></td> </tr> <tr><td valign="top"><pre>insertBefore(newChild, refChild)</td> <td valign="top">Inserts a child node into the document before the given node. </td> </tr> <tr><td valign="top"><pre>replaceChild(newChild, oldChild)</td> <td valign="top">Replaces the old node with the new node. </td> </tr> <tr><td valign="top"><pre>removeChild(oldChild)</td> <td valign="top">Removes the child node from the document. </td> </tr> <tr><td valign="top"><pre>appendChild(newChild)</td> <td valign="top">Adds the node to the document.</td> </tr> <tr><td valign="top"><pre>hasChildNodes()</td> <td valign="top"> Returns true if the node has children.</td> </tr> <tr><td valign="top"><pre>cloneNode(deep)</td> <td valign="top"> Generates a copy of the tree under the given node.</td> </tr> <tr><td colspan="2"><b>Selected Element Methods</b><br /> <font face="verdana, helvetica, sans-serif" size="1"><i>An Element has these methods in addition to the methods of Node.</i></font></td> </tr> <tr><td valign="top"><pre>getAttribute(name)</td> <td valign="top">Gets the value of the named attribute. </td> </tr> <tr><td valign="top"><pre>setAttribute(name, value)</td> <td valign="top">Sets the value of the named attribute.</td> </tr> <tr><td valign="top"><pre>removeAttribute(name)</td> <td valign="top">Removes the named attribute.</td> </tr> <tr><td valign="top"><pre>getElementsByTagName(name)</td> <td valign="top"> Returns a list of matching nodes (NodeList).</td> </tr> </table> <font face="verdana" size="1"><b>Table 1. Sample list of critical DOM methods.</b></font> </p> <h3>Creating the view</h3> Nodes are created using the <code>createElement</code> and <code>createTextNode</code> methods. Manipulating nodes can, however, be tricky because it is very easy to build invalid tree structures, and therefore authors must familiarize themselves with the branching patterns that occur in documents. For instance, to create a well-formed table in the DOM, a <code>TBODY</code> element must be explicitly created and added to the tree. If the <code>TBODY</code> element is neglected, the nodes comprising the entire table form an invalid tree and the result is unpredictable. This example shows how to correctly add a table. </p> Our example's <code>createView()</code> function creates table-row and -cell elements and appends them as children to <code>TBODY</code>, as shown in Listing 6. </p> <table width="100%" cellpadding="3" cellspacing="4" border="1"> <tr> <td valign="top"> <pre> function createView(bodyelement, themodel) { table = document.createElement("TABLE") table.border = 1 table.id = "viewtable" tablebody = document.createElement("TBODY") var modelcount = 0 for(var i=0; i < model.length; i++) { currentRow = document.createElement("TR") currentCell = document.createElement("TD") currentCell.appendChild(document.createTextNode(model[i])) currentRow.appendChild(currentCell) tablebody.appendChild(currentRow) } table.appendChild(tablebody) bodyelement.appendChild(table) return table } |
createView()
function creates a new table.
The cells are filled with the model data. Once the cells are assembled, the table body is appended to the table and the completed table is appended to the document body.
Note that elements are spawned in the document using the createElement
function, but they must be actually appended to it with a separate method, appendChild
.
The whole point of the MVC pattern is repurposability. Accordingly, createView
could be generalized to provide alternatives to a table view, such as a select object, a tree, or even a text area. This sample produces a one-column table. Multi-column tables are more complex.
Responding to user events
When the user clicks the button, the view requests a refresh: The table needs to be updated.
The controller refreshes the model first, then the view. It copies the currently selected data into the model, and then replaces the cells' text with the contents of the model, using the utility functions in section (C), as shown in Listing 7.
// Copy the specified dataset into the model. function refreshModel(arraycurrent) { // Populate the model with the current datastore. for(var i=0; i <: arraycurrent.length; i = i + 1) { model[i] = arraycurrent[i] } return model } // Refresh the model first, then the view. function refreshView(dataset) { // Populate the model with new data. refreshModel(dataset) tablebody = document.getElementsByTagName("TBODY").item(0) var count = 0 replaceAllText(tablebody) } |
refreshModel()
and refreshView()
functions.
Is this a real MVC application?
Not really. It's a toy application, built for illustration. A real MVC application would at least have a function to edit the data that appears in the view. One point that should be stressed is that in MVC, changes to the data must be sent to the datastore first, and only then should the view be refreshed with the edited data, and not the other way around.
Dismantling the view
Some DOM methods allow one to easily manipulate nodes, notably the cloneNode()
, removeNode()
, replaceNode()
, and swapNode()
methods. These methods provide copy, move, and delete functionality to the entire document tree.
The example in Listing 8 shows how objects can be destroyed. In this function, one identifies the table and the buttons by their ID attributes, and thus it isn't necessary to navigate the document tree looking for them.
// One can also destroy HTML objects using the DOM. function destroyView() { objecttodestroy = document.getElementById("viewtable") body = getBody() body.removeChild(objecttodestroy) // Now destroy the buttons. objecttodestroy = document.getElementById("whosaid") body.removeChild(objecttodestroy) objecttodestroy = document.getElementById("goaway") body.removeChild(objecttodestroy) } |
destroyView()
function.
Conclusion
After a glance at DHTML incompatibilities in previous versions of the Microsoft Internet Explorer and Netscape Navigator browsers, this article explored a simple dynamic document that creates, modifies, and destroys HTML elements using the W3C's DOM API for JavaScript. This example also shows how to begin implementing the MVC design pattern in a user interface. Finally, this sample illustrates the new syntax for web pages introduced with XHTML.
Mitch Gould is a human-factors programmer for General Picture in Forest Grove, Oregon. This article is adapted from his book-in-progress, Human Factors Programming using the DOM, a guide to innovative user interfaces for the Mozilla browser. Article copyright 1999 by General Picture. All rights reserved.
JavaScript for the MVC example
Understanding the Document Object Model