The DOM API – JavaScript in the Browser
Recall, from our previous discussion, the three spheres of concern that exist in the browser: content, presentation, and behavior; HTML, CSS, and JavaScript. Now that we are familiar with JavaScript itself as a general-purpose programming language, how do we use it to build dynamic front-end user interfaces?
The bridge between JavaScript programs and the content that is rendered in the browser is the Document Object Model (DOM) API. The DOM API is a formal specification maintained by the W3C which defines a standard way for scripts to read and manipulate the browser’s in-memory document object. The content and style of each element, and even the structure of the document overall, is exposed in this object-oriented interface.
As we have already seen, the DOM is a tree structure of nodes. Here is a small sample HTML document with which we can explore some common DOM manipulation operations.
<!DOCTYPE html>
<html>
<head>
<title>DOM Page</title>
</head>
<body>
<h1>The main heading</h1>
<p class="highlight">An interesting summary of this content.</p>
<p>
Some supplementary details to accompany our discussion.
It also has a <a href="#">link</a>.
</p>
<div class="widget">
<div class="foo"></div>
</div>
<table>
<thead>
<tr>
<th>School</th>
<th>Color</th>
</tr>
</thead>
<tbody>
<tr>
<td>UNC Chapel Hill</td>
<td>Carolina Blue</td>
</tr>
<tr>
<td>NC State</td>
<td>Wolfpack Red</td>
</tr>
</tbody>
</table>
</body>
</html>
DOM Manipulation
For most use cases, DOM manipulation will happen under document.body
since it
is the only part of the document that has rendered visual components. Let’s
construct a DOM tree for the body of our document above.
Each node is represented by an entity box. Notice that, in addition to the
element nodes, our DOM also has several text nodes. Element nodes are created
in accordance to the tags we author in the HTML document markup, whereas text
nodes are created implicitly when building the DOM in order to contain each
discrete piece of text. Text nodes are always leaf nodes, as they cannot contain
any more deeply-nested content. You can see in the element p.highlight
that
its contained text is split into separate sibling text nodes by an <a>
element, which itself has a child text node as well. We didn’t have to
explicitly create these separate text nodes – the browser does this
automatically.
On some of the nodes, we have listed additional element properties which we
specified through attributes in the markup, such as element classes and anchor
href
value. In reality, each node has an abundance of interesting properties
and methods which are implemented as part of the DOM API. In a classical-like,
object-oriented pattern, each node inherits from a hierarchy of interfaces
depending on its node and element type.
For example, any given paragraph element p
is an instance of
HTMLParagraphElement
. Its full hierarchy up the JavaScript prototype chain is:
For reference, here’s what our document looks like when rendered in the browser:
Our entry point into the DOM is through the global document
object. By far,
the most flexible tool for finding existing elements in the document is the
Selectors API, which allows us to search for elements using the same syntax as
CSS selectors. The singular form,
.querySelector(selectors)
will return the first element it finds that matches any of the comma-separated
selectors in the single string selectors
argument. When invoked on document
,
it searches the entire document; when invoked on any other element, it searches
only in the subtree containing that element’s descendants. If no matching
element is found, it returns null
. Elements are searched in order of
depth-first, pre-order traversal of the document’s nodes.
document.querySelector('p'); // <p.highlight>An interesting summary... </p>
The plural form of the query selector, .querySelectorAll
, returns an iterable
NodeList
of all elements which match at least one of the given selectors. If
no matches are found, it returns an empty NodeList
.
document.querySelectorAll('p');
// NodeList(2) [p.highlight, p]
document.querySelectorAll('figure');
// NodeList(0) []
NodeList
s are not instances of Array
, but they are iterable and also have a
.forEach
method. If needed, we can convert them to real arrays using
Array.from
.
let paragraphs = document.querySelectorAll('p');
paragraphs.length; // 2
paragraphs instanceof Array; // false
typeof paragraphs.map; // "undefined"
let parArr = Array.from(paragraphs);
parArr instanceof Array; // true
typeof paragraphs.map; // "function"
Once we have a reference to an element in the DOM, we can manipulate it. Common manipulations include adding, replacing, or removing content, toggling classes, or directly manipulating style.
Let’s run through a few quick fixes for our page to demonstrate some of these functionalities. Think about our overall approach and don’t worry about memorizing syntax or property names – that’s what we have reference texts for.
First, we want to switch the order of the table rows so that NC State appears first (for alphabetical sorting reasons, of course).
// Get handle to the NC State row, which is currently
// the second row in the table's body
let tbody = document.querySelector('tbody');
let stateRow = tbody.querySelector('tr:nth-child(2)');
// Move stateRow to the top: insert it before the
// currently-first row (this automatically removes
// then re-inserts the stateRow node)
tbody.insertBefore(stateRow, tbody.firstChild);
// Done!
Great. While we’re here, let’s change the color of the text in the table to match the schools’ respective colors.
// Get only table cell element for "wolfpack red"
// We know it's the last cell on the row
let redCell = stateRow.querySelector('td:last-child');
redCell.style.color = 'rgb(204, 0, 0)';
// Let's make it bold, too
redCell.style.fontWeight = 'bold';
// Now get cell for Carolina blue
let blueCell = stateRow.nextElementSibling // next row
.querySelector('td:last-child');
blueCell.style.color = 'rgb(123, 175, 212)';
Awesome. Now let’s make all of the paragraphs use the Arial typeface, and make
the opening paragraph (the one with class highlight
) stand out by making the
text a bit bigger.
Array.from(document.querySelectorAll('p')).forEach(
p => (p.style['font-family'] = 'Arial, sans-serif'),
);
document.querySelector('p.highlight').style['font-size'] = '1.25rem';
One last thing: we just got back a response from our backend API with some
messages that need to be displayed in the widget div
.
let data = {
messages: [
'This is an important message!',
'Here is an alert!',
'You have mail.',
],
};
let widget = document.querySelector('.widget');
// We'll put a total count in the first div.foo
widget.querySelector('.foo').innerText = `You have ${
data.messages.length
} unread messages.`;
// Make an ordered list <ol> with the messages
let list = document.createElement('ol');
// At this point the list is not rendered
// anywhere on screen
data.messages.forEach(message => {
let item = document.createElement('li');
item.innerText = message;
list.appendChild(item);
});
// Now add the complete list to the widget
widget.appendChild(list);
And here’s the result!
Handling Events
Part of the DOM API is the Event
model. From MDN,
DOM Events are sent to notify code of interesting things that have taken place. Each event is represented by an object which is based on the Event interface, and may have additional custom fields and/or functions used to get additional information about what happened. Events can represent everything from basic user interactions to automated notifications of things happening in the rendering model.
Events can be triggered by a wide variety of sources, including mouse, touch, or keyboard events; animation and other rendering events; and DOM mutation events. There are far too many to even mention briefly here – I highly recommend browsing through MDN’s Event reference and related articles, listed in the Appendix under Further Readings.
We can briefly look at a simple example of how we can use JavaScript to attach
event handling behavior in our program. Let’s imagine that somewhere in the DOM
we have a button
element with id="mybtn"
. Every time we click the button, we
want to update its text to include the number of times it has already been
clicked.
We can “listen” to events fired on specific DOM elements by registering event listener functions on them in a callback style. The listener functions are invoked with the fired Event object as an argument.
let btn = document.getElementById('mybtn');
let i = 0; // Maintain state between clicks
// Attach click event handler
btn.addEventListener('click', function(event) {
i++;
btn.innerText = `Clicked ${i} times`;
});
In modern web applications which are driven by constant user interactions with a myriad of elements throughout the DOM, properly registering event listeners and managing state among all of the possible async operations becomes an arduously tedious task. This is one of the reasons for the success of popular front-end frameworks such as Google’s Angular1 and Facebook’s React,2 which provide data-binding in MVC-like patterns. Essentially, they do all of the heavy lifting required to wire up event listeners between views, models, and controllers, so that developers working in JavaScript can simply update model objects and have the effects automatically applied to appropriately render in the DOM. Two-way data-binding works similarly in the opposite direction: for example, for a text input which is bound to a certain model property, the model object will be automatically updated as the user types into it.
The abstractions these libraries provide indeed make possible large-scale applications that otherwise would simply not be feasible to build or maintain. The JavaScript ecosystem continues to evolve at a rapid pace – some new library or tool or framework is bound to come along almost every day. Some will catch on and build their own passionate ecosystem, some will become incredibly popular for but a brief flash of time; the majority will never reach the eyes of more than a few people. However magical any framework may seem, or what conventions it employs or abstractions it provides, it is important to remember that, under the hood, they’re all just JavaScript.
Investing time in learning the mechanisms and intricacies of the language itself and the web platform in which it operates is the best way to ensure that your knowledge and skills will last well beyond today’s hottest frameworks, and set a foundation of understanding for the rest of your career.