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 our 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.
The DOM Playground
The panels opposite show a simple web app where we can experiment with the DOM. Specifically, we’re going to use JavaScript code we write in playground.js to interact with the page index.html. The page already has a couple of elements defined in it - a button with id="some-button"
, and a text input with id="some-input"
.
Additionally, the JavaScript code in page-console.js hijacks the console.log()
method so that instead of printing to the regular console, it injects the output to a div
element on the page. This will help make the console more accessible in Codio. Also, it demonstrates just how dynamic a language JavaScript is - we just altered the behavior of a core function!
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.
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 listremove()
which takes one or more string arguments which are class names removed from the class listtoggle()
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.
- 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.
- Now we alter the new
<p>
tag, adding the words"Now I see you"
with the Element.innerHTML property. - 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:
- Node.parent the parent of this Node
- Node.childNodes a NodeList of this node’s children
- Node.nextSibling returns the node following this one at the same level in the DOM tree
- Node.previousSibling returns the node proceeding this one in the DOM tree at the same level
- Node.appendChild() adds the an element as the last child of the node
- Node.removeChild() removes a child from the node and returns it (this means the element becomes unattached)
- Node.replaceChild() replaces a child element with another