Skip to content

Week 5 - Document Object Model

All the JavaScript that we have read up until this point could be run either in the browser or with NodeJS. It has been Core JavaScript which works the same in both places.

On top of core JavaScript it depends on which engine you are using. In the browser we have access to the Web APIs. In NodeJS there are a bunch of Node APIs.

You can attach one or more JavaScript files to your your webpage by using a <script> tag.

<script src="myscript.js"></script>

The recommended place to initially add your scripts is inside the body element, after all your other HTML.

<body>
<p>Some content</p>
<script src="myscript.js"></script>
</body>

OR what we will be doing in the near future, so you should start doing it now, is putting all your script tags inside the <head> and adding the type="module" attribute. This will allow us to import scripts.

<html>
<head>
<script src="myscript.js" type="module"></script>
<!-- wait until the DOM has loaded & script will be a protected module -->
<script src="myscript.js" defer></script>
<!-- wait until after the DOM has loaded -->
</head>
<body></body>
</html>

Running JS in the browser

Another recommended feature that you can add to your script tag is the defer attribute. This will tell modern browsers to wait until all your HTML has loaded before trying to run your script. The type="module" attribute mentioned above has the same effect as the defer attribute.

defer and async

If you add multiple JavaScript files to the same HTML file they will, by default, all be able to see each other’s code. If you declare a variable or function in one of the files then the other file will be aware of them. All the code from each of the files will be loaded into the same global scope.

The exception to this is if you are using ES Modules. This is a recent but fully supported feature added in the last few years. You can add type="module" to your initial JavaScript file <script> tag, and then it will be able to import other files as modules. Each JavaScript file module will have it’s own scope.

Here is a quick simple example of the import process. Say we have an HTML script tag that loads main.js.

<script src="js/main.js" type="module"></script>

Then, inside the main.js file we can import a file from the same directory called awesome.js.

import { message } from './awesome.js';
//import a function called message from inside file
message('Emma');
//call the function after it is imported

Inside the awesome.js file we have to explain that the message function is allowed to be exported.

function message(name) {
console.log(`${name} is awesome!`);
}
export { message };

We can export any variables and functions that we want from a JS file. Then the variables and functions can be import into any file, as long as the file doing the importing was loaded by the HTML file with type="module".

We will be talking about modules a lot more later in the semester as our code becomes more complex.

When the browser reads an HTML file it creates an Object model representing all the content on the page. It builds a tree structure that helps it to know which elements are nested inside each other, which elements are parents and which are children.

This is a visual representation of how a browser sees the page. You can think of each indent as the start of a new branch in the DOM tree. Inside the html object, there are two children - head and body. Inside the head are two more children - title and link. title has a child, which is #text. The link element has no children. Inside body > header > h1 there are two children - img and #text. The img and the #text are siblings.

doctype
html
head
title
#text
link
body
header
h1
img
#text
nav
a
#text
a
#text
a
#text
main
h2
#text
p
#text
footer
p
#text

You need to be able to visualize every page in this manner. Understand when you are searching for things or altering things that you will be working with parents, children, and siblings.

One of the things that sets NodeJS and JavaScript in the browser apart is the DOM. NodeJS does NOT have access to the DOM.

The DOM is NOT part of core JavaScript.

The DOM is an add-on that browsers get as part of the window object.

Everything you do in the browser, which is not part of core JavaScript, exists inside the window object. Because of this, You can write window as the first step in accessing an Object in the DOM OR you can omit it.

window.alert('This is a pop up message');
alert('This is also a pop up message');
//both these lines access the same alert method

Each one of the things listed above, in the DOM diagram, is a Node. There are several kinds of types of nodes, including ElementNode, TextNode, DocumentFragment, and Comment. There are others but these are, by far, the most common.

ElementNodes are the tags that you write in your HTML document.

TextNodes are the strings that you write between the opening and closing tag in your HTML document.

There are JavaScript node properties and methods which work on all the types, some that work only on Element nodes, and some that work only on Text nodes.

Inside any DOM ElementNode you can add or read attributes. Many of the attributes that you write in your HTML will have a corresponding attribute in JavaScript. Eg: if your element in the HTML has an id attribute, then when you reference that element in your JS, it will have an id property.

