The Document Object Model (DOM)

The document object model is the data structure that a browser reads to display content in a browser. Often referred to as the DOM it is a data structure that includes data we see in the <body> tag and data we don't see in the <head> tag.

Learning Objectives

  1. Use the API methods on the document object that will enable programatic operations; create nodes, read values out of the DOM, update parts of the DOM tree, and delete nodes - (CRUD).
  2. Evaluate the benefits and problems with manipulating the DOM using javascript in this way.

The history of the DOM

HTML existed long before the mid-90s and web pages could be styled in rudimentary ways, with styles controlled by HTML (e.g. <center>Centred text</center>). Later, in 1996, CSS would be released and would allow developers to decouple presentation (CSS) from document structure (HTML). The structure of web pages and styling was now standardised, but what about interactivity? What if the developer wanted to tell the user what the date and time was, or open a new window programmatically?

Around this time, Microsoft and Netscape were duking it out for browser superiority and it was in fact a Netscape developer (Brandan Eich), who created a client-side scripting language in ten days that would later become JavaScript. It was initially called Mocha, then LiveScript, then later, JavaScript. It wasn't without its problems, but JavaScript enabled designers and developers to easily add dynamic features to their websites. For this reason, JavaScript rapidly emerged as the standard scripting language for the web.

As we learned above, Microsoft and Netscape were the main players in the mid-90s browser market. Each company favoured their own implementation of JavaScript; Microsoft had JScript and Netscape and JavaScript. Microsoft's JScript was actually just JavaScript but with a different name to avoid trademark issues.

The first unofficial version of the DOM was called the Legacy DOM and this paved the way for client-side validation and other visual effects. At this point though, implementations of web pages with dynamic behaviour started to diverge. Whilst some Legacy DOM operations remained compatible, other dynamic extensions did not.

By the time JavaScript was standardised in 1997, the W3C set out its recommendations for the stardardisation of the DOM. The DOM is now standardised across all browsers and is maintained by the Web Hypertext Application Technology Working Group (WHATWG).

The DOM tree

The document is a tree like structure. The root of the tree structure is the <html> tag. Tags throughout the document have an opening and closing parts. They are pairs with the opening tag enclosed by angle brackets and the closing tag enclosed by angle brackets and a leading 'forward slash'. This is also called a node.

<html></html>

Nodes have 'children'. The <html> tag has 2 children: <head> and <body>. A node has children by enclosing them between its opening and closing tags. A node can have children, and those children can have children. Eventually a node will have content.

<html>
    <head>
        <title>My Title</title>
    </head>
    <body>
        <h1>A Heading</h1>
        <a href="/link-url">Link text</a>
    </body>
</html>

Below is a visual representation of the HTML above. Can you see where it gets the name 'tree' from?

An diagram of the document object model for a simple HTML page

The great power of XML like structures of which HTML is an example is the way additional attributes can be added to the node. In the example above the <button> element has content; the text "Click Me" and 2 attributes, a type attribute that makes it a submit button, and a disabled attribute that prevents the button being clickable. These html attributes are key value pairs (key="value") and they are added to the opening tag.

It is very helpful to have this mental picture of the DOM as a tree. You can explore the tree like structure of HTML documents for yourself by adding the 'DOM node tree viewer' extension to chrome. With this extension you'll be able to explore the DOM tree of any site you visit.

❓What other tree like structures can you think of? (not including actual trees)

Accessing the DOM with JS

When we write HTML we are constructing this tree. Once it is loaded in the browser we can programmatically access this tree structure using Javascript. The first thing to learn is how to get our javascript running in the browser along side the DOM.

Loading JS in the browser

There are 3 ways to write javascript and have it run in the browser.

  1. inline
  2. in a <script> tag
  3. using src attribute

Inline

You can write javascript as a value for some HTML attributes. For example you can write javascript to handle a onclick event.

<button id="JS-test-id" type="button" onclick="(event) => {console.info(event.target.id)}">Test JS</button>

This HTML element will be rendered with an event listener listening for the 'click' event on the button, we have also provided a line of javascript code for that handler to run - it should log in the console the id of the element that was just clicked - JS-test-id. The event object you see above is injected by the browser.

script tag

You can open a <script> tag and inside this tag write javascript. This block of javascript code is run when the page loads.

<script>
    const socket = new Websocket('ws://localhost:3000/')
    socket.on('message', msg => {
        renderChatMessage(msg.data)
    })
</script>

from a source file

You can write your javascript in a source file and serve that file via a web server so the file has a URL (universal remote locator or 'web address'). A <script> tag that references a file's URL as a value in its src attribute will load that file in the browser.

<script src="http://localhost:3000/js/main.js"></script>

The content of the file becomes the content of the <script> tag and is executed when the file has finished loading.

Reference the DOM in javascript

Javascript that runs in the browser will be able to reference other javascript objects that are injected into the global scope by the browser. For example the document object.

We can write code that uses functions that are properties of this globally scoped object. Global scope in the browser means the document object can be referenced by any of your code any where in your code.

Lets create a programmatic reference to a DOM node based on it's id.

const element = document.getElementById('JS-test-id')

I now have a reference to a DOM element I can manipulate it programmatically.

element.classList.add('active')

That code just added a class called 'active' to the button element.

<button id="JS-test-id" class="active">Test JS</button>

Changed your mind? What if you wanted to remove a class?

element.classList.remove('active')

