# DOM in practice

Four hours of building: a shopping list, a mouse that counts, a slideshow, a coin to find, and a bookmark that runs your code on any site.

Multimedia & Creative Technology · 2.1 Webcoding · Lesson 7 · version 1.0.0 · 2026-10-26

Tags: javascript, dom, html

Slides: <https://slides.mctantwerp.be/en/webcoding/06-dom-practice>

Speaker notes: <https://slides.mctantwerp.be/en/webcoding/06-dom-practice.notes.md>

<!-- slide 2 -->

## Agenda

1. Warming up
2. How to tackle an exercise
3. Level two
4. Level three
5. Bookmarklets

<!-- slide 3 -->

## 1. Warming up

<!-- slide 4 -->

### Cheat sheet

| Find and change |   |
| --- | --- |
| One element | `querySelector('.card')` |
| All of them | `querySelectorAll('.card')` |
| Change the text | `el.textContent = 'Hi'` |
| Switch a class | `el.classList.toggle('open')` |
| Set a style | `el.style.left = x + 'px'` |

| Build and react |   |
| --- | --- |
| A new element | `document.createElement('li')` |
| Put it on the page | `list.append(item)` |
| Take it off | `el.remove()` |
| React | `el.addEventListener('click', fn)` |
| Stop the browser | `event.preventDefault()` |

<!-- slide 5 -->

### p.textContent = '<b>Hi</b>'; What shows up on the page?

- A. <b>Hi</b>, tags and all
- B. Hi, in bold
- C. Hi, without bold
- D. Nothing, textContent only reads

**Answer:** A. <b>Hi</b>, tags and all

textContent puts text on the page, and text is never turned into elements. For bold you need innerHTML, and never with something a visitor typed.

<!-- slide 6 -->

### document.querySelectorAll('li').textContent = 'Hello'; What happens?

- A. Nothing changes on the page, and there is no error
- B. Every li says Hello
- C. The first li says Hello
- D. TypeError: textContent is not a function

**Answer:** A. Nothing changes on the page, and there is no error

querySelectorAll gives a NodeList, a list of elements. A list has no textContent of its own, so the line sets a new property on the list and no li changes. Loop over it with forEach and set it per element.

<!-- slide 7 -->

### Uncaught TypeError: Cannot read properties of null (reading 'addEventListener'). What is the first thing to check?

- A. The selector, and whether the script has defer
- B. The name of the event
- C. Whether the function has brackets
- D. Whether the browser supports addEventListener

**Answer:** A. The selector, and whether the script has defer

null means querySelector found nothing. Either the selector has a typo or misses its dot or hash, or the script ran before the element existed.

<!-- slide 8 -->

### Level one

