facebook google plus twitter
Webucator's Free JavaScript Tutorial

Lesson: The HTML Document Object Model

Welcome to our free JavaScript tutorial. This tutorial is based on Webucator's Introduction to JavaScript Training course.

The HTML Document Object Model (DOM) is a W3C standard that defines a set of HTML objects and their methods and properties. JavaScript can be used to access, to create, and to destroy these objects, to invoke their methods, and to manipulate their properties.

A subset of the object hierarchy is shown below:HTML DOM

This lesson is concerned with the different ways of identifying and manipulating document nodes. While we have looked at some of these features in previous lessons, we present them here together for completeness.

Lesson Goals

  • Learn to work with the HTML DOM.
  • Learn to access specific nodes.
  • Learn to access nodes by tag name, class name, and CSS selector.
  • Learn to access nodes hierarchically.
  • Learn to create and remove nodes.
  • Learn to dynamically create an HTML page.

CSS Selectors

We will start with an introduction/review of CSS selectors as we can make use of them to access elements with JavaScript. There are several different types of selectors, including:

  • Type
  • Descendant
  • Child
  • Class
  • ID
  • Attribute
  • Universal

Selectors identify the element(s) affected by the CSS rule.

Type Selectors

Type selectors specify elements by tag name and affect every instance of that element type. The rule below specifies that the text of every p element should be darkgreen and use a 10-point Verdana font:

p {
	color: darkgreen;
	font-family: Verdana;
	font-size: 10pt;
}	  

Descendant Selectors

Descendant selectors specify elements by ancestry. Each "generation" is separated by a space. For example, the following rule states that strong elements within p elements should have red text:

p strong {
	color: red;
}

With descendant selectors generations can be skipped. In other words, the code above does not require that the strong element is a direct child of the p element.

Child Selectors

Child selectors specify a direct parent-child relationship and are indicated by placing a > sign between the two tag names:

p > strong {
	color: red;
}

In this case only strong elements that are direct children of p elements are affected.

Class Selectors

In HTML, almost all elements can take the class attribute, which assigns a class name to an element. The names given to classes are arbitrary, but should be descriptive of the purpose of the class. In CSS, class selectors begin with a dot. For example, the following rule creates a class called "warning," which makes the text of all elements of that class bold and red:

.warning {
	font-weight: bold;
	color: #f00;
}

Following are a couple of examples of elements of the warning class:

<h1 class="warning">WARNING</h1>
<p class="warning">Don't go there!</p>

If the class selector is preceded by an element name, then that selector only applies to the specified type of element. To illustrate, the following two rules indicate that h1 elements of the class "warning" will be underlined, while p elements of the class "warning" should be bold, but will not be underlined:

h1.warning {
	color: #f00;
	text-decoration: underline;
}
				
p.warning {
	color: #f00;
	font-weight: bold;
}

