Fundamentals of Web Application Development · DRAFTFreeman

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.

dom t1 text t2 text t3 text t4 text t5 text t6 text t7 text t8 text t9 text t10 text t11 text p1 p p1->t2 p2 p p2->t3 p2->t5 a a p2->a div1 div div2 div div1->div2 th1 th th1->t6 th2 th th2->t7 tr1 tr tr1->th1 tr1->th2 tr2 tr td1 td tr2->td1 td2 td tr2->td2 tr3 tr td3 td tr3->td3 td4 td tr3->td4 td1->t8 td2->t9 td3->t10 td4->t11 body body body->p1 body->p2 body->div1 h1 h1 body->h1 table table body->table h1->t1 a->t4 thead thead table->thead tbody tbody table->tbody thead->tr1 tbody->tr2 tbody->tr3
Effective DOM tree created by the above document

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:

%3 p p (DOM node instance) HTMLParagraphElement HTMLParagraphElement p->HTMLParagraphElement n Node EventTarget EventTarget n->EventTarget HTMLElement HTMLElement HTMLParagraphElement->HTMLElement Element Element HTMLElement->Element Element->n Object Object EventTarget->Object
Inheritance hierarchy for a p element instance

For reference, here’s what our document looks like when rendered in the browser:

dom render 1
Render 1

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) []

NodeLists 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!

dom render 2
Render 2

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.