**[The DOM](https://slides.mctantwerp.be/en/webcoding/06-dom)**: Change it, Pick a font, Random text and Bigger. The five exercises of level one, with their previews and solutions.

> [!NOTE]
> Level two builds on level one. Unfinished exercises from last time come first.

<!-- slide 9 -->

## 2. How to tackle an exercise

<!-- slide 10 -->

### The starter

#### One folder per exercise

```text
webcoding/
├── 2-1-delete-list/
│   ├── index.html
│   └── script.js
├── 2-2-mouse-travel/
│   ├── index.html
│   ├── style.css
│   └── script.js
└── ...
```

#### index.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>2.1 Delete from a list</title>
  <script src="script.js" defer></script>
</head>
<body>
  <h1>2.1 Delete from a list</h1>
</body>
</html>
```

<!-- slide 11 -->

### Plan in comments

```js
// A heart under a photo.
// A click on the heart likes the photo,
// a second click takes the like back.

// 1. Select: the heart
// 2. Listen: a click on the heart
// 3. Change: toggle the class liked
```

What has to happen, in plain words, before any code.

> [!TIP]
> Select, listen, change. Three comments, and the rest is filling them in.

<!-- slide 12 -->

### Then fill it in

```js
// 1. Select: the heart
const heart = document.querySelector('#heart');

// 2. Listen: a click on the heart
heart.addEventListener('click', function () {
  // 3. Change: toggle the class liked
  heart.classList.toggle('liked');
});
```

1. One element, so `querySelector`.
2. The event, and the function that runs on it.
3. One line does both: on with the first click, off with the second.

<!-- slide 13 -->

### When it does not work

| You see | Usually |
| --- | --- |
| `Cannot read properties of null` | The selector found nothing: a typo, a missing dot or hash, or no defer |
| `addEventListener is not a function` | It is called on a list from querySelectorAll. Loop over it first |
| `forEach is not a function` | An HTMLCollection from getElementsBy... Use querySelectorAll |
| It runs once on load, never on a click | Brackets after the function name in addEventListener |
| The change flashes and is gone | A link or a form reloads the page. preventDefault |
| Nothing at all, no error | Is script.js loaded? A 404 in the console, or a console.log that never shows |

<!-- slide 14 -->

## 3. Level two

<!-- slide 15 -->

### A listener per item

```html
<style>.done { text-decoration: line-through; }</style>
<ul>
  <li>Buy fries</li>
  <li>Learn the DOM</li>
  <li>Sleep</li>
</ul>

<script>
  document.querySelectorAll('li').forEach(function (item) {
    item.addEventListener('click', function () {
      item.classList.toggle('done');
    });
  });
</script>
```

<!-- slide 16 -->

### ⭐⭐ 2.1: Delete from a list

1. A page with a list. A click on an item removes that item from the list.

> [!TIP]
> **Delete from a list**
>
> ```js
> const items = document.querySelectorAll('li');
>
> items.forEach(function (item) {
>   item.addEventListener('click', function () {
>     item.remove();
>   });
> });
> ```

Expected result: a shopping list of five items. Click an item and it disappears from the list.

<!-- slide 17 -->

### Measuring a distance

```html
<div id="pad" style="height: 200px; background: silver"></div>
<script>
  const pad = document.querySelector('#pad');
  let previous = null;
  pad.addEventListener('click', function (event) {
    if (previous !== null) {
      const dx = event.clientX - previous.x;
      const dy = event.clientY - previous.y;
      console.log(Math.sqrt(dx * dx + dy * dy));
      console.log(Math.hypot(dx, dy));
    }
    previous = { x: event.clientX, y: event.clientY };
  });
</script>
```

<!-- slide 18 -->

### ⭐⭐ 2.2: Mouse travel

1. A page that shows how many pixels your mouse has travelled so far.
2. Every `mousemove` adds the distance since the previous one.

> [!TIP]
> **Mouse travel**
>
> *index.html*
>
> ```html
> <p>Your mouse travelled <span id="distance">0</span> px</p>
> ```
>
> *script.js*
>
> ```js
> const output = document.querySelector('#distance');
> let total = 0;
> let lastX = null;
> let lastY = null;
>
> document.addEventListener('mousemove', function (event) {
>   if (lastX !== null) {
>     const dx = event.clientX - lastX;
>     const dy = event.clientY - lastY;
>     total += Math.hypot(dx, dy);
>     output.textContent = Math.round(total);
>   }
>   lastX = event.clientX;
>   lastY = event.clientY;
> });
> ```

Expected result: a counter in the middle of the page that keeps adding up how many pixels the mouse has moved.

<!-- slide 19 -->

### A random colour

```html
<button id="paint">Paint</button>
<script>
  function randomColour() {
    const r = Math.floor(Math.random() * 256);
    const g = Math.floor(Math.random() * 256);
    const b = Math.floor(Math.random() * 256);
    return `rgb(${r}, ${g}, ${b})`;
  }
  const paint = document.querySelector('#paint');
  paint.addEventListener('click', function () {
    document.body.style.backgroundColor = randomColour();
    console.log(document.body.style.backgroundColor);
  });
</script>
```

<!-- slide 20 -->

### ⭐⭐ 2.3: Colour trip

Fast flashing colours can trigger a seizure in people with photosensitive epilepsy. Keep it on your own screen, and look away when it gets too much.

1. Extend 2.2: every mouse move also gives the page a random background colour.

> [!TIP]
> **Colour trip**
>
> The randomColour function from the previous slide, and one extra line at the end of the mousemove listener of 2.2.
>
> ```js
> document.addEventListener('mousemove', function (event) {
>   // ... the distance of 2.2
>
>   document.body.style.backgroundColor = randomColour();
> });
> ```

Expected result: the distance counter of the previous exercise, and on every mouse move the page gets a random background colour.

<!-- slide 21 -->

### The array is the truth

```js
const todos = ['Fries', 'DOM'];

function render() {
  list.innerHTML = '';
  todos.forEach(function (todo) {
    const item = document.createElement('li');
    item.textContent = todo;
    list.append(item);
  });
}

button.addEventListener('click', function () {
  todos.push(input.value);
  render();
});
```

1. The data, in an array. This is what counts.
2. Empties the list and builds it again from the array. An empty string in `innerHTML` is safe.
3. Change the array first, then render. `input.value` is what is typed in the field.

<!-- slide 22 -->

### ⭐⭐ 2.4: Array to list

1. An array in JavaScript, shown as a ul on the page.
2. Add an item to the array, and it shows up in the list as well.
3. Extra: a click on an item removes it from the array and from the list.

> [!TIP]
> **Array to list**
>
> *index.html*
>
> ```html
> <input id="artist" placeholder="An artist">
> <button id="add">Add</button>
> <ul id="list"></ul>
> ```
>
> *script.js*
>
> ```js
> const artists = ['Stromae', 'Angèle'];
> const input = document.querySelector('#artist');
> const list = document.querySelector('#list');
>
> function render() {
>   list.innerHTML = '';
>   artists.forEach(function (artist) {
>     const item = document.createElement('li');
>     item.textContent = artist;
>     list.append(item);
>   });
> }
>
> document.querySelector('#add').addEventListener('click', function () {
>   artists.push(input.value);
>   input.value = '';
>   render();
> });
>
> render();
> ```

Expected result: a field with an Add button, the array in JavaScript and a list on the page. Every name you add goes into the array, and the list shows it too.

<!-- slide 23 -->

### Moving an element

```html
<style>#box { position: absolute; background: tomato; }</style>
<button id="move">Move</button>
<div id="box" style="width: 80px; height: 80px"></div>
<script>
  const box = document.querySelector('#box');
  const move = document.querySelector('#move');
  move.addEventListener('click', function () {
    const x = Math.random() * (innerWidth - box.offsetWidth);
    const y = Math.random() * (innerHeight - box.offsetHeight);
    box.style.left = x + 'px';
    box.style.top = y + 'px';
  });
</script>
```

<!-- slide 24 -->

### ⭐⭐ 2.5: Runaway

1. An image on the page. As soon as the mouse comes near, it runs away to a random place.
2. Extra: count how many times it escaped.

> [!TIP]
> **Runaway**
>
> *style.css*
>
> ```css
> #runner {
>   position: absolute;
>   width: 150px;
> }
> ```
>
> *script.js*
>
> ```js
> const runner = document.querySelector('#runner');
>
> runner.addEventListener('mouseenter', function () {
>   const x = Math.random() * (innerWidth - runner.offsetWidth);
>   const y = Math.random() * (innerHeight - runner.offsetHeight);
>   runner.style.left = x + 'px';
>   runner.style.top = y + 'px';
> });
> ```

Expected result: an image on the page. As soon as the mouse touches it, it jumps to a random other place.

<!-- slide 26 -->

## 4. Level three

<!-- slide 27 -->

### Remember it in a variable

```html
<button id="free">Free fries</button>
<p id="left">3 left</p>

