Skip to content
wannacode

The DOM: the page from JavaScript

In short: The DOM is the tree of objects through which JavaScript reads and changes the page: querySelector finds an element, textContent changes its text, and createElement and append add new ones.

Problem

The price stays "—"

The price and the stock of the product come from JavaScript. A colleague put the script at the very top "so it starts sooner". The page shows "—" instead of a price.

HTML, CSS, JS

The console says two things. console.log(price) printed null, and the next line fails with TypeError: Cannot set properties of null. The element <strong id="price"> is right there in the HTML. Why can't JavaScript find it?

The idea

The document as an object

In the HTML module you saw that the browser builds a tree from the text. JavaScript works with the same tree: to it the page is an object named document. Every element in the tree is an object with fields and methods. That is why the tree is called the DOM: the Document Object Model.

You find an element with a selector, just like in CSS:

const price = document.querySelector('#price'); // by id
const title = document.querySelector('h1'); // by tag
const first = document.querySelector('.product'); // by class: the first one
const links = document.querySelectorAll('nav a'); // all links inside nav
  • querySelector walks the tree from the top down and returns the first element that matches. With no match it returns null.
  • querySelectorAll returns all of them as a NodeList: a list with length and forEach, but no map or filter. For those, turn it into an array: [...links]. Arrays and forEach come from the JavaScript course: arrays and lambdas.
  • The NodeList is a snapshot: an element added later is not in it.

null means "there is no such element", not "an empty element". The error comes after: null has no textContent, and the code fails on the next line.

The idea

When the script runs

The parser reads the HTML from the top down. When it reaches <script>, it stops, runs the script and only then goes on. In the problem the script is before <article>. When it runs, the tree has only <body>, and the price is not there yet. That is why querySelector returns null.

The fix is to make the script wait for the tree:

<head>
<meta charset="utf-8">
<script src="script.js" defer></script>
</head>
  • defer says: download the script right away, but run it when all of the HTML has been read. By then every element is in the tree.
  • The older way is a <script> at the very bottom, just before </body>. It works for the same reason.
  • defer only works on a script with src. On a script with code between the tags it does nothing.

Try it

Fix the price

Fix the page so that it shows the price and the stock. Do not touch script.js: the problem is when it runs, not what it does.

HTML, CSS, JS

The idea

The text: textContent and innerHTML

You change the element you found through its fields. For text there are two:

price.textContent = '€1.80'; // the text as it is
note.innerHTML = 'Price: <strong>€1.80</strong>'; // the text is read as HTML
  • textContent puts in text. If it contains <strong>, that shows up as characters, not as a tag.
  • innerHTML hands the text to the parser, which builds new elements from it. The old children of the element are gone.

innerHTML with data from a user (a name, a review, a search) is dangerous. If it holds HTML with code, that code runs in the browser of everyone who opens the page: this is an XSS attack, and the lesson "Untrusted input" is about it. So text goes in with textContent, and new elements are made with createElement (below).

After the change the browser redraws by itself. Not the whole page, only what changed.

See it

See how the page changes

On the left is the tree, on the right is the page the browser draws from it. The code finds the cart, changes its text and class and adds a new line to the list.

const cart = document.querySelector('#cart');
cart.textContent = 'Cart: 1';
cart.classList.add('full');
const li = document.createElement('li');
li.textContent = 'Milk';
document.querySelector('ul').append(li);
Step by step, in words
  1. The parser has already built the tree from the HTML. On the right is the page the browser draws from it. JavaScript sees the same tree through document.
  2. querySelector('#cart') walks the tree from the top and compares each element with the selector. It returns the first match: cart now points to that node. With no match it returns null.
  3. textContent replaces the text of the node. The browser notices the change and redraws only that part of the page. The rest stays as it was.
  4. classList.add puts a class on the element. A CSS rule for .full gives it a new colour, and again only the cart is redrawn.
  5. createElement makes a new <li>, and textContent gives it text. It is not in the tree yet, so the page does not change.
  6. append puts it as the last child of <ul>. Now it is in the tree, and the list on the page grows by one line.
  7. JavaScript never draws the page itself. It changes the tree, and the browser draws what changed. The page is not loaded again.

JavaScript draws nothing by itself. It changes the tree, and the browser draws the change. The page is not loaded again, so the text in the fields and the place you scrolled to stay.

Try it

Only 3 left

Show the stock in #stock: "Only 3 left". When there are fewer than 5, add the class low to the element (the CSS makes it red). The bold text from the class stock must stay.

HTML, CSS, JS

The idea

Attributes, properties and classes

In HTML an element has attributes: value="1", disabled, data-id="7". In JavaScript it has properties: fields of the object. Most attributes have a property with the same name, but the two are not the same thing.

  • input.value is what is in the field now. The value attribute is the starting value and does not change while the buyer types. You read the attribute with getAttribute('value').
  • button.disabled = true turns the button off. The property is true or false.
  • data-… attributes are for your own data. You read them through dataset: data-stock is dataset.stock, and data-product-id is dataset.productId. The value is always a string.
  • You work with classes through classList: add, remove, toggle (adds the class if it is missing and removes it if it is there) and contains.
HTML, CSS, JS

The console shows that the property is 3, while the attribute is still 1. dataset.stock is "12", a string: turn it into a number with Number(...) before you calculate with it.

Try it

Find the bug

The sale should make every price red, but the console says Cannot read properties of undefined (reading 'add').

Pick the line with the bug.

The idea

New elements

You make a new element in three steps: create it, give it content and put it in the tree.

const li = document.createElement('li'); // a new <li>, not on the page yet
li.textContent = 'Milk';
li.dataset.id = '2';
document.querySelector('ul').append(li); // now it is the last child of <ul>

Until you put it in with append, the element is not on the page. append also takes several elements at once: li.append(title, button). prepend puts it at the start, and remove() takes the element out of the tree.

More precisely, if you are curious

More ways to search. getElementById('price') is like querySelector('#price'). getElementsByClassName and getElementsByTagName return a "live" list: it changes when the tree changes. querySelectorAll returns a snapshot. After an append of a new .price, the live list has one more element, and the snapshot does not.

Searching inside an element. Every element has querySelector, not only document: card.querySelector('.price') searches only inside the card.

setAttribute('disabled', 'false') turns the button off. For disabled only the presence of the attribute matters, not its value. So you write button.disabled = false or removeAttribute('disabled').

innerHTML and <script>. A script put in with innerHTML does not run. An attribute like onerror on an image does. That is why innerHTML with someone else's data is dangerous even without <script>.

className versus classList. className is the whole class attribute as one string. Assigning to it replaces all the classes at once.

Exercise

The product card

script.js has a product object. Fill in the card from it: the name is the heading (#name), the price is "€8.90" (two digits after the point), and the stock is "Sold out" when stock is 0. Then the card gets the class sold-out and the button is turned off. Put a data-id with the id of the product on the card. For every label in badges, add an <li> to #badges.

HTML, CSS, JS

Test

Check yourself

1. What does document.querySelector('#total') return when the page has no element with id="total"?
2. The script is in <head> without defer and looks for a button in <body>. What does it find?
3. A product name comes from a user. How do you put it on the page?
4. The buyer typed 3 into <input value="1">. What do input.value and input.getAttribute('value') give?
5. Which one adds the class sale to every .price element?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·