There is a very extensive API for the document object. For our purposes we want to learn to do the following: Create, read, update and delete nodes thus we will be able to manipulate the DOM using javascript.

Manipulating the DOM with JS

The code examples that follow will look at how to take a simple JSON data structure and create, read, update and delete that data structure in the UI layer of the browser.

Create

The first thing we are going to do is create a new node in the DOM tree that will be the root node for all the nodes that will be our menu items. The document API exposes a few functions we can use to create DOM nodes.

const menuListElement = document.createElement("ul")
document.body.append(menuListElement)

Above we are appending a new node menuListElement to the root node body. The content of this node is going to be other nodes, or children. This is an important thing to consider as we work with our tree like structure. The content of this node is not 'content', it's other nodes. We will create DOM nodes that render the data of each item in the data array. These will be 'leaf' nodes; they will terminate the branching by yielding the content. Our aim is to see and read these menu items with their prices in the browser.

[
    {
        "item": "Mackerel, wakame dashi, Cornish baby beetroots",
        "price": 17.00,
        "isVegetarian": false
    },
    {
        "item": "Heritage mixed beetroots, goats curd dumpling, vanilla",
        "price": 25.00,
        "isVegetarian": true
    },
]

As we iterate over the array of menu items we need to create valid DOM nodes for each item and append them (add them) to the parent node menuListElement. There are a number of steps to do this if we are to honour the data structure of the DOM tree.

Create the <li> element

We have to create another valid DOM node. That could be a <li>, <article>, or any HTML tag in this list. As this will be an item in a list I'm going to create a <li> element.

const menuItemElement = document.createElement("li")

The variable menuItemElement is now a valid DOM element. We have created a node that can be added to the DOM tree. At the moment it is empty; we need to add the child nodes that this node is the parent of. There are 3 pieces of content to display so this node does not actually have content it just has 3 child nodes. In the code example below, I am iterating over the JSON above and dealing with each item in the JSON array using forEach.

menuItemsData.forEach(data => {
    const menuItemElement = document.createElement("li")
    const item = document.createTextNode(data.item)
    const price = document.createTextNode(` £${data.price}`)
    
    menuItemElement.appendChild(item)
    menuItemElement.appendChild(price)
    menuElement.appendChild(menuItemElement)
})

In the first part of the function above we create content nodes using createTextNode, then we do the work of assembling our subtree structure by appending the item and price text to the <li>, then finally adding the <li> to the parent menuElement.

Use logic and add attributes

To take this a step further we have a boolean value in our data so if a dish is vegetarian we could display a small icon. As we are writing javascript code we can use logic so only dishes with the isVegetarian flag set to true will append an <img> to the <li>.

if(data.isVegetarian) {
    const isVegIcon = document.createElement("img")
    isVegIcon.setAttribute("src", "http://clipart-library.com/images/zTXodk5Ec.png")
    isVegIcon.setAttribute("style", "height:1rem;margin-left:.25rem;")
    menuItemElement.appendChild(isVegIcon)
}

Can you see the way we can manipulate the attributes of the DOM elements we are creating? For example adding the src url of an image, and adding inline CSS styles. Powerful.

Read

Can you see your menu items rendered in the browser?

We'll be able to now read from the DOM tree. If I wanted to read the price of the last menu item I can access and read the content of a DOM node like this.

const lastListElement = document.querySelector("ul li:last-child")
lastListElement.innerText.substring(lastListElement.innerText.length - 3)

To access elements in the DOM you can assign a unique id to the element i.e. my-id. To select this element use document.getElementById('my-id'). To access elements in the DOM without using document.getElementById you can use document.querySelector combined with CSS selectors to help you target elements in the DOM tree.

Update

If something in our menu list needs to change we can do the work of updating what is currently in the DOM. For this example we are going to update the price of the first item in the list to be £20.00. Remember the source of truth for our menu is the JSON data structure, not the UI layer. To make an update we modify the underlying data, then use that data as a bases to render our UI representation.

How would you go about updating the JSON data structure?

To re-render the list from a new version of the data we are going to use another part of the DOM API called DocumentFragment. A common use for DocumentFragment is to create an instance of a fragment, then assemble a DOM subtree within it using javascript forEach, then append or insert the fragment into the DOM using Node interface methods such as appendChild() or insertBefore(). Doing this moves the fragment's nodes into the DOM, leaving behind an empty DocumentFragment. Because all of the nodes are inserted into the document at once, only one reflow and render is triggered instead of potentially one for each node inserted if they were inserted separately. This is more performant.

const updateFragment = new DocumentFragment()

menuItemsData.forEach(data => {
    const menuItemElement = document.createElement("li")
    const item = document.createTextNode(data.item)
    const price = document.createTextNode(` £${data.price}`)
    
    menuItemElement.appendChild(item)
    menuItemElement.appendChild(price)
    
    updateFragment.appendChild(menuItemElement)
})

menuElement.append(updateFragment)

Destroy

The final operation to learn about is deleting elements from the DOM tree. Lets remove the first item on the menu. Start with your data model. Remove the item from the array (using splice). You'll have to figure out the index of the item that was clicked. You can then just re-render the list.

Sometimes you might want to remove a node in the DOM tree. You can select the node and call remove() on it. In the code example below we remove the last item in the list.

document.querySelector('li:last-child').remove()

Assignment

Can you manipulate the DOM tree using javascript? You should be able to make a GET request to your server in order to retrieve restaurant data from your API server. You can use the Fetch API for this.

Extension