<script>
  const button = document.querySelector('#free');
  const left = document.querySelector('#left');
  let portions = 3;

  button.addEventListener('click', function () {
    portions--;
    left.textContent = portions + ' left';
    button.disabled = portions === 0;
  });
</script>
```

<!-- slide 28 -->

### ⭐⭐⭐ 3.1: Counter

1. A number and two buttons: one adds one, the other takes one off.
2. The number never goes below 0, and never above 10.
3. Extra: the button that would cross the limit is disabled.

> [!TIP]
> **Counter**
>
> ```js
> const output = document.querySelector('#count');
> const minus = document.querySelector('#minus');
> const plus = document.querySelector('#plus');
> let count = 0;
>
> function update() {
>   output.textContent = count;
>   minus.disabled = count === 0;
>   plus.disabled = count === 10;
> }
>
> plus.addEventListener('click', function () {
>   if (count < 10) {
>     count++;
>   }
>   update();
> });
>
> minus.addEventListener('click', function () {
>   if (count > 0) {
>     count--;
>   }
>   update();
> });
>
> update();
> ```

Expected result: a number with a minus and a plus button. It never goes under 0 or over 10, and the button that would cross the limit is disabled.

<!-- slide 29 -->

### Next, and back to the start

```js
const photos = ['waffle', 'fries', 'beer'];
let index = 0;