There are a few exceptions to the naming of the properties. The most notable is the class attribute in HTML. In JavaScript, you have to use the property name className because class is a reserved keyword.

There are also dataset properties, which are properties that you can invent and insert into the HTML. These attributes all have a name that begins with data-.

<p data-me="steve" data-time="3pm">This paragraph has dataset attributes.</p>

In JavaScript we can add attributes, update the value of attributes, check if attributes exist, read the value of attributes or remove attributes. We will talk more about these methods soon.

One of the objects inside window that you will access most is the document object. It contains all the methods and properties for working with HTML and CSS through JavaScript.

let h = document.querySelector('h1');
//find the first h1 element on the page
console.log(h.textContent);
//output the #text inside the h1 element.

We could, but typically do not, put window in front of the document.querySelector command.

Intro to the DOM

It is important to remember that there are two main types of Nodes in the DOM - TextNodes and ElementNodes.

Some of the properties and methods that you use will be looking at Nodes and some will be looking at ElementNodes. The general object type Node refers to both kinds, whereas Element nodes refer just to the tags like <p> or <div> or <ul>.

When you look at the tree structure created as the DOM, this distinct is important. In the following example, the <div> element has TWO children, but FIVE childnodes. The carriage return and spaces that come after <div>, </h2>, and </p> count as text nodes and therefore are childnodes.

<div>
<h2>A Heading</h2>
<p>a paragraph</p>
</div>

The <h2> and <p> element each have 1 children and 1 childnode.

Nodes vs Elements

Nodelists vs HTMLCollections

ChildNodes vs children

A task that you will frequently do in JavaScript is locating parts of your webpage so you can read the content or update the content. Eg: Find the sidebar so you can add new links. Find the main <ul> so you can add a new list of products.

Traversing the DOM

Finding HTML Elements

querySelector, querySelectorAll, and getElementById

Section titled “querySelector, querySelectorAll, and getElementById”

The two methods we use the most to find elements on the page are document.querySelector and document.querySelectorAll. The difference between them is that querySelector finds the first match starting at the top of the page and querySelectorAll finds ALL the matches.

If you want to change content on the page, find out what the content inside an element is, move an element, or delete an element then you need to find it first.

let p = document.querySelector('p');
//find the first paragraph on the page
let ps = document.querySelectorAll('p');
//find ALL the paragraphs on the page

The querySelector method will return a single ElementNode.

The querySelectorAll method will return a NodeList. A NodeList can be thought of as an Array of ElementNodes.

When you want to make changes to a webpage or read the contents of a webpage with JavaScript then you need a way of locating elements. The three methods that you will use most frequently when accessing web page content are:

let element = document.querySelector(cssSelector); //returns a single element node
let nodes = document.querySelectorAll(cssSelector); //returns a NodeList
let element = document.getElementById(id); //returns a single element node

querySelector( ) and getElementById( ) search the webpage for the first element node that matches.

let anchor = document.querySelector('.main p a');

In the example above, the variable anchor will contain either null, if no match found, or the first anchor tag inside a paragraph inside an element with the className .main. You can pass ANY valid CSS selector, as a string, to this method.

let foot = document.getElementById('footer');

In this example, the variable foot will contain either null or the element on the page that has the id footer. You pass any string that should match an id of an element on your page.

querySelectorAll( ) will return a NodeList, which is similar to an Array in that it is a numbered list, but it can only contain HTML Nodes. When you call querySelectorAll( ), it will ALWAYS return a NodeList. The NodeList might be empty, but it is still a NodeList with length zero.

let paragraphs = document.querySelectorAll('#main p');

The above example will return a NodeList containing ALL of the paragraphs inside the element with the id main. Notice that this is a valid CSS selector, just like the querySelector( ) call above.

If you want to loop through all the elements inside of paragraphs, you could use a for loop.

Alternatively, a NodeList has a forEach method that works just like the Array forEach method.

