Working with the DOM

Ethan Brown

Working with the DOM

The Document Object Model

The Document Object Model, or DOM, is a convention for describing the structure of an HTML document, and it’s at the heart of interacting with the browser.

Conceptually, the DOM is a tree. A tree consists of nodes: every node has a parent (except for the root node), and zero or more child nodes. The root node is the document, and it consists of a single child, which is the <html> element. The <html> element, in turn, has two children: the <head> element and the <body> element (Figure 1-1 is an example DOM).

Every node in the DOM tree (including the document itself) is an instance of the Node class (not to be confused with Node.js, the subject of the next chapter). Node objects have a parentNode and childNodes properties, as well as identifying properties such as nodeName and nodeType.

Note

The DOM consists entirely of nodes, only some of which are HTML elements. For example, a paragraph tag (<p>) is an HTML element, but the text it contains is a text node. Very often, the terms node and element are used interchangeably, which is rarely confusing, but not technically correct. In this chapter, we’ll mostly be dealing with nodes that are HTML elements, and when we say “element” we mean “element node.”

Figure 1-1. DOM tree

For the following examples, we’ll use a very simple HTML file to demonstrate these features. Create a file called simple.html:

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Simple HTML</title>
        <style>
            .callout {
                border: solid 1px #ff0080;
                margin: 2px 4px;
                padding: 2px 6px;
            }
            .code {
                background: #ccc;
                margin: 1px 2px;
                padding: 1px 4px;
                font-family: monospace;
            }
        </style>
    </head>
    <body>
        <header>
            <h1>Simple HTML</h1>
        </header>
        <div id="content">
            <p>This is a <i>simple</i> HTML file.</p>
            <div class="callout">
                <p>This is as fancy as we'll get!</p>
            </div>
            <p>IDs (such as <span class="code">#content</span>)
                are unique (there can only be one per page).</p>
            <p>Classes (such as <span class="code">.callout</span>)
                can be used on many elements.</p>
            <div id="callout2" class="callout fancy">
                <p>A single HTML element can have multiple classes.</p>
            </div>
        </div>
    </body>
</html>

Every node has the properties nodeType and nodeName (among others). nodeType is an integer identifying what type of node it is. The Node object contains constants that map to these numbers. The types of node we’ll be primarily dealing with in this chapter are Node.ELEMENT_NODE (HTML elements) and Node.TEXT_NODE (text contents, usually within HTML elements). For more information, see the MDN documentation for nodeType.

It’s an instructive exercise to write a function that traverses the entire DOM and prints it to the console, starting with document:

function printDOM(node, prefix) {
    console.log(prefix + node.nodeName);
    for(let i=0; i<node.childNodes.length; i++) {
        printDOM(node.childNodes[i], prefix + '\t');
    }
}
printDOM(document, '');

This recursive function does what’s known as a depth-first, pre-order traversal of a tree. That is, it follows branches all the way before moving on to the next branch. If you run it in a browser with a page loaded, you will see the entire structure of the page printed out to the console.

While this is an instructive exercise, it would be a tedious and inefficient way to manipulate HTML (having to traverse the entire DOM to find what you’re looking for). Fortunately, the DOM provides methods to locate HTML elements more directly.

Tip

While writing your own traversal function is a good exercise, the DOM API provides the TreeWalker object, which allows you to iterate through all of the elements in the DOM (optionally filtering by certain element types). For more information, see the MDN documentation for document.createTreeWalker.

Some Tree Terminology

The concept of a tree is straightforward and intuitive, and lends itself to similarly intuitive terminology. A node’s parent is its direct parent (that is, not a “grandparent”) and a child is a direct child (not a “grandchild”). The term descendant is used to refer to a child, or a child’s child, or so on. The term ancestor is used to refer to a parent, the parent’s parent, and so on.

DOM “Get” Methods

The DOM provides “get” methods that allow you to quickly locate specific HTML elements.

The first of these is document.getElementById. Every HTML element on a page may be assigned a unique ID, and document.getElementById can retrieve an element by its ID:

document.getElementById('content');     // <div id="content">...</div>

Warning

Browsers don’t do anything to enforce the uniqueness of IDs (though an HTML validator will catch these issues), so it is incumbent on you to ensure that IDs are unique. As the construction of web pages gets more complicated (with components coming from multiple sources), it’s becoming increasingly difficult to avoid duplicate IDs. For this reason, I recommend using them carefully and sparingly.

document.getElementsByClassName returns a collection of elements that have the given class name:

const callouts = document.getElementsByClassName('callout');

And document.getElementsByTagName returns a collection of elements that have the given tag name:

const paragraphs = document.getElementsByTagName('p');

Warning

All of the DOM methods that return a collection do not return a JavaScript array, but an instance of HTMLCollection, which is an “array-like” object. You can iterate over it with a for loop, but the Array.prototype methods (such as map, filter, and reduce) won’t be available. You can convert an HTMLCollection to an array by using the spread operator: [...document.getElementsByTagName(p)].

Querying DOM Elements

getElementById, getElementsByClassName, and getElementsByTagName are useful, but there’s a much more general (and powerful) method that can locate elements not just by a single condition (ID, class, or name), but also by the element’s relationship to other elements. The document methods querySelector and querySelectorAll allow you to use CSS selectors.

CSS selectors allow you to identify elements by their name (<p>, <div>, etc.), their ID, their class (or combination of classes), or any combination thereof. To identify elements by name, simply use the name of the element (without angle brackets). So a will match all <a> tags in the DOM, and br will match all the <br> tags. To identify elements by their class, use a period before the class name: .callout will match all elements that have the class callout. To match multiple classes, just separate them with periods: .callout.fancy will match all elements that have the class callout and the class fancy. Lastly, they can be combined: for example, a#callout2.callout.fancy will match <a> elements with ID callout2, and classes callout and fancy (it’s very rare to see a selector that uses element name, ID, and class(es)…but it is possible).

The best way to get the hang of CSS selectors is to load the sample HTML provided in this chapter in a browser, open the browser’s console, and try them out with querySelectorAll. For example, in the console, type document.querySelectorAll('.callout'). All of the examples in this section will produce at least one result with querySelectorAll.

So far, we’ve been talking about identifying specific elements no matter where they appear in the DOM. CSS selectors also enable us to locate elements according to their position in the DOM.

If you separate multiple element selectors with a space, you can select nodes with a specific ancestry. For example, #content p will select <p> elements that are descendants of whatever element has the ID content. Likewise, #content div p will select <p> elements that are inside a <div> that are inside an element with the ID content.

If you separate multiple elements selectors with a greater-than sign (>), you can select nodes that are direct children. For example, #content > p will select <p> elements that are children of an element with ID content (contrast this with "#content p“).

Note that you can combine ancestry and direct child selectors. For example, body .content > p will select <p> tags that are direct children of elements with class content that are descendants of <body>.

There are more sophisticated selectors, but the ones covered here are the most common. To learn more about all the selectors available, see the MDN documentation on selectors.

Manipulating DOM Elements

Now that we know how to traverse, get, and query elements, what do we do with them? Let’s start with content modification. Each element has two properties, textContent and innerHTML, that allow you to access (and change) the element’s content. textContent strips out all HTML tags and provides text data only, whereas innerHTML allows you to create HTML (which results in new DOM nodes). Let’s see how we can access and modify the first paragraph in our example:

const para1 = document.getElementsByTagName('p')[0];
para1.textContent;    // "This is a simple HTML file."
para1.innerHTML;      // "This is a <i>simple</i> HTML file."
para1.textContent = "Modified HTML file";       // look for change in browser
para1.innerHTML = "<i>Modified</i> HTML file";  // look for change in browser

Warning

Assigning to textContent and innerHTML is a destructive operation: it will replace whatever is in that element, no matter how big or complex. For example, you could replace the entire page contents by setting innerHTML on the <body> element!

Creating New DOM Elements

We’ve already seen how to implicitly create new DOM nodes by setting an element’s innerHTML property. We can also explicitly create new nodes with document.createElement. This function creates a new element, but it doesn’t add it to the DOM; you’ll have to do that in a separate step. Let’s create two new paragraph elements; one will become the first paragraph in <div id="content">, and the other will become the last:

const p1 = document.createElement('p');
const p2 = document.createElement('p');
p1.textContent = "I was created dynamically!";
p2.textContent = "I was also created dynamically!";

To add these newly created elements to the DOM, we’ll use the insertBefore and appendChild methods. We’ll need to get references to the parent DOM element (<div id="content">), and its first child:

const parent = document.getElementById('content');
const firstChild = parent.childNodes[0];

Now we can insert the newly created elements:

parent.insertBefore(p1, firstChild);
parent.appendChild(p2);

insertBefore takes the element to inset first, and then a “reference node,” which is the node to insert before. appendChild is very simple, appending the specified element as the last child.