for (let i = 0; i < 7; i++) {
  console.log(index, photos[index]);
  index = (index + 1) % photos.length;
}
```

> [!NOTE]
> `(index + 1) % photos.length` is the remainder after dividing by the length. After the last photo, it is 0 again.

<!-- slide 30 -->

### ⭐⭐⭐ 3.2: Slideshow

1. One photo on the page. Every click on it shows the next one.
2. After the last photo, the first one comes back.
3. Extra: a "2 / 4" under the photo, and a button to go back.

> [!TIP]
> **Slideshow**
>
> ```js
> const photos = ['images/waffle.jpg', 'images/fries.jpg', 'images/chocolate.jpg'];
> const photo = document.querySelector('#photo');
> let index = 0;
>
> photo.addEventListener('click', function () {
>   index = (index + 1) % photos.length;
>   photo.src = photos[index];
> });
> ```

Expected result: one photo on the page. Every click on it shows the next one, and after the last photo the first one comes back.

<!-- slide 31 -->

### ⭐⭐⭐ 3.3: No right click

1. Block the right mouse button on the whole page, so nobody gets to "View page source" that way.
2. Show a message on the page instead.

> [!TIP]
> **No right click**
>
> ```js
> const message = document.querySelector('#message');
>
> document.addEventListener('contextmenu', function (event) {
>   event.preventDefault();
>   message.textContent = 'Nope.';
> });
> ```

Expected result: a right click on the page opens no menu. A message says "Nope" instead.

<!-- slide 32 -->

### Open and closed

```html
<div class="item">
  <button class="question">Is it open book?</button>
  <p class="answer" hidden>Yes.</p>
</div>
<script>
  const answer = document.querySelector('.answer');
  const question = document.querySelector('.question');
  question.addEventListener('click', function () {
    answer.hidden = !answer.hidden;
  });
</script>
```

*hidden*

```html
<div class="item">
  <button class="question">Is it open book?</button>
  <p class="answer" hidden>Yes.</p>
</div>
<script>
  const answer = document.querySelector('.answer');
  const question = document.querySelector('.question');
  question.addEventListener('click', function () {
    answer.hidden = !answer.hidden;
  });
</script>
```

*details*

```html
<details>
  <summary>Is it open book?</summary>
  <p>Yes. And this one needs no JavaScript.</p>
</details>
```

<!-- slide 33 -->

### ⭐⭐⭐ 3.4: Accordion

1. A list of questions. A click on a question shows its answer, a second click hides it again.
2. One script for every question, however many there are.
3. Extra: opening one question closes the others.

> [!TIP]
> **Accordion**
>
> *index.html*
>
> ```html
> <div class="item">
>   <button class="question">When is the exam?</button>
>   <p class="answer">In the exam period of period 2.</p>
> </div>
> <div class="item">
>   <button class="question">Can I use Google?</button>
>   <p class="answer">Yes, it is open book.</p>
> </div>
> ```
>
> *style.css*
>
> ```css
> .answer { display: none; }
> .item.open .answer { display: block; }
> ```
>
> *script.js*
>
> ```js
> document.querySelectorAll('.question').forEach(function (question) {
>   question.addEventListener('click', function () {
>     question.closest('.item').classList.toggle('open');
>   });
> });
> ```

Expected result: three questions. A click on a question opens its answer under it, a second click closes it again.

<!-- slide 34 -->

### Which one was it?

```html
<button data-colour="gold">Gold</button>
<button data-colour="tomato">Tomato</button>
<button data-colour="teal">Teal</button>

<script>
  const buttons = document.querySelectorAll('button');

  buttons.forEach(function (button, index) {
    button.addEventListener('click', function () {
      console.log('Button number', index);
      document.body.style.background = button.dataset.colour;
    });
  });
</script>
```

<!-- slide 35 -->

### ⭐⭐⭐ 3.5: Find the coin

1. A grid of buttons. A coin hides behind one of them, picked at random when the page loads.
2. A wrong button shows a 👎, the right one a 🪙 and the number of tries it took.
3. Extra: after the coin is found, no button can be clicked anymore.

> [!TIP]
> **Find the coin**
>
> *index.html*
>
> ```html
> <div id="board">
>   <button>?</button>
>   <button>?</button>
>   <button>?</button>
>   <!-- six more -->
> </div>
> <p id="message">Where is the coin?</p>
> ```
>
> *script.js*
>
> ```js
> const buttons = document.querySelectorAll('#board button');
> const message = document.querySelector('#message');
> const coin = Math.floor(Math.random() * buttons.length);
> let tries = 0;
>
> buttons.forEach(function (button, index) {
>   button.addEventListener('click', function () {
>     tries++;
>     button.disabled = true;
>
>     if (index === coin) {
>       button.textContent = '🪙';
>       message.textContent = 'Found it in ' + tries + ' tries!';
>     } else {
>       button.textContent = '👎';
>     }
>   });
> });
> ```

Expected result: nine buttons with a question mark. The coin hides behind one random button. A wrong one shows a thumbs down, the right one the coin and the number of tries, and then the game stops.

<!-- slide 37 -->

## 5. Bookmarklets

<!-- slide 38 -->

### A bookmark that runs code

```js
javascript:alert(document.title);
```

An address that starts with `javascript:` instead of `https:`.

A click runs the code on the page that is open, on any site.

<!-- slide 39 -->

### Wrapped in a function

```js
javascript:(function () {
  const links = document.querySelectorAll('a');
  alert(links.length + ' links on this page');
})();
```

1. A function without a name, around all of the code.
2. Your variables live inside it and never touch the ones of the site.
3. The brackets at the end run it right away.

<!-- slide 40 -->

### DIY: Install one

1. Show the bookmarks bar of your browser: Windows <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd>, Mac <kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd>.
2. Drag one of the black buttons to the bar.
3. Open any site, and click the bookmark.

Bookmarklet "Count the links": `javascript:(function(){const links = document.querySelectorAll('a'); alert(links.length + ' links on this page');})();`

Bookmarklet "Edit anything": `javascript:(function(){document.designMode = 'on';})();`

Bookmarklet "Comic Sans": `javascript:(function(){document.querySelectorAll('*').forEach(function (el) { el.style.fontFamily = 'Comic Sans MS, cursive'; });})();`

Bookmarklet "Missing alt": `javascript:(function(){const images = document.querySelectorAll('img:not([alt])'); images.forEach(function (img) { img.style.outline = '8px solid red'; }); alert(images.length + ' images without alt');})();`

<!-- slide 41 -->

### Bouncy

```js
const style = document.createElement('style');
style.textContent = `
  @keyframes bounce {
    to { transform: translateY(-10px); }
  }`;
document.head.append(style);

document.querySelectorAll('body *').forEach(function (el) {
  el.style.animation = 'bounce 0.4s infinite alternate';
});
```

Bookmarklet "Bouncy": `javascript:(function(){const style = document.createElement('style'); style.textContent = '@keyframes bounce { to { transform: translateY(-10px); } }'; document.head.append(style); document.querySelectorAll('body *').forEach(function (el) { el.style.animation = 'bounce 0.4s infinite alternate'; });})();`

> [!WARNING]
> Moving everything on a page is the opposite of accessible. Fine for a joke, on your own screen.

<!-- slide 42 -->

### 🎉 4.1: Your own bookmarklet

Think of one, build it, use it. Fun, useful or completely pointless. A few ideas to start from:

1. Every image on the page becomes a photo of your cat.
2. Dark mode for any site, with a CSS filter on the html element.
3. Count the words on a page and say how long it takes to read.
4. Every heading gets its tag next to it, h1 to h6, to check the structure of a site.

<!-- slide 43 -->

### Next up

- **16/11** DOM in practice: Today
- **23/11** Forms: HO-A.01.39
- **30/11** JavaScript libraries
- **07/12** jQuery

> [!NOTE]
> Every exercise of the forms lesson starts with **querySelector**, **addEventListener** and **textContent**. More of today, with fields.

## Glossary

- **clientX and clientY**: The position of the mouse in the viewport, in pixels from the top left corner. Every mouse event has them. (slide 17)
- **Render function**: A function that empties a part of the page and builds it again from the data. Change the array, call the function, and the page follows. (slide 21)
- **Modulo**: The remainder of a division, written with %. (index + 1) % length counts up and jumps back to 0 after the last item. (slide 29)
- **Data attribute**: An attribute of your own on an element, starting with data-: data-colour="red". JavaScript reads it through element.dataset.colour. (slide 34)
- **disabled**: A property of buttons and form fields. Set it to true and the element can no longer be clicked or changed. (slide 27)
- **Bookmarklet**: A bookmark whose address starts with javascript: instead of https:. A click runs that code on the page you are looking at. (slide 38)

## Links and sources

### In the deck

- [kdgmt.be/discord](https://kdgmt.be/discord) (slide 44)

### Sources

- [Webcoding exercises (old Notion page)](https://mctantwerp.notion.site/Exercises-143a7d6a1cfb800ab1d2f7f77ca068c6): The same exercises, with gifs instead of live previews
- [MouseEvent: clientX property](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX)
- [Math.hypot()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot)
- [HTMLElement: dataset property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset)
- [Remainder (%)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder)
- [<details>: The Details disclosure element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details): An accordion without JavaScript
- [Document: designMode property](https://developer.mozilla.org/en-US/docs/Web/API/Document/designMode)
- [Understanding WCAG: Three flashes or below threshold](https://www.w3.org/WAI/WCAG22/Understanding/three-flashes-or-below-threshold.html): Why the colour exercise comes with a warning