let paragraphs = document.querySelectorAll('#main p');
//regular function version
paragraphs.forEach(function (p, index) {
p.textContent = `Paragraph ${index} updated by the first forEach`;
});
//arrow function version
paragraphs.forEach((p, index) => {
p.textContent = `Paragraph ${index} updated by the second forEach`;
});

There are a couple other methods that can be used to get a list of elements - getElementsByTagName() and getElementsByClassName(). As the names imply you would give a tag name or a class name to find the matches. These methods existed before querySelector and querySelectorAll were added to JavaScript and it is rare to find them actually used.

If you had a variable called someElement you could use any of the following properties.

  • someElement.parentElement Get the parent element that wraps someElement
  • someElement.childNodes Get a list of all the text and element children of someElement
  • someElement.children Get a list of all the element children of someElement
  • someElement.nextSibling Get the text or element sibling that comes after someElement
  • someElement.nextElementSibling Get the element sibling that comes after someElement
  • someElement.previousSibling Get the text or element sibling that comes before someElement
  • someElement.previousElementSibling Get the sibling element that comes before someElement
  • someElement.firstElementChild Get the first element child of someElement
  • someElement.lastElementChild Get the last element child of someElement

Finding Parent Elements

closest and matches

There are a number of ways that you can create new content on a webpage. The two main approaches are:

  1. You can create Elements with the document.createElement() method and text nodes with the document.createTextNode() method. After creating the elements you can add attributes like className. With different nodes created, you can use the append() or appendChild() method to build whatever tree structure you want.
  2. You can create a String that contains whatever HTML you would write if you were putting it in the HTML file. Then, with the append() or setHTML() methods or the innerHTML property you can convert the string into actual nodes in your page. Template strings are commonly used for this approach.
  3. An HTML template can be created and cloned. We will look at this approach more after we cover fetching remote data.

The createElement method will create any HTML element you need. Just pass in the name of the tag and it will return a new element of that type.

The createTextNode method will create a text node. Just pass in the string that you want to use as the text and it will return the new text node.

There is also a createDocumentFragment() method that lets us create a self-removing wrapper for chunks of HTML that will will inject into the page. More about this later.

let p = document.createElement('p'); //creates an Element Node of the type provided
let txt = document.createTextNode('hello world'); //creates a TextNode
let df = document.createDocumentFragment(); //creates a Document Fragment

The elements can have attributes too. For most attributes the common approach is to use the property of the same name.

Let’s use this HTML as the example of what we want to create. We will assume that the main element is already in our HTML file and we want to create and add all the content inside it.

<main>
<div class="header">
<h2>Some Heading</h2>
</div>
<div class="content">
<p><img src="./img/logo.png" alt="Company logo" /> Lorem.</p>
<p>Ipsum.</p>
</div>
</main>

Here is the JavaScript that will create everything inside the <main> element.

let main = document.querySelector('main');
//create the elements
let header = document.createElement('div');
let content = document.createElement('div');
let h2 = document.createElement('h2');
let p1 = document.createElement('p');
let p2 = document.createElement('p');
//approach one to create the text node
let lorem = document.createTextNode(' Lorem.');
//approach two is to use the .textContent property
p2.textContent = 'Ipsum.';
let img = document.createElement('img');
//add the attributes
header.className = 'header';
content.className = 'content';
img.src = './img/logo.png';
img.alt = 'Company logo';
//then start with the innermost nodes and start appending
//if you didn't use p2.textContent then you need to append a textNode
// p2.appendChild(ipsum); //appendChild can only accept one child at a time
// appendChild can be used to append a child TextNode or a child ElementNode
p1.append(img, lorem); //the order is important. append can accept multiple children
content.append(p1, p2);
header.append(h2);
main.append(header, content);
//appending to main is the last step because this adds it to the page

The HTML String approach is the alternative to creating elements. Instead we are writing out the whole string and then asking the browser to do the work of parsing all the elements and appending them at the same time.

Here is the String approach for the same content.

let main = document.querySelector('main');
//use a template string incase you have variables that you want to embedded in the string.
let myhtml = `<div class="header">
<h2>Some Heading</h2>
</div>
<div class="content">
<p><img src="./img/logo.png" alt="Company logo" /> Lorem.</p>
<p>Ipsum.</p>
</div>`;
main.innerHTML = myhtml;
//alternative
main.setHTML(myhtml);

It is important to note that this approach is going to replace any content that is already inside of <main> and not just append it to the existing content.

Manipulating HTML can be done quite easily once you understand the parent-child-sibling relationship between Nodes and the difference between Element nodes and Text nodes.

Once you have found the ElementNode you need, then you can start to manipulate the content.

let content = document.querySelector('p.first');
//find the first paragraph with the class "first".
content.textContent = 'The new paragraph content';
//change the text inside the paragraph
let main = document.querySelector('main');
//find the main element
let p = document.createElement('p');
//create a new paragraph
p.textContent = 'A new paragraph for the page';
//add some text inside the paragraph
main.appendChild(p);
//add the newly created paragraph as the last child of the main element

It is important to note that both the append and the appendChild methods always inject the new child element or child textNode as the LAST child. It will always be added at the bottom of the parent element.

Adjacent DOM Manipulation

More methods for manipulation

If you want to inject your new textNode or element in a location other than the last position, then you need to use the insertBefore(), insertAfter(), insertAdjacentElement(), insertAdjacentText(), or insertAdjacentHTML() methods. The insert before and after methods want a reference element as well as the child node to insert before or after. The insertAdjacent methods want a reference element plus one of four reference positions. Take this HTML as an example:

<h2>Some heading</h2>
<ul>
<li>first item</li>
<li>second item</li>
<li>third item</li>
</ul>
<p>Some more text</p>

If the <ul> is my reference element, I can inject my new element in one of the four positions:

  • before the <ul> starts beforebegin
  • after the <ul> starts but before the first <li> afterbegin
  • after the last <li> but still inside the <ul> beforeend
  • after the <ul> ends but before the <p> afterend

The four strings used as the position values are bolded in the list above.

referenceElement.insertAdjacentElement(position, childElement); //insert Element
referenceElement.insertAdjacentText(position, childTextNode); //insert textNode
referenceElement.insertAdjacentHTML(position, HTMLString); //parse string and insert

InsertBefore and insertAdjacentElement

It is worth noting, when you create an element with createElement or reference and element with querySelector or the other methods, then that element still exists whether it is just in memory or on the page.

let p = document.createElement('p'); //exists in memory
p.textContent = 'I exist!'; //p is still in memory only
let header = document.querySelector('header'); // exists on the page
let main = document.querySelector('main'); // exists on the page
let footer = document.querySelector('footer'); // exists on the page

When you call the append or appendChild or remove or removeChild methods you are actually MOVING the element. You are moving it from memory to the page, from the page to memory, OR from one location in the page to another.

header.append(p); //moved p from memory to inside header on page
main.append(p); //moved p from inside header to inside main
footer.append(p); //moved p from inside main to inside footer
footer.removeChild(p); //moved p from inside footer on page to in memory

The above code sample does NOT create three copies of the paragraph.

If you did want to create copies of the paragraph then you could use the cloneNode method.

let copy1 = p.cloneNode(true); //creates a copy of p and includes anything it contained
let copy2 = p.cloneNode(true); //creates a copy of p and includes anything it contained
//now we have p, copy1, and copy2 as three separate elements

If you want to remove HTML that already exists we have a few options.

  1. With the innerHTML property or the textContent property you can set the content of any element to an empty String.
let div = document.querySelector('div'); //find the first div on the page
let p = div.querySelector('p'); //find the first paragraph inside div
p.textContent = ''; //set the text inside the paragraph to empty
div.innerHTML = ''; //remove all html and text inside div. could also use .setHTML()
  1. Using the remove() or removeChild() methods.
p.remove(); //will remove the paragraph from whatever its parent is.
div.remove(); //will remove div and everything it contains
p.parentElement.removeChild(p); //refer to the parent of the p and use removeChild to target what will be removed
div.removeChild(p); //remove the paragraph from it's parent div

Difference between remove and removeChild

