The Document Object Model

Now that we’ve reviewed the basic syntax and structure of the JavaScript language, and how to load it into a page, we can turn our attention to what it was created for - to interact with web pages in the browser. This leads us to the Document Object Model (DOM).

The DOM is a tree-like structure that is created by the browser when it parses the HTML page. Then, as CSS rules are interpreted and applied, they are attached to the individual nodes of the tree. Finally, as the page’s JavaScript executes, it may modify the tree structure and node properties. The browser uses this structure and properties as part of its rendering process.

The Document Instance

The DOM is exposed to JavaScript through an instance of the Document class, which is attached to the document property of the window (in the browser, the window is the top-level, a.k.a global scope. Its properties can be accessed with or without referencing the window object, i.e. window.document and document refer to the same object).

This document instance serves as the entry point for working with the DOM.

The Dom Tree

The DOM tree nodes are instances of the Element class, which extends from the Node class, which in turn extends the EventTarget class. This inheritance chain reflects the Separation of Concerns design principle: the EventTarget class provides the functionality for responding to events, the Node class provides for managing and traversing the tree structure, and the Element class maintains the element’s appearance and properties.

Selecting Elements on the Page

One of the most important skills in working with the DOM is understanding how to get a reference to an element on the page. There are many approaches, but some of the most common are:

Selecting an Element by its ID

If an element has an id attribute, we can select it with the Document.getElementByID() method. Let’s select our button this way. Add this code to your playground.js file:

var button = document.getElementById("some-button");
console.log(button);

You should see the line [object HTMLButtonElement] - the actual instance of the DOM node representing our button (the class HTMLButtonElement is an extension of Element representing a button).

Selecting a Single Element by CSS Selector

While there are additional selectors for selecting by tag name, class name(s), and other attributes, in practice these have largely been displaced by functions that select elements using a CSS selector.

Document.querySelector() will return the first element matching the CSS selector, i.e.:

var button = document.querySelector('#some-button');

Works exactly like the document.getElementById() example. But we could also do:

var input = document.querySelector('input[type=text]');

Which would grab the first <input> with attribute type=text.

Selecting Multiple Elements by CSS Selector

But what if we wanted to select more than one element at a time? Enter document.querySelectorAll(). It returns a NodeList containing all matching nodes. So the code:

var paras = document.querySelectorAll('p.highlight');

Will populate the variable paras with a NodeList containing all <p> elements on the page with the highlight class.

Warning

While a NodeList is an iterable object that behaves much like an array, it is not an array. Its items can also be directly accessed with bracket notation ([]) or NodeList.item(). It can be iterated over with a for .. of loop, and in newer browsers, NodeList.forEach(). Alternatively, it can be converted into an array with Array.from().

Element.querySelector() and Element.querySelectorAll()

The query selector methods are also implemented on the element class, with Element.querySelector() and Element.querySelectorAll(). Instead of searching the entire document, these only search their descendants for matching elements.

Events

Once we have a reference to an element, we can add an event listener with EventTarget.addEventListener(). This takes as its first argument, the name of the event to listen for, and as the second, a method to invoke when the event occurs. There are additional optional arguments as well (such as limiting an event listener to firing only once), see the MDN documentation for more details.

For example, if we wanted to log when the user clicks our button, we could use the code:

document.getElementById("some-button").addEventListener('click', function(event) {
  event.preventDefault();
  console.log("Button was clicked!");
});

Notice we are once again using method chaining - we could also assign the element to a var and invoke addEventListener() on the variable. The event we want to listen for is identified by its name - the string 'click'. Finally, our event handler function will be invoked with an event object as its first argument.

Also, note the use of event.preventDefault(). Invoking this method on the event tells it that we are taking care of its responsibilities, so no need to trigger the default action. If we don’t do this, the event will continue to bubble up the DOM, triggering any additional event handlers. For example, if we added a 'click' event to an <a> element and did not invoke event.preventDefault(), when we clicked the <a> tag we would run our custom event handler and then the browser would load the page that the <a> element’s href attribute pointed to.

Common Event Names

The most common events you’ll likely use are

  • "click" triggered when an item is clicked on
  • "input" triggered when an input element receives input
  • "change" triggered when an input’s value changes
  • "load" triggered when the source of a image or other media has finished loading
  • "mouseover" and "mouseout" triggered when the mouse moves over an element or moves off an element
  • "mousedown" and "mouseup" triggered when the mouse button is initially pressed and when it is released (primarily used for drawing and drag-and-drop)
  • "mousemove" triggered when the mouse moves (used primarily for drawing and drag-and-drop)
  • "keydown", "keyup", and "keypressed" triggered when a key is first pushed down, released, and held.

Note that the mouse and key events are only passed to elements when they have focus. If you want to always catch these events, attach them to the window object.

There are many more events - refer to the MDN documentation of the specific element you are interested in to see the full list that applies to that element.

Event Objects

The function used as the event handler is invoked with an object representing the event. In most cases, this is a descendant class of Event that has additional properties specific to the event type. Let’s explore this a bit with our text input. Add this code to your playground.js, reload the page, and type something into the text input:

document.getElementById("some-input").addEventListener("input", function(event) {
  console.log(event.target.value);
});

Here we access the event’s target property, which gives us the target element for the event, the original <input>. The input element has the value property, which corresponds to the value attribute of the HTML that was parsed to create it, and it changes as text is entered into the <input>.

Modifying DOM Element Properties

One of the primary uses of the DOM is to alter properties of element objects in the page. Any changes to the DOM structure and properties are almost immediately applied to the appearance of the web page. Thus, we use this approach to alter the document in various ways.

Attributes

The attributes of an HTML element can be accessed and changed through the DOM, with the methods element.getAttribute(), element.hasAttribute() and element.setAttribute().

Let’s revisit the button in our playground, and add an event listener to change the input element’s value attribute:

document.getElementById("some-button").addEventListener("click", function(event) {
  document.getElementById("some-input").setAttribute("value", "Hello World!")
});

Notice too that both event handlers we have assigned to the button trigger when you click it. We can add as many event handlers as we like to a project.

Styles

The style property provides access to the element’s inline styles. Thus, we can set style properties on the element:

document.getElementById("some-button").style = "background-color: yellow";

Remember from our discussion of the CSS cascade that inline styles have the highest priority.

Class Names

Alternatively, we can change the CSS classes applied to the element by changing its element.classList property, which is an instance of a DOMTokensList, which exposes the methods:

  • add() which takes one or more string arguments which are class names added to the class list
  • remove() which takes one or more string arguments which are class names removed from the class list
  • toggle() which takes one or more strings as arguments and toggles the class name in the list (i.e. if the class name is there, it is removed, and if not, it is added)

By adding, removing, or toggling class names on an element, we can alter what CSS rules apply to it based on its CSS selector.

Altering the Document Structure

Another common use for the DOM is to add, remove, or relocate elements in the DOM tree. This in turn alters the page that is rendered. For example, let’s add a paragraph element to the page just after the <h1> element:

var p = document.createElement('p');
p.innerHTML = "Now I see you";
document.body.insertBefore(p, document.querySelector('h1').nextSibling);

Let’s walk through this code line-by-line.

  1. Here we use Document.createElement() to create a new element for the DOM. At this point, the element is unattached to the document, which means it will not be rendered.
  2. Now we alter the new <p> tag, adding the words "Now I see you" with the Element.innerHTML property.
  3. Then we attach the new <p> tag to the DOM tree, using Node.insertBefore() method and Node.nextSibling property.

The Node interface provides a host of properties and methods for traversing, adding to, and removing from, the DOM tree. Some of the most commonly used are: