# The DOM & events

> Find, read, change and create page elements with JavaScript, and respond to clicks, typing and form submissions.

Frontend Developer roadmap · JavaScript · 18 min read · https://levelcode.org/roadmaps/frontend/dom-and-events

HTML and CSS describe a page; JavaScript makes it react. Every interactive feature you have used – a menu that opens, a form that checks your input, a list that filters as you type – comes down to two skills: changing the page through the **DOM**, and running code when something happens through **events**. This lesson teaches both, finishing with event delegation, the technique that keeps interactive lists simple.

## What the DOM is

When the browser parses HTML, it builds the **Document Object Model**: a tree of JavaScript objects, one per element, text run and comment. This HTML:

```html
<ul id="menu">
  <li class="item">Home</li>
  <li class="item active">Shop</li>
</ul>
```

becomes a tree you can walk and change:

```text
ul#menu
├── li.item        → text "Home"
└── li.item.active → text "Shop"
```

Two consequences matter. First, the DOM is not your HTML file: the browser fixes errors while parsing (closing unclosed tags, adding a missing `<tbody>`), and your JavaScript changes the DOM, never the file. DevTools' Elements panel shows the live DOM. Second, everything starts from the global `document` object, which represents the whole page.

Your script needs the elements to exist before it can use them. Load it with `defer`, which runs it after the document has been parsed:

```html
<script src="/app.js" defer></script>
```

## Finding elements

`querySelector` takes any CSS selector and returns the **first** matching element, or `null`. `querySelectorAll` returns **all** matches as a `NodeList`:

```js
const menu = document.querySelector("#menu");
const active = document.querySelector("#menu .active");
const items = document.querySelectorAll(".item");

items.length; // 2
items.forEach((item) => console.log(item.textContent));

const asArray = [...items]; // spread into a real array for map/filter
```

You can also search inside an element, which is faster and avoids matching elsewhere on the page: `menu.querySelectorAll("li")`. To go **up** the tree, `element.closest(selector)` returns the nearest ancestor (or the element itself) that matches.

`getElementById("menu")` is an older, equivalent way to find one element by `id`.

> [!WARNING]
> If a selector matches nothing, `querySelector` returns `null`, and the next line throws `TypeError: Cannot read properties of null`. The usual causes are a typo in the selector, or a script that runs before the element exists – check for `defer`.

## Reading and changing elements

### Text and HTML

```js
const heading = document.querySelector("h1");

heading.textContent;               // read the text
heading.textContent = "New title"; // replace it – any HTML is shown literally
```

`innerHTML` reads and writes markup as a string. It is convenient for trusted, static snippets, but **never** put user-provided text into it: a "name" like `<img src=x onerror="stealCookies()">` would be parsed and executed. That attack is called cross-site scripting (XSS). Use `textContent` for anything that came from a user or a server.

### Attributes and properties

Elements expose most HTML attributes as **properties**:

```js
const link = document.querySelector("a");
link.href;                    // "https://example.com/about" – resolved to an absolute URL
link.getAttribute("href");    // "/about" – exactly as written in the HTML

const input = document.querySelector("#email");
input.value;                  // what the user has typed right now
input.disabled = true;
```

The difference matters for form fields: `input.value` is the current value, while `getAttribute("value")` is the initial value from the HTML.

### Classes, data and visibility

Change appearance by toggling classes and let CSS do the styling:

```js
const card = document.querySelector(".card");

card.classList.add("selected");
card.classList.remove("selected");
card.classList.toggle("expanded");   // add if missing, remove if present
card.classList.contains("expanded"); // true or false
```

`data-*` attributes store small pieces of data on elements, and `dataset` reads them with camelCase names:

```html
<button class="add" data-product-id="42">Add to basket</button>
```

```js
document.querySelector(".add").dataset.productId; // "42" – always a string
```

To hide and show elements, set the `hidden` property (`panel.hidden = true`). You can also set inline styles with `element.style.color = "red"`, but prefer classes: they keep styling in CSS and are easy to undo.

## Creating and removing elements

To add content, create elements, fill them in, and insert them:

```js
const list = document.querySelector("#results");

const products = [
  { id: 1, name: "Socks" },
  { id: 2, name: "Cap" },
];

const items = products.map((product) => {
  const li = document.createElement("li");
  li.textContent = product.name;
  li.dataset.id = product.id;
  return li;
});

list.replaceChildren(...items); // remove everything inside, then add these
```

| Method | Effect |
| --- | --- |
| `parent.append(a, b)` | Insert at the end of `parent` (elements or strings) |
| `parent.prepend(a)` | Insert at the start of `parent` |
| `el.before(a)` / `el.after(a)` | Insert as a sibling of `el` |
| `el.remove()` | Remove `el` from the page |
| `parent.replaceChildren(...nodes)` | Replace all of `parent`'s children; with no arguments, empty it |

Building all the elements first and inserting them with a single call, as above, keeps the update in one place and lets the browser process the change in one go.

## Events

An **event** is a signal that something happened: a click, a key press, a form submission, the page finishing loading. You subscribe with `addEventListener`:

```js
const button = document.querySelector("#like");
let likes = 0;

button.addEventListener("click", (event) => {
  likes += 1;
  button.textContent = `Liked ${likes} times`;
});
```

The handler receives an **event object** describing what happened. Its most useful members:

- `event.target` – the element where the event started (the innermost element clicked).
- `event.currentTarget` – the element the listener is attached to.
- `event.preventDefault()` – cancel the browser's default action, such as following a link or submitting a form.
- `event.key` – for keyboard events, the key pressed (`"Enter"`, `"Escape"`, `"a"`).

### Events you will use most

| Event | Fires when |
| --- | --- |
| `click` | An element is activated by mouse, touch, or Enter/Space on a button |
| `input` | A field's value changes, on every keystroke |
| `change` | A field's value is committed (on blur for text, immediately for checkboxes and selects) |
| `submit` | A form is submitted, by button or by pressing Enter in a field |
| `keydown` | A key is pressed |
| `focusin` / `focusout` | An element gains or loses focus (these bubble; `focus` and `blur` do not) |
| `DOMContentLoaded` | The HTML has been fully parsed |

### Handling forms

Listen for `submit` on the **form**, not `click` on the button, so pressing Enter works too. `FormData` collects every named field:

```js
const form = document.querySelector("#signup");

form.addEventListener("submit", (event) => {
  event.preventDefault(); // stay on the page instead of navigating
  const data = Object.fromEntries(new FormData(form));
  console.log(data); // { email: "ada@example.com", plan: "pro" }
  form.reset();
});
```

Built-in validation still runs first: the `submit` event only fires once required fields and patterns are satisfied.

### Removing listeners

`removeEventListener` needs the **same function** that was added, so you cannot remove an anonymous arrow function. Keep a reference, or pass `{ once: true }` for a listener that should run only once:

```js
function onKey(event) {
  if (event.key === "Escape") closeDialog();
}
document.addEventListener("keydown", onKey);
document.removeEventListener("keydown", onKey);

button.addEventListener("click", showWelcome, { once: true });
```

## Propagation and event delegation

When you click a `<span>` inside a `<button>` inside a `<li>`, which element "was clicked"? All of them. An event travels in three phases:

1. **Capture** – from `window` down through each ancestor to the target.
2. **Target** – at the element itself.
3. **Bubble** – back up from the target through every ancestor to `window`.

Listeners run in the bubble phase by default, so a listener on the `<ul>` hears clicks on every `<li>` inside it. `event.stopPropagation()` stops the journey, but use it rarely: it silently breaks any other code listening further up.

Here is the whole journey of one click on a remove button inside a list item. Solid arrows going down are the capture phase, the note on the button is the target phase, and the dashed arrows going back up are the bubble phase, where ordinary listeners run.

```sequence
title: One click, three phases
participants: window, document, ul, li, button
window -> document: click (capture)
> The browser works out the path from window to the target, then walks down it. Only listeners added with { capture: true } run on the way down. The html and body elements are on the path too; they are left out here.
document -> ul: click (capture)
ul -> li: click (capture)
li -> button: click (capture)
> The capture phase stops at the target's parent.
note button: click (target)
> Target phase: the button's own listeners run, capturing ones first. event.target is the button.
button --> li: click (bubble)
> Now the event travels back up. Listeners added without the capture option run in this phase.
li --> ul: click (bubble)
> A delegated listener on the ul runs here. event.target is still the button, while event.currentTarget is the ul.
ul --> document: click (bubble)
document --> window: click (bubble)
> Calling event.stopPropagation() in any listener along the way would end the journey at that element.
```

**Event delegation** uses bubbling to handle events for many children with **one** listener on their parent:

```html
<ul id="basket">
  <li data-id="1">Socks <button class="remove">Remove</button></li>
  <li data-id="2">Cap <button class="remove">Remove</button></li>
</ul>
```

```js
const basket = document.querySelector("#basket");

basket.addEventListener("click", (event) => {
  const button = event.target.closest(".remove");
  if (!button) return;              // the click was not on a remove button

  const item = button.closest("li");
  console.log("Removing", item.dataset.id);
  item.remove();
});
```

`closest` matters because `event.target` might be an element *inside* the button, such as an icon. Delegation has two big advantages: items added later work automatically, with no new listeners, and a list of 1,000 items needs one listener instead of 1,000.

> [!TIP]
> Use real `<button>` elements for anything clickable that is not a link. They are focusable and fire `click` on Enter and Space for free; a `<div>` with a click listener does neither.

## Practice

Build a to-do list in one HTML file with a `<script>` at the end of the body (or a deferred script):

1. A form with a text input and an "Add" button. Submitting adds a new item and clears the input. Ignore empty or whitespace-only entries.
2. Each item shows its text, a checkbox to mark it done (strike through the text using a `.done` class), and a "Delete" button.
3. A counter shows "N items left", updating after every change.
4. Use **one** click/change listener on the list for all items (event delegation), and never use `innerHTML` with the user's text.

**Solution**

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>To-do</title>
  <style>
    .done span { text-decoration: line-through; opacity: 0.6; }
  </style>
</head>
<body>
  <h1>To-do</h1>
  <form id="new-todo">
    <label for="todo-text">New task</label>
    <input id="todo-text" name="text" autocomplete="off">
    <button type="submit">Add</button>
  </form>
  <ul id="todos"></ul>
  <p id="count">0 items left</p>

  <script>
    const form = document.querySelector("#new-todo");
    const input = document.querySelector("#todo-text");
    const list = document.querySelector("#todos");
    const count = document.querySelector("#count");

    function updateCount() {
      const left = list.querySelectorAll("li:not(.done)").length;
      count.textContent = `${left} ${left === 1 ? "item" : "items"} left`;
    }

    function createTodo(text) {
      const li = document.createElement("li");

      const label = document.createElement("label");
      const checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      const span = document.createElement("span");
      span.textContent = text; // safe: never parsed as HTML
      label.append(checkbox, " ", span);

      const del = document.createElement("button");
      del.type = "button";
      del.className = "delete";
      del.textContent = "Delete";

      li.append(label, " ", del);
      return li;
    }

    form.addEventListener("submit", (event) => {
      event.preventDefault();
      const text = input.value.trim();
      if (!text) return;
      list.append(createTodo(text));
      form.reset();
      input.focus();
      updateCount();
    });

    // Delegation: checkbox changes bubble up to the list
    list.addEventListener("change", (event) => {
      if (event.target.type !== "checkbox") return;
      event.target.closest("li").classList.toggle("done", event.target.checked);
      updateCount();
    });

    // Delegation: delete clicks bubble up to the list
    list.addEventListener("click", (event) => {
      const del = event.target.closest(".delete");
      if (!del) return;
      del.closest("li").remove();
      updateCount();
    });
  </script>
</body>
</html>
```

`classList.toggle("done", checked)` with a second argument adds the class when `checked` is true and removes it when false, keeping the class in sync with the checkbox. The `change` event is used for the checkbox because it reliably fires when the box is toggled by mouse or keyboard. The counter is recomputed from the DOM rather than tracked separately, so it can never drift out of sync.

## Key takeaways

- The DOM is the browser's live tree of objects built from your HTML; JavaScript changes the DOM, not the file.
- Find elements with `querySelector`, `querySelectorAll` and `closest`; expect `null` when nothing matches.
- Use `textContent` for text, `classList` for styling, `dataset` for small data – and never put user input in `innerHTML`.
- Create elements with `createElement`, then insert them with `append`, `before`, `after` or `replaceChildren`.
- `addEventListener` gives you an event object; call `preventDefault()` to stop default actions like form submission.
- Events bubble up to ancestors, so one delegated listener with `closest` can handle every item in a list, including future ones.

## Quiz

1. A user types <b>Hi</b> into a comment box. Which line displays it safely, exactly as typed?

   A. comment.innerHTML = value
   B. comment.outerHTML = value
   C. comment.textContent = value
   D. document.write(value)

   **Answer: C.** textContent treats the value as plain text, so the tags are shown literally and nothing is executed. The other options parse the string as HTML, which allows cross-site scripting.

2. A click listener is attached to a <ul>. The user clicks an <svg> icon inside a <button> inside an <li>. What is event.currentTarget?

   A. The <ul>
   B. The <svg>
   C. The <button>
   D. The <li>

   **Answer: A.** currentTarget is always the element the listener is attached to. event.target would be the innermost element clicked, here the svg (or a shape inside it), which is why delegation code uses event.target.closest().

3. Your form's submit handler runs, but the page reloads immediately and your changes vanish. What is missing?

   A. event.stopPropagation()
   B. A click listener on the submit button
   C. The defer attribute on the form
   D. event.preventDefault()

   **Answer: D.** Submitting a form navigates by default. preventDefault() cancels that default action. stopPropagation only stops bubbling and does not prevent navigation.

4. Why does event delegation work for list items that are added after the listener was attached?

   A. addEventListener automatically attaches to new matching elements
   B. The listener is on the parent, and events from new children bubble up to it
   C. The browser re-runs scripts when the DOM changes
   D. querySelectorAll returns a live list that updates

   **Answer: B.** Delegation relies on bubbling: any child's event reaches the parent's listener, whenever the child was created. querySelectorAll actually returns a static NodeList.