In the list below there are a series of property pairs. The first properties will return a NodeList or a single Node. Remember that Nodes can be element nodes, text nodes, or comments. The second property will return a list of Element Nodes or a single Element Node.

  • node.childNodes v node.children
  • node.firstChild v. node.firstElementChild
  • node.lastChild v. node.lastElementChild
  • node.nextSibling v. node.nextElementSibling
  • node.previousSibling v. node.previousElementSibling
  • node.parentNode v. node.parentElement

These last three node properties return a single piece of information about the node.

node.nodeName; //returns the tagname, if an element node
node.nodeType; //returns the integer referencing the type of node. Eg: 1=element, 3=text
node.nodeValue; //returns the string inside a text node
parentNode.appendChild(newNode); //add a new child node inside the parent
parentNode.removeChild(childNode); //remove the child from the parent
node.replaceChild(newNode); //replace one node with a new one
parentNode.contains(node); //checks if the node is a descendant of the parent
node.hasChildNodes(); //returns a boolean indicating if the node has child nodes
node.hasAttributes(); //returns a boolean if the node has attributes
parentNode.insertBefore(newNode, referenceNode); //inserts a new node into the DOM
//immediately before the reference node

More DOM methods

Injecting Strings into HTML

Creating HTML Content with JS

Here is a list of common properties that map to attributes in the HTML as well as some methods that you could use to manipulate the CSS classes assigned to an element.

let c = element.className; //get or set the value of the class attribute
//one string with all the class names
let list = element.classList; //get the list of css classnames assigned to the element (a DOMTokenList)
element.classList.add(className); //add a new css class to the list
element.classList.remove(className); //remove a single css class from the list
element.classList.replace(oldClassName, newClassName);
element.classList.contains();
element.classList.toggle(className); //remove if exists, add if it doesn't exist
let nm = element.tagName; //retrieve the HTML element's tag name eg: P, LI, A, DIV
element.href = 'http://www.example.com';
element.src = 'http://www.example.com/photo.jpg';
element.id = 'bob';
element.title = 'mouseover text';
element.alt = 'alternate description for image'; //accessibility matters
element.style = 'display:none;'; // create an inline style. Not recommended.

There are a few specific methods for working with HTML attributes.

let main = document.querySelector('main'); //first main element on page
let img = document.querySelector('img'); //first image element on page
let mi = main.querySelector('main img'); //first image element inside main
mi.setAttribute('src', 'http://www.example.com/photo.jpg'); //set the image's src attribute
let title = mi.getAttribute('title'); //retrieve the value of the title attribute
let hasAlt = mi.hasAttribute('alt'); //boolean indicating if the img has an alt attribute
mi.removeAttribute('href'); //remove the href attribute if it exists

Having the ability to create your own custom attributes can be a very useful tool and allow you to store things like product reference ids in the HTML. To address this need, HTML5 standardized an approach to inventing custom attributes - called dataset properties.

You are allowed to create your own HTML attributes in the HTML or through JavaScript as long as their name starts with data-. Here is an example of what the HTML would look like.

<p data-price="22.33" data-id="a57dd55d7d8d" data-link="http://abc.io/product">Some product information</p>

And here is the JavaScript that would create that same HTML.

let p = document.createElement('p');
p.setAttribute('data-price', '22.33');
p.setAttribute('data-id', 'a57dd55d7d8d');
p.setAttribute('data-link', 'http://abc.io/product');
document.body.append(p);
//document.body points to the <body> element

It should be noted that ALL dataset property values are strings. If you want to store a numeric or boolean value in a data-set property, that is fine, but you will have convert the value to number or boolean after extracting it.

let priceTxt = p.getAttribute('data-price'); //extract the attribute value
let price = parseFloat(priceTxt); //convert from string to number

A documentFragment is similar to an HTML element in that it can contain other HTML Elements and text nodes. The difference is that they only really exist in memory. They do not appear on the page.

We can use them to transport HTML from memory to the webpage.

Think of the HTML that you want to add to your page as a bunch of sand. The documentFragment is a bucket used to hold the HTML and carry it to the page. When you get to the page, the HTML is dumped out of the bucket on to the page.