Because both rules indicate that the color should be red (#f00), this could be rewritten as follows:

.warning {
	color: #f00;
}
				
h1.warning {
	text-decoration: underline;
}
				
p.warning {
	font-weight: bold;
}

Note that you can assign an element any number of classes simply by separating the class names with spaces like this:

<div class="class1 class2 class3">...

ID Selectors

As with the class attribute, in HTML, almost all elements can take the id attribute, which is used to uniquely identify an element on the page. In CSS, id selectors begin with a pound sign (#) and have arbitrary names. The following rule will indent the element with the "main-text" id 20 pixels from the left and right:

#main-text {
	margin-left: 20px;
	margin-right: 20px;
}

<div id="main-text">
	This is the main text of the page...
</div>

Attribute Selectors

Attribute selectors specify elements that contain a specific attribute. They can also specify the value of that attribute.

The following selector affects all links with a target attribute:

a[target] {
	color: red;
}

The following selector would only affect links whose target attribute is "_blank":

a[target='_blank'] {
	color: red;
}

Now, with that bit of CSS review out of the way, let's move on to the HTML DOM.

The innerHTML Property

Most HTML elements have an innerHTML property, which can be used to access and modify the HTML within an element.

innerHTML Illustration

Given the code:

<p>I <strong>love</strong> JavaScript.</p>

the innerHTML property of the p element would be: I <strong>love</strong> JavaScript.

Tip

You can use the innerHTML property to either get the element's innerHTML value (as shown above) or to set the element's innerHTML value. More on this later in the lesson.

Nodes, NodeLists, and HTMLCollections

In JavaScript, you will see the words Node and NodeList used often. For the most part, you can think of a Node as one of the following:

  1. The document object.
  2. An element.
  3. A snippet of text within an element.

A NodeList is a list of Node elements and is similar to an array.

An HTMLCollection is very similar to a NodeList except that:

  1. HTMLCollections are live, meaning that they take into account page changes. NodeLists are static.
  2. HTMLCollections can only contain element nodes; whereas NodeLists can contain any type of Node; however, most of the time a NodeList will be lists of elements.

Don't Worry

If the difference between Nodes and Elements and between NodeLists and HTMLCollections seems fuzzy to you, don't worry too much about it. For the most part, you can think of Nodes and Elements as interchangeable and NodeLists and HTMLCollections as arrays containing elements. It's not until you get to pretty advanced JavaScript that you have to be able to differentiate between these different types.

Accessing Element Nodes

JavaScript provides several different ways to access elements on the page. We will look at the following methods:

  • getElementById() - returns a single Element Node.
  • getElementsByTagName() - returns an HTMLCollection of Element Nodes.
  • querySelectorAll() - returns a NodeList of Element Nodes.
  • querySelector() - returns a single Element Node.

getElementById()

We have already seen the document.getElementById(id) method, which returns the first element with the given id (there shouldn't be more than one on the page!) or null if none is found. The following example illustrates how getElementById() works:

Code Sample:

HTMLDOM/Demos/get-element-by-id.html
---- C O D E   O M I T T E D ----

window.addEventListener('load', function() {
  var elem = document.getElementById('beatles-list');
  alert(elem.innerHTML);
});
---- C O D E   O M I T T E D ----

<body>
<main>
<h1>Rockbands</h1>
<h2>Beatles</h2>
  <ol id="beatles-list">
    <li>Paul</li>
    <li>John</li>
    <li>George</li>
    <li>Ringo</li>
  </ol>
  <h2>Rolling Stones</h2>
  <ol id="stones-list">
    <li>Mick</li>
    <li>Keith</li>
    <li>Charlie</li>
    <li>Bill</li>
  </ol>
</main>
</body>
</html>

Code Explanation

When this page loads, the following alert box will pop up:getElementById()

getElementsByTagName()

The getElementsByTagName() method of an element node retrieves all descendant (children, grandchildren, etc.) elements that have the specified tag name and stores them in a NodeList, which can be treated like an array of elements. The following example illustrates how getElementsByTagName() works:

Code Sample:

HTMLDOM/Demos/get-elements-by-tag-name.html
---- C O D E   O M I T T E D ----

window.addEventListener('load', function() {
  var elems = document.getElementsByTagName('li');
  var msg = "";
  for (var elem of elems) {
    msg += elem.innerHTML + "\n";
  }
  alert(msg);
});
</script>
<title>getElementsByTagName()</title>
</head>
<body>
<main>
  <h1>Rockbands</h1>
  <h2>Beatles</h2>
  <ol>
    <li>Paul</li>
    <li>John</li>
    <li>George</li>
    <li>Ringo</li>
  </ol>
  <h2>Rolling Stones</h2>
  <ol>
    <li>Mick</li>
    <li>Keith</li>
    <li>Charlie</li>
    <li>Bill</li>
  </ol>
</main>
</body>
</html>

Code Explanation

When this page loads, the following alert box will pop up:getElementsByTagName()

getElementsByClassName()

The getElementsByClassName() method is applicable to all elements that can have descendant elements. It is used to retrieve all the descendant (children, grandchildren, etc.) elements that have a specific class name. For example, the following code would return a NodeList containing all elements of the "warning" class:

var warnings = document.getElementsByClassName('warning');

querySelectorAll() and querySelector()

We can exploit the various CSS selectors (reviewed above) by using querySelectorAll() and querySelector(). Unlike the getElementById(), getElementsByTagName(), and getElementsByClassName() methods, which find elements by one specific value (id, tag name, and class name, respectively), document.querySelector() provides a way to find an element using many different properties of the element, and querySelectorAll() provides a way to find all such elements. For example, the following code would return a node list containing all a elements that are direct children of td elements:

var linksInTds = document.querySelectorAll('td>a');

The document.querySelector() method is the same as document.querySelectorAll() but rather than returning a list, it returns only the first element found. The following two lines of code would both return the first link element found in an td element:

var firstLinkInTd = document.querySelectorAll('td>a')[0];
var firstLinkInTd = document.querySelector('td>a');

Now you have a chance to play with these methods using Chrome DevTools Console. You will start with the following file:

Code Sample:

HTMLDOM/Demos/getting-elements.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../normalize.css">
<link rel="stylesheet" href="../styles.css">
<title>Getting Elements</title>
</head>
<body>
<main>
  <div id="board">
    <div class="row">
      <div class="col">A</div>
      <div class="col">B</div>
      <div class="col">C</div>
    </div>
    <div class="row">
      <div class="col">D</div>
      <div class="col">E</div>
      <div class="col">F</div>
    </div>
    <div class="row">
      <div class="col">G</div>
      <div class="col">H</div>
      <div class="col">I</div>
    </div>
  </div>
</main>
</body>
</html>

Code Explanation

  1. Open HTMLDOM/Demos/getting-elements.html in Google Chrome and open the console:Getting Elements
  2. Using the console, write code to do the following:
    1. Turn the background of the whole board to pink:Getting Elements - board pink
    2. Turn the second row to lime:Getting Elements - middle row lime
    3. Turn the middle cell to white:Getting Elements - middle cell white
    4. Refresh the page and clear the console to start with the original board. Turn the first column pink. There are several ways to do this. Can you figure out more than one?Getting Elements - left col pink
    5. Refresh the page and clear the console to start with the original board. Change the content of the squares from A-I to 1-9:Getting Elements - letters to numbers
  3. Here are possible solutions:
    1. Turn the background of the whole board to pink:Getting Elements - board pink solution
    2. Turn the second row to lime:Getting Elements - middle row lime solution
    3. Turn the middle cell to white:Getting Elements - middle cell white solution
    4. Refresh the page and clear the console to start with the original board. Turn the first column pink:Getting Elements - left col pink solution 1 Three possible solutions:Getting Elements - left col pink solution 1 Getting Elements - left col pink solution 2 Getting Elements - left col pink solution 3
    5. Refresh the page and clear the console to start with the original board. Change the content of the squares from A-I to 1-9:Getting Elements -letters to numbers solution

Accessing Elements

Duration: 10 to 15 minutes.

In this exercise, you will practice accessing elements in JavaScript.

  1. Open HTMLDOM/Exercises/chessboard-table.html in your browser. It contains an 8 x 8 table:Chessboard Exercise
  2. Open HTMLDOM/Exercises/chessboard-table.html for editing.
  3. Add JavaScript so that when the page loads, it checkers the table to look like this:Chessboard Solution

Solution:

HTMLDOM/Solutions/chessboard-table.html
---- C O D E   O M I T T E D ----

  window.addEventListener('load', function(e) {
    var oddrows = document.querySelectorAll('tr.odd');
    var evenrows = document.querySelectorAll('tr.even');
    for (row of oddrows) {
      var evencols = row.querySelectorAll('.even');
      for (col of evencols) {
        col.style.backgroundColor = 'black';
      }
    }
    for (row of evenrows) {
      var oddcols = row.querySelectorAll('.odd');
      for (col of oddcols) {
        col.style.backgroundColor = 'black';
      }
    }
  });
</script>
---- C O D E   O M I T T E D ----

Code Explanation

The solution shown here is just one of many ways to do this.

Dot Notation and Square Bracket Notation

In the first lesson of this course we took a look at two ways in which we can access elements in JavaScript: dot notation and square bracket notation. Let's review these concepts again.

Dot notation lets us refer to hierarchical DOM elements starting with the top-most element (window) then a set of dot-separated names, referencing elements by their name. For instance, to get an input element with the name fname inside a form with the name loginform, we might use the following (as long as there are no hyphens in the names):

window.document.loginform.fname

Collections of Elements

A document can have multiple form elements as children. We call this the document's forms collection. We can reference the specific form by its order on the page. Like arrays, collections in JavaScript start with index 0, so the first form on the page would be forms[0].

window.document.forms[0].fname

window is Implicit

As window is the implicit top-level object, we don't have to refer to it explicitly. The above code samples could be written as:

document.loginform.fname
document.forms[0].fname

Similarly, we can reference objects with square bracket notation, where the key is the name of the element:

document['loginform']['fname']

This is equivalent to the dot-notation references we showed earlier and can be used interchangeably.

Let's play with this a little in the Chrome DevTools Console using the following file:

Code Sample:

HTMLDOM/Demos/forms.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../normalize.css">
<link rel="stylesheet" href="../styles.css">
<title>Forms</title>
</head>
<body>
<main>
  <form name="form-a">
    <input name="fname">
  </form>
  <form name="form-b">
    <input name="fname">
  </form>
  <form name="form-c">
    <input name="fname">
  </form>
  <form name="form-d">
    <input name="fname">
  </form>
</main>
</body>
</html>

Code Explanation

Notice the file has four form elements named "form-a", "form-b", "form-c", and "form-d". Each of those forms has an input element named "fname".

  1. Open HTMLDOM/Demos/forms.html in Google Chrome.
  2. In the console, type document.forms; and press Enter. Then click on the triangle (circled below) to expand the collection.Forms CollectionNotice that you see both ways of referencing the forms, by index (0, 1, 2, and 3) and by name (form-a, form-b, form-c, and form-d).
  3. Now type document["forms"]; and press Enter and notice that you get the same result, demonstrating that you can use dot and square-bracket notation interchangeably.
  4. Now run each of the following and notice that they all deliver the first form:
    1. document.forms[0];
    2. document["form-a"];
    document['form-a']
  5. However, if you try to access the same form using dot notation you will get an error: document.form-a errorsThis is because of the hyphen in the name. It reads this as "document.form minus a" and errors because a is undefined. So, when using hypens in names, you should use square-bracket notation or use another technique for getting the objects.
  6. You can use either dot notation or square-bracket notation to access the "fname" input elements, because the name doesn't contain a hyphen:fname inputs
  7. Enter your name in the first form's textbox and type document.forms['form-a']['fname'].value (or one of the other variations) at the console: input value
  8. Now use JavaScript to set the value of fname in form-b: input value form-b

Accessing Elements Hierarchically

JavaScript provides a variety of methods and properties for accessing elements based on their hierarchical relationship. The most common are shown in the table below:

Properties for Accessing Element Nodes
Property Description
children A collection of the element's child elements.
firstElementChild A reference to an element's first child element. The equivalent of children[0].
lastElementChild A reference to an element's last child element. The equivalent of children[children.length-1].
previousElementSibling A reference to the previous element at the same level in the document tree.
nextElementSibling A reference to the next element at the same level in the document tree.
parentNode A reference to an element's parent node.

What is a node?

A node is an object in the document tree. Elements, attributes, and text snippets are all examples of nodes. While there are some obscure exceptions, you can generally expect the parentNode of an element to be an element.

The children property returns a collection of element nodes. The other properties return a single element node.

These properties provide a flexible way to get elements on the page, relative to their parents, siblings, or children. We can do anything with the returned elements that we did previously with getElementById(), querySelector() and the other methods - set the background color, change the font style, etc.

Let's take a look at how we might use these properties:

Code Sample:

HTMLDOM/Demos/elem-hierarchy.html
---- C O D E   O M I T T E D ----
<script>
  function modify() {
    var list = document.getElementById('list');
    var liFirst = list.firstElementChild;
    liFirst.style.backgroundColor = 'pink';
    var liLast = list.lastElementChild;
    liLast.style.backgroundColor = 'aqua';
    var siblingPrev = liLast.previousElementSibling;
    siblingPrev.style.backgroundColor = 'lime';

    for (item of list.children) {
      item.innerHTML += ' - check';
    }
  }

  window.addEventListener('load', function() {
    var goBtn = document.getElementById('btn-go');
    goBtn.addEventListener('click', modify);
  });
</script>
<title>Element Hierarchy</title>
</head>
<body>
<main>
  <button id="btn-go">Go</button>
  <ul id="list">
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
    <li>Item 4</li>
    <li>Item 5</li>
  </ul>
</main>
</body>
</html>

Code Explanation

Our simple page displays a button and five unordered list items, with text "Item 1", "Item 2", etc.

Clicking the button calls the function modify(), which does the following:

  • Gets the first child of the list using firstElementChild, and sets its background to pink.
  • Gets the last child of the list using lastElementChild, and sets its background to aqua.
  • Gets the next-to-last child of the list using previousElementSibling (relative to the already-gotten liLast), and sets its background to lime.
  • Loops through all the list items (children of the list) adding " - check" to the innerHTML.

We'll ask you to try out these properties in the next exercise.

Working with Hierarchical Elements

Duration: 10 to 15 minutes.

You will start with the code shown below:

Code Sample:

HTMLDOM/Exercises/elem-hierarchy.html
---- C O D E   O M I T T E D ----
  function create() {
    var board = document.getElementById('board');
    var topRow = board.firstElementChild;
    var trLeftCol = topRow.firstElementChild;
    trLeftCol.style.backgroundColor='rgba(0, 255, 255, .5)';
    var trCenterCol = trLeftCol.nextElementSibling;
    trCenterCol.style.backgroundColor='rgba(102, 255, 255, .5)';
    var trRightCol = topRow.lastElementChild;
    trRightCol.style.backgroundColor='rgba(204, 255, 255, .5)';
  }

  window.addEventListener('load', function() {
    var goBtn = document.getElementById('btn-go');
    goBtn.addEventListener('click', create);
  });
---- C O D E   O M I T T E D ----
  <button id="btn-go">Go</button>
  <div id="board">
    <div class="row">
      <div class="col">A</div>
      <div class="col">B</div>
      <div class="col">C</div>
    </div>
    <div class="row">
      <div class="col">D</div>
      <div class="col">E</div>
      <div class="col">F</div>
    </div>
    <div class="row">
      <div class="col">G</div>
      <div class="col">H</div>
      <div class="col">I</div>
    </div>
  </div>
---- C O D E   O M I T T E D ----

Code Explanation

rgba(R, G, B, A) Functional Notation

We are using rgba(R, G, B, A) functional notation in this exercise. R, G, and B indicate the amount of Red, Green, and Blue in the color. A indicates the opacity level: 0 (fully transparent) to 1 (full opacity).

In this exercise, you will practice working with JavaScript's hierarchical elements.

  1. Open HTMLDOM/Exercises/elem-hierarchy.html in the browser, click on the "Go" button, and notice how the background colors of the first row's cells change.Hierarchy Exercise
  2. Note that a click handler has been added to the button so that the function create() is called when the user clicks the button.
  3. Finish the create() function so that each cell has a different color. You can use your own colors or the ones listed below:
    1. rgba(0, 255, 255, .5)
    2. rgba(102, 255, 255, .5)
    3. rgba(204, 255, 255, .5)
    4. rgba(255, 0, 255, .5)
    5. rgba(255, 102, 255, .5)
    6. rgba(255, 204, 255, .5)
    7. rgba(255, 255, 0, .5)
    8. rgba(255, 255, 102, .5)
    9. rgba(255, 255, 204, .5)

Solution:

HTMLDOM/Solutions/elem-hierarchy.html
---- C O D E   O M I T T E D ----
<script>
  function create() {
    var board = document.getElementById('board');
    var topRow = board.firstElementChild;
    var trLeftCol = topRow.firstElementChild;
    trLeftCol.style.backgroundColor='rgba(0, 255, 255, .5)';
    var trCenterCol = trLeftCol.nextElementSibling;
    trCenterCol.style.backgroundColor='rgba(102, 255, 255, .5)';
    var trRightCol = topRow.lastElementChild;
    trRightCol.style.backgroundColor='rgba(204, 255, 255, .5)';

    var middleRow = topRow.nextElementSibling;
    var mrLeftCol = middleRow.firstElementChild;
    mrLeftCol.style.backgroundColor='rgba(255, 0, 255, .5)';
    var mrCenterCol = mrLeftCol.nextElementSibling;
    mrCenterCol.style.backgroundColor='rgba(255, 102, 255, .5)';
    var mrRightCol = middleRow.lastElementChild;
    mrRightCol.style.backgroundColor='rgba(255, 204, 255, .5)';

    var bottomRow = board.lastElementChild;
    var brLeftCol = bottomRow.firstElementChild;
    brLeftCol.style.backgroundColor='rgba(255, 255, 0, .5)';
    var brCenterCol = brLeftCol.nextElementSibling;
    brCenterCol.style.backgroundColor='rgba(255, 255, 102, .5)';
    var brRightCol = bottomRow.lastElementChild;
    brRightCol.style.backgroundColor='rgba(255, 255, 204, .5)';
  }

  window.addEventListener('load', function() {
    var goBtn = document.getElementById('btn-go');
    goBtn.addEventListener('click', create);
  });
</script>
---- C O D E   O M I T T E D ----

Code Explanation

Accessing Attributes

Essentially, all standard attributes of HTML elements can be accessed as properties of the element. For example, given the following link:

<a href="https://www.google.com"
	id="google" target="_blank">Google</a>

We can access the value of the target attribute like this:

var gLink = document.getElementById('google');
console.log(gLink.target);

Likewise, we can set the value of the target attribute using the target property:

gLink.target = "searchWin";

To test this:

  1. Open HTMLDOM/Demos/attributes.html in Google Chrome.
  2. Click on the Google link and notice that it opens in a new window or tab.
  3. Run the code above at the console:Attributes

You can also access and modify attribute values using the following methods and properties:

Methods and Properties for Working with Attributes
Method/Property Description
hasAttribute(attName) Returns a Boolean (true/false) value indicating whether or not the element to which the method is applied includes the given attribute.
getAttribute(attName) Returns the attribute value or null if the attribute doesn't exist.
setAttribute(attName, attValue) Adds an attribute with a value or, if the attribute already exists, changes the value of the attribute.
removeAttribute(attName) Removes the attribute (if it exists) from an element.
attributes Property referencing the collection of an element's attributes.

Creating New Nodes

The document node has separate methods for creating element nodes and creating text nodes: createElement() and createTextNode(). These methods each create a node in memory that then has to be placed somewhere in the object hierarchy. A new node can be inserted as a child to an existing node with that node's appendChild() and insertBefore() methods.

You can also use the appendChild() and insertBefore() methods to move an existing node - the node will be removed from its current location and placed at the new location (since the same node cannot exist twice in the same document).

These methods and some others are described in the table below:

Methods for Inserting and Removing Nodes
Method Description
appendChild() Takes a single parameter: the node to insert, and inserts that node after the last child node.
insertBefore() Takes two parameters: the node to insert and the child node that it should precede. The new child node is inserted before the referenced child node.
replaceChild() Takes two parameters: the new node and the node to be replaced. It replaces the old node with the new node and returns the old node.
remove() Removes an element from the Document Object Model. It does not destroy the element, it just removes it from its parent.

Focusing on a Field

When you visit https://www.google.com, you will notice that the search input field gets immediate focus, so that you can start typing your search right away: Google focus()

This is accomplished using the focus() method of the input element, like this:

var searchInput = document.getElementById('search');
searchInput.focus();

It is often tied to the window's load event, like this:

Code Sample:

HTMLDOM/Demos/focus.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../normalize.css">
<link rel="stylesheet" href="../styles.css">
<script>
  window.addEventListener("load", function() {
    var searchInput = document.getElementById('search');
    searchInput.focus();  
  });
</script>
<title>Focus</title>
</head>
<body>
<main>
  <form>
    <input id="search" name="search">
    <button>Search</button>
  </form>
</main>
</body>
</html>

Code Explanation

Open HTMLDOM/Demos/focus.html in your browser to see how it works.

Shopping List Application

Using what we have learned in this lesson, we will build the one-page shopping list application shown below:

Shopping List App

Open HTMLDOM/Solutions/shopping-list.html in your browser to see how the finished application works:

  1. Notice that "Page Loaded" is logged and the New Item field gets focus.
  2. Add Milk by clicking on the + button next to Milk under Common Items.
  3. Add Lettuce by typing "Lettuce" in the New Item field and pressing the + button. Notice the New Item field gets focus, making it easy to enter another value.
  4. Add Bread by typing "Bread" in the New Item field and pressing the Enter key.
  5. Try adding Bread again both by clicking the + button and using the New Item field. Both attempts should fail silently.
  6. Try pressing the + button next to an empty New Item field. It should fail silently.
  7. Try entering just spaces in the New Item field and pressing the + button. It should fail silently.
  8. Remove Milk by clicking on the - button next to Milk under Active List.

The HTML (HTMLDOM/Exercises/shopping-list.html) and CSS (HTMLDOM/Exercises/shopping-list.css) have already been completed. You will build the JavaScript (HTMLDOM/Exercises/shopping-list.js) piece by piece.

Logging

Duration: 15 to 25 minutes.

In this exercise, you will complete the log(msg) function.

  1. Open HTMLDOM/Exercises/shopping-list.html in your editor. Examine the section of the code shown below. The ordered list will contain the log. You will need to access that ordered list and add list items to it with JavaScript.
    <section id="log">
    	<h2>Log</h2>
    	<ol></ol>
    </section>
  2. Open HTMLDOM/Exercises/shopping-list.js in your editor.
  3. In the log(msg) function, write code to:
    1. Access the ordered list shown above and save it in a variable.
    2. Create a new list item element and save it in a variable.
    3. Get the current date and save it in a variable.
    4. Set the innerHTML of the new list item to the current local time using the toLocaleTimeString() method, followed by a colon, followed by the msg passed to log(msg). For example, "5:53:12 PM: Page Loaded".
    5. Append the new list item to the ordered list.
  4. Test your code in the browser. When the page loads, it should log "Page Loaded". If it isn't working, use the console to help you debug.

Solution:

HTMLDOM/Solutions/shopping-list.1.js
/* Log Messages */
function log(msg) {
  // Access the ordered list shown above and save it in a variable
  var log = document.querySelector('section#log>ol');
  // Access the ordered list shown above and save it in a variable
  var newItem = document.createElement('li');
  // Get the current date and save it in a variable
  var now = new Date();
  // Set the innerHTML of the new list item 
  newItem.innerHTML = now.toLocaleTimeString() + 
    ': <em>' + msg + '</em>';
  // Append the new list item to the ordered list
  log.appendChild(newItem);
}
---- C O D E   O M I T T E D ----

Adding EventListeners

Duration: 25 to 40 minutes.

In this exercise, you will add EventListeners in the init() function so that you can log when a new item is added. You will not yet write the code to actually add the items. You will do that in the next exercise.

  1. Open HTMLDOM/Exercises/shopping-list.html in your editor. You will have to listen for the following events:
    1. Clicks on any button element with the class "btn-add".
    2. Clicks on the button element with the id "add-new-item".
    3. Keyup events on the input element with the id "new-item".
  2. Open HTMLDOM/Exercises/shopping-list.js in your editor if it isn't already open.
  3. Beneath the log('Page Loaded'); line, declare the following three variables:
    1. btnListAdd - A collection of button elements with the class "btn-add".
    2. btnAddNewItem - The button element with the id "add-new-item".
    3. newItem - The input element with the id "new-item".
  4. Add a line of code to place focus on the newItem input, so the user can just start typing in a new item.
  5. Each button in the btnListAdd collection is coded as follows:
    <button class="btn-add" name="Milk">+</button>
    When the user clicks one of these buttons, your code should pass the name of that button as the argument for product to the addToList(product) function. To do this, you will need to loop through these buttons, adding click EventListeners to each. You will need to know which of the buttons is clicked (e.currentTarget) so that you get the value of its name attribute.
  6. The add-new-item button is coded as folows:
    <button id="add-new-item">+</button>
    And the associated text field is:
    <input id="new-item">
    When the user clicks the "add-new-item" button, your code should:
    1. Pass the value of the text field as the argument for product to the addToList(product) function.
    2. Clear the text field.
    3. Place focus on the text field.
  7. Finally, you need to add an EventListener for the keypress event on the "new-item" text field. The callback function should check if the key pressed was the Enter key. If it was, it should:
    1. Pass the value of the text field as the argument for product to the addToList(product) function.
    2. Clear the text field.
    3. Place focus on the text field.
  8. Test your code in the browser. At this point, the shopping lists won't change, but logging should work when you add new items. If it isn't working, use the console to help you debug.

Solution:

HTMLDOM/Solutions/shopping-list.2.js
---- C O D E   O M I T T E D ----
function init() {
  log('Page Loaded');
  var btnListAdd = document.getElementsByClassName('btn-add');
  var btnAddNewItem = document.getElementById('add-new-item');
  var newItem = document.getElementById('new-item');
  newItem.focus();

  /* Add event listeners to all common list Add buttons */
  for (btn of btnListAdd) {
    btn.addEventListener('click', function(e) {
      var button = e.currentTarget;
      var product = button.name;
      addToList(product);
    });
  }

  /* Add event listener to New Item Add button */
  btnAddNewItem.addEventListener('click', function() {
    addToList(newItem.value);
    newItem.value='';
    newItem.focus();
  });

  /* 
    Add event listener capturing Enter press while
    focus is on New Item field 
  */
  newItem.addEventListener('keypress', function(e) {
    if (e.key === 'Enter') {
      addToList(newItem.value);
      newItem.value='';
      newItem.focus();
    }
  });
}

window.addEventListener("load", init);

Adding Items to the List

Duration: 15 to 25 minutes.

In this exercise, you will write the addToList() function.

  1. Open HTMLDOM/Exercises/shopping-list.js in your editor if it isn't already open.
  2. Currently, the addToList() function should look like this:
    function addToList(product) {
      log(product + ' added.')
    }
    You will write your code above the log(product + ' added.') line that does the following:
    1. Removes leading and trailing whitespace from the passed-in product, so that if the user enters " Milk ", we store it as "Milk".
    2. Access the "active-items-list" ordered list and save it in a variable.
    3. Create a new list item element and save it in a variable.
    4. Set the title of the new list item element to the product name.
    5. Set the innerHTML of the new list item element to the product name.
    6. Append the new list item to the "active-items-list" ordered list.
  3. Test your code in the browser. You should now be able to add items to list. If it isn't working, use the console to help you debug.

Solution:

HTMLDOM/Solutions/shopping-list.3.js
---- C O D E   O M I T T E D ----
function addToList(product) {
  product = product.trim();

  var activeList = document.getElementById('active-items-list');
  var newItem = document.createElement('li');
  newItem.title = product;
  newItem.innerHTML = product;
  activeList.appendChild(newItem);
  log(product + ' added.');
}
---- C O D E   O M I T T E D ----

Dynamically Adding Remove Buttons to the List Items

Duration: 15 to 25 minutes.

In this exercise, you will continue to work in the addToList() function. You will add remove buttons to the list items you created in the last exercise.

  1. Open HTMLDOM/Exercises/shopping-list.js in your editor if it isn't already open.
  2. Currently, the addToList() function should look something like this:
    function addToList(product) {
      product = product.trim();
    
      var activeList = document.getElementById('active-items-list');
      var newItem = document.createElement('li');
      newItem.title = product;
      newItem.innerHTML = product;
      activeList.appendChild(newItem);
      log(product + ' added.')
    }
    You will write your code below the log(product + ' added.') line that does the following:
    1. Create a button element with a minus sign that calls removeFromList() when clicked and append it to the new list item.
    2. Add a space between the product name and the new button.
    3. Check if the list item being added is in the common list items. If it is, disable the "add" button for that list item by setting its disabled property to true. Hint: Look at the name attributes of the buttons in the "common-list-items" list. Can you use querySelector() to find a button with the same name as the new list item you're adding?
    Note that these directions are intentionally less specific than in the previous exercises.
  3. Test your code in the browser. The list items in the 'active-items-list' ordered list should now have remove buttons. They won't actually remove the items, but they should log "Item removed" when clicked. Also, any item in the "common-list-items" list that is also in the "active-items-list" should have its "add" button disabled (red and unclickable). If your code isn't working, use the console to help you debug.

Solution:

HTMLDOM/Solutions/shopping-list.4.js
---- C O D E   O M I T T E D ----
function addToList(product) {
  product = product.trim();

  var activeList = document.getElementById('active-items-list');
  var newItem = document.createElement('li');
  newItem.title = product;
  newItem.innerHTML = product + ' '; // space before button
  activeList.appendChild(newItem);
  log(product + ' added.');

  var btnRemove = document.createElement('button');
  btnRemove.innerHTML = '-';
  btnRemove.addEventListener('click', removeFromList);
  newItem.appendChild(btnRemove);

  // Check if list item being added is in common list items
  // If it is, we need to disable its button there.
  var selector = '#common-items-list>li>button[name="' + product + '"]';
  var btnMatch = document.querySelector(selector);
  if (btnMatch) {
    btnMatch.disabled = true;
  }
}
---- C O D E   O M I T T E D ----

Code Explanation

Removing List Items

Duration: 15 to 25 minutes.

In this exercise, you will write the removeFromList() function to remove elements from the 'active-items-list' ordered list.

  1. Open HTMLDOM/Exercises/shopping-list.js in your editor if it isn't already open.
  2. Currently, the removeFromList() function should look like this:
    function removeFromList(e) {
      log('Item Removed');
    }
    1. Using the passed-in event (e), access the list item that contains the button that was clicked to call this function and assign that list item to a variable.
    2. Remove that item from the list.
    3. Change log('Item Removed') to log the name of the product removed.
    4. Check if the list item being removed is in the common list items. If it is, re-enable the "add" button for that list item by setting its disabled property to false.
  3. Test your code in the browser. When a remove button is clicked, the associated list item should now get removed and the log should tell you which item was removed. In addition, if there is an associated list item in the "common-list-items" list, its "add" button should be re-enabled. If your code isn't working, use the console to help you debug.

Solution:

HTMLDOM/Solutions/shopping-list.5.js
---- C O D E   O M I T T E D ----
/* Remove item from list */
function removeFromList(e) {
  var item = e.currentTarget.parentNode;
  item.remove();
  log(item.title + ' removed.');

  // Check if list item being removed is in common list items
  //  If it is, we need to enable its button there.
  var selector = '#common-items-list>li>button[name="' + 
    item.title + '"]';
  var btnMatch = document.querySelector(selector);
  if (btnMatch) {
    btnMatch.disabled = false;
  }
}
---- C O D E   O M I T T E D ----

Preventing Duplicates and Zero-length Product Names

Duration: 15 to 25 minutes.

In this exercise, you will finalize the shopping list by preventing duplicate values and empty strings from being added to the "active-items-list" list.

  1. There are a couple of issues still. Open HTMLDOM/Exercises/shopping-list.js in your browser.
  2. Add Milk via the Common Items list and then try adding it again using the New Item form field. Milk will be listed twice in your Active List. We'll fix that.
  3. Press the + button next to the empty New Item form field. It will add an empty item to your Active List. We'll fix that too.
  4. Open HTMLDOM/Exercises/shopping-list.js in your editor if it isn't already open.
  5. Below the line in which you trim the product name, add code that checks if that product is already listed in the "active-items-list" list. If it is or if the trimmed product name is an empty string, return false so that the rest of the code in the function doesn't run.
  6. Test your code in the browser.
    1. Add Milk via the Common Items list and then try adding it again using the New Item form field. It should fail silently.
    2. Press the + button next to the empty New Item form field. It should fail silently.
  7. If your code isn't working, use the console to help you debug.

Solution:

HTMLDOM/Solutions/shopping-list.js
---- C O D E   O M I T T E D ----
/* Add product to list */
function addToList(product) {
  product = product.trim();

  // Check if list item is already in active list
  //  or if product is empty string.
  var selector = '#active-items-list>li[title="' + product + '"]';
  var btnMatch = document.querySelector(selector);
  if (btnMatch || !product.length) {
    return false;
  }
  var activeList = document.getElementById('active-items-list');
  var newItem = document.createElement('li');
  newItem.title = product;
  newItem.innerHTML = product + ' ';
  activeList.appendChild(newItem);
  log(product + ' added.');

  var btnRemove = document.createElement('button');
  btnRemove.innerHTML = '-';
  btnRemove.addEventListener('click', removeFromList);
  newItem.appendChild(btnRemove);

  // Check if list item being added is in common list items
  // If it is, we need to disable its button there.
  var selector = '#common-items-list>li>button[name="' + product + '"]';
  var btnMatch = document.querySelector(selector);
  if (btnMatch) {
    btnMatch.disabled = true;
  }
}
---- C O D E   O M I T T E D ----

Manipulating Tables

HTML tables can be created and manipulated dynamically with JavaScript. Each table, tbody, thead, and tfoot element contains a rows array and methods for inserting and deleting rows: insertRow() and deleteRow(). Each tr element contains a cells array and methods for inserting and deleting cells: insertCell() and deleteCell(). The following example shows how these objects can be used to dynamically create HTML tables.

First let's take a look at how the page works in the browser. Open HTMLDOM/Demos/table.html in your browser to follow along.

  1. When it first loads, you see a screen like this:Manipulating Tables 1
  2. Fill in the form and press the + sign several times:Manipulating Tables 2
  3. Press the X next to one of the rows to delete that row:Manipulating Tables 3
  4. Press the - next to Delete all people to remove all rows and get back to where we started:Manipulating Tables 4

Now let's look at the code:

Code Sample:

HTMLDOM/Demos/table.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../normalize.css">
<link rel="stylesheet" href="../styles.css">
<script>
function addRow(tbodyId, cells){
  // Get the tbody and insert a new row
  var tbody = document.getElementById(tbodyId);
  var newRow = tbody.insertRow();

  // Insert cells based on passed-in cells array
  var newCell;
  for (var i=0; i < cells.length; i++) {
    newCell = newRow.insertCell();
    newCell.innerHTML = cells[i];
  }

  // Insert a final cell with a Delete button
  newCell = newRow.insertCell();
  var btnDelete = document.createElement('button');
  btnDelete.innerHTML = 'X';
  btnDelete.addEventListener('click', function(e) {
    btnDelete.parentNode.parentNode.remove();
  });
  newCell.appendChild(btnDelete);
}

function deleteAllRows(tbodyId) {
  var tbody = document.getElementById(tbodyId);
  while (tbody.rows.length > 0) {
    tbody.deleteRow(0);
  }
}

function prepareCells(fName, lName) {
  //Create a cells array to pass to the 
  var cells = [fName.value, lName.value];
  addRow('people', cells);
  fName.value = '';
  lName.value = '';
  fName.focus();
}

window.addEventListener('load', function() {
  var btnAdd = document.getElementById("btn-add");
  var btnDeleteAll = document.getElementById("btn-delete-all");
  var fName = document.getElementById('firstname');
  var lName = document.getElementById('lastname');

  btnAdd.addEventListener('click', function() {
    prepareCells(fName, lName);
  });

  lName.addEventListener('keypress',function(e) {
    if (e.key === 'Enter') {
      prepareCells(fName, lName);
    }
  });

  btnDeleteAll.addEventListener('click', function() {
    deleteAllRows('people');
  });

  fName.focus();
});
</script>
<title>Manipulating Tables</title>
</head>
<body id="table-demo">
<main>
<table>
  <thead>
    <tr>
      <th>First Name</th>
      <th>Last Name</th>
      <th>Admin</th>
    </tr>
  </thead>
  <tbody id="people"></tbody>
  <tbody>
    <tr>
      <td><input id="firstname" placeholder="First Name"></td>
      <td><input id="lastname" placeholder="Last Name"></td>
      <td><button type="button" id="btn-add">+</button></td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th colspan="2">Delete all people:</th>
      <td><button type="button" id="btn-delete-all">-</button></td>
    </tr>
  </tfoot>
</table>
</main>
</body>
</html>

Code Explanation

The body of the page contains a table with a thead that contains a single row of headers:

<thead> <tr> <th>First Name</th> <th>Last Name</th> <th>Admin</th> </tr> </thead>

Below the thead are two tbody elements.

  1. The first is empty and has an id of "people". We will add and remove people from this tbody.
  2. The second contains form elements for adding new rows:
    <tr> <td><input id="firstname" placeholder="First Name"></td> <td><input id="lastname" placeholder="Last Name"></td> <td><button type="button" id="btn-add">+</button></td> </tr>

Below the tbody elements is a tfoot element with a button for deleting all rows.

The JavaScript contains two generic functions: addRow() and deleteAllRows(). By "generic", we mean that these functions are not tied to this application. They could be used with any table.

The addRow() function takes two parameters: the id of the tbody element to which to add the row and an array of strings to populate the new row's cells:

function addRow(tbodyId, cells){ // Get the tbody and insert a new row var tbody = document.getElementById(tbodyId); var newRow = tbody.insertRow(); // Insert cells based on passed-in cells array var newCell; for (var i=0; i < cells.length; i++) { newCell = newRow.insertCell(); newCell.innerHTML = cells[i]; } // Insert a final cell with a Delete button newCell = newRow.insertCell(); var btnDelete = document.createElement('button'); btnDelete.innerHTML = 'X'; btnDelete.addEventListener('click', function(e) { btnDelete.parentNode.parentNode.remove(); }); newCell.appendChild(btnDelete); }

Note this line of code:

btnDelete.parentNode.parentNode.remove();

The first parentNode is the cell that contains btnDelete. The second parentNode is the row that contains that cell. That is the row that we are removing. We've added some styling below to make this easier to see:btnDelete.parentNode.parentNode

The deleteAllRows() function takes one parameter: the id of the tbody element containing the rows to be deleted. It then uses a while loop to delete the first row over and over until there are no rows left:

function deleteAllRows(tbodyId) { var tbody = document.getElementById(tbodyId); while (tbody.rows.length > 0) { tbody.deleteRow(0); } }

The other JavaScript wires up the eventListeners and prepares the cells for passing data to addRow().