let df = new DocumentFragment(); //version one of creating a document fragment
let dfAlt = document.createDocumentFragment(); //version two. Both work
let p = document.createElement('p');
let h2 = document.createElement('h2');
h2.className = 'mainTitle';
h2.textContent = 'The Heading';
p.textContent = 'Lorem Ipsum';
df.append(h2, p); //now the h2 and paragraph are inside the document fragment
document.body.append(df); //now the h2 and paragraph are on the page
//but the document fragment is still only in memory
//the h2 and paragraph were transfer from memory to the body element.

DOM manipulation with DocumentFragments

When you are dynamically creating content for your webpages, you will frequently be looping through an Array of Objects to get the content. Each of the objects will have properties with the same property names. You will take the values of those properties and inject them into the HTML that you are creating.

Let’s say that this is your data which you will use to create the new HTML.

let info = [
{ id: 'a6f7', txt: 'Told ya' },
{ id: 'b5f7', txt: 'Not real data' },
{ id: 'c2f7', txt: 'But it looks like it' },
{ id: 'a1f4', txt: 'And works like it' },
{ id: 'a6ee', txt: 'So we will use it' },
];

Now, use the Array map method to loop through the Objects and build an HTML String.

When the String is fully built, call one of the methods or properties to inject the content once.

//version one
// use the Element.setHTML() method to append a string that includes HTML
let one = document.querySelector('.one');
let html = info
.map((item) => {
let str = `<p data-ref="${item.id}">${item.txt}</p>`;
return str;
})
.join('');
one.setHTML(html);
//version two
// use the innerHTML property to append a string that includes HTML
let two = document.querySelector('.two');
let html2 = info
.map((item) => {
let str = `<p data-ref="${item.id}">${item.txt}</p>`;
return str;
})
.join('');
two.innerHTML = html2;
//version three
//use the DOMParser parseFromString method to convert a string to an HTML document
// and then append the document's body property value
let three = document.querySelector('.three');
let html3 = info
.map((item) => {
return `<p data-ref="${item.id}">${item.txt}</p>`;
})
.join('');
let parser = new DOMParser();
let doc = parser.parseFromString(html3, 'text/html');
three.append(doc.body);
//version four
// use createElement followed by append to create an array of HTML elements
// then use Element.append( ...theArray ) with the spread operator to append the array of Elements
let four = document.querySelector('.four');
let html4 = info.map((item) => {
let p = document.createElement('p');
p.append(item.txt);
p.setAttribute('data-ref', item.id);
return p;
});
four.append(...html4);
// four.append( html4[0], html4[1], html4[2], html4[3] );

The same process can be done using the createElement method and wrapping everything inside a documentFragment. The documentFragment holds all the new HTML in memory and is used to transport all the new HTML to the page. Once the new HTML has been loaded in the page, the documentFragment removes itself and leaves behind the new HTML.

If you want to dynamically style your web page content there are three properties that belong to every Element Node.

  • style
  • className
  • classList

The style property lets you set the value of any single CSS property on any Element. Inside the style property is a list of every CSS property name. However, the names are slightly different. Any CSS property name that includes a hyphen, like background-color, will be written as a single camel-case name - backgroundColor.

Every value is a string and needs quotation marks.

let elem = document.querySelector('p'); //get the first paragraph
elem.style.border = '1px solid red';
elem.style.backgroundColor = 'khaki';

The className property lets you set the contents of the HTML attribute class="". We can’t use class as an HTML attribute from within JavaScript, because class is a reserved keyword.

let elem = document.querySelector('p'); //get the first paragraph
elem.className = 'big red content';

The classList property is a DOMTokenList, which is similar to an Array of all the values inside the HTML class="" attribute. The classList property has a series of methods that we can call to manage and update the list of values.

let elem = document.querySelector('p'); //get the first paragraph
elem.classList.add('active'); // add the class `active` to the HTML
elem.classList.remove('active'); // remove the class `active` to the HTML
elem.classList.toggle('active'); // add the class `active` to the HTML if it doesn't exist, remove if it does.
elem.classList.contains('active'); // check IF the class `active` is assigned to the element
elem.classList.replace('active', 'inactive'); // replace the class `active` with the class 'inactive'

The classList methods are the most efficient way to update your DOM element styling and are considered the best practice over style or className.