0 / 91
Week 4 · Day 25 of 91

The DOM and events

JavaScript Foundations II and the Browser

Objective

Connect JavaScript behavior to page elements.

Events are interrupts, promises represent future results, and the DOM is the browser’s live model of the page.

  • querying elements
  • event listeners
  • reading and updating content

Why this matters

Until now your JavaScript printed to the Console and your HTML sat there being static. Today you join them: JavaScript reads what a person typed, and changes what is on the screen, with no page reload. That single capability is what separates a document from an application.

By the end of the hour you will have a page where typing a component and pressing a button adds a row to a list in front of you — the first piece of the inventory app you finish this week.

What the DOM actually is

When the browser loads your HTML file, it does not keep the text. It parses the file once and builds a tree of objects in memory, one object per element, nested exactly as your tags were nested. That tree is the DOM — the Document Object Model. What you see on screen is drawn from the tree, not from the file.

Two consequences, and both matter today.

The tree is live. Change an object in it and the screen updates immediately — there is no "apply" step and no reload. Your JavaScript is editing the same structure the renderer is reading.

The tree is not the file. Nothing you do to the DOM touches index.html on disk. Refresh the page and the browser reparses the original file, and every change you made is gone. Persisting changes takes real storage, which is Day 27.

The DOM is the loaded configuration, not the config file

Firmware reads a configuration from flash at boot into a struct in RAM, and the running system behaves according to that struct. Write to the struct and behaviour changes at once; the flash image is untouched, and the next power cycle reloads the original. The HTML file is the flash image. The DOM is the struct in RAM.

See both, side by side

On your served page (Day 23's npx serve), press Ctrl+U — that is view-source, the raw file the server sent. Now open DevTools' Elements panel: that is the live tree. Delete a node in Elements and it vanishes from the page. Reload: it is back, because the file never changed.

Querying elements

To change an element you need a reference to its object. document is the built-in object representing the whole page, and it has two methods you need:

const list = document.querySelector("#component-list");
const rows = document.querySelectorAll(".component-row");
  • querySelector(selector) returns the first element matching a CSS selector, or null if nothing matches. The selectors are the ones you learned on Day 11 — #id, .class, tag.
  • querySelectorAll(selector) returns all matches as a list you can loop over with forEach. If nothing matches, the list is empty — not null.

That null is the most common first-day failure. querySelector finding nothing does not throw; the crash comes on the next line, as TypeError: Cannot read properties of null — an error you learned to read yesterday.

Give the elements you script an id. It is unambiguous, it survives restyling, and it says "this one is wired to code".

Reading and updating content

Once you hold an element, a few properties do most of the work.

Property What it does
element.textContent Reads or sets the element's text. Setting it replaces all children.
input.value The current contents of a form control. Always a string.
element.classList add, remove, toggle a CSS class
element.hidden true hides the element, false shows it
const heading = document.querySelector("#count");
heading.textContent = "3 components";

To add a new element rather than change an existing one, build it and attach it:

const row = document.createElement("li");
row.textContent = "R1 Resistor 10k — 42 left";
document.querySelector("#component-list").append(row);

createElement makes a node that exists but is not in the tree yet, so nothing is visible. append puts it in as the last child, and that is the moment it appears.

`textContent`, not `innerHTML`

innerHTML accepts a string and parses it as HTML. Feed it text a user typed and any tags in that text become real elements — including <script>-driven attacks. That vulnerability is called XSS (cross-site scripting) and Day 53 covers it properly. textContent treats the string as text, always. Use it for anything a person supplied.

Event listeners

Your code cannot sit in a loop asking "has the button been clicked yet?" — that would freeze the page, since the same single thread draws the screen. Instead you register a callback and the browser calls it when the thing happens.

const addButton = document.querySelector("#add-button");

addButton.addEventListener("click", (event) => {
  console.log("clicked", event.type);
});

addEventListener takes an event type (a string like "click", "input", "submit", "change", "keydown") and a callback — the same callback idea from Day 22, now called by the browser instead of by an array method. The listener runs on every matching event; you register it once.

The browser passes your callback one argument, the event object. Two properties earn their keep: event.type (which event fired) and event.target (which element it happened on).

Events are interrupts

A microcontroller does not poll a button in a tight loop; it registers an interrupt service routine and gets on with other work. When the pin changes, the hardware calls the ISR. That is precisely what addEventListener does: the callback is the ISR, the event type is the trigger condition, and the browser is the interrupt controller. The same rule applies too — keep the handler short, because while it runs, nothing else on the page can.

A doorbell, not a window

Polling is standing at the window checking whether anyone has arrived. A listener is fitting a doorbell and going back to work. You are not watching; you are called.

Walkthrough

Extend the Day 23 project. Replace the <body> of index.html:

<body>
  <h1>Component inventory</h1>

  <form id="add-form">
    <label for="name-input">Name</label>
    <input id="name-input" type="text" />

    <label for="qty-input">Quantity</label>
    <input id="qty-input" type="number" />

    <button id="add-button" type="button">Add component</button>
  </form>

  <p id="message" hidden></p>
  <ul id="component-list"></ul>

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

The labels are wired with for/id exactly as on Day 10, so the form stays usable by keyboard. type="button" is deliberate: a button inside a form defaults to submitting it, which reloads the page. Tomorrow you handle submit properly; today you sidestep it.

Now main.js:

const nameInput = document.querySelector("#name-input");
const qtyInput = document.querySelector("#qty-input");
const addButton = document.querySelector("#add-button");
const list = document.querySelector("#component-list");
const message = document.querySelector("#message");

function showMessage(text) {
  message.textContent = text;
  message.hidden = text === "";
}

addButton.addEventListener("click", () => {
  const name = nameInput.value.trim();
  const quantityText = qtyInput.value.trim();
  const quantity = Number(quantityText);

  if (name === "") return showMessage("Name is required.");
  if (quantityText === "") return showMessage("Quantity is required.");
  if (!Number.isInteger(quantity) || quantity < 0) {
    return showMessage("Quantity must be a whole number, zero or more.");
  }

  const row = document.createElement("li");
  row.textContent = `${name} — ${quantity} left`;
  list.append(row);

  showMessage("");
  nameInput.value = "";
  qtyInput.value = "";
  nameInput.focus();
});

Serve it and try it:

cd ~/fullstack-journey/inventory-app
npx serve

Type Resistor 10k and 42, press the button, and a row appears with no reload. Note the address bar does not flicker and the Console keeps its history — the page never went away. Those three guard clauses are Day 17's pattern: reject the bad cases first, then do the work.

Checkpoint

Press the button with an empty name and confirm the message appears and no row is added. Then open Elements and watch a new <li> appear inside #component-list as you add one. You are seeing the live tree change.

Your turn

Build the deliverable: a page that updates without reloading and validates basic input.

  1. Get the walkthrough page working. Add three components and confirm three <li> elements exist in the Elements panel.
  2. Add a category <input id="category-input"> with a label, and include it in the row text.
  3. Add a <p id="count"> above the list. After each successful add, set its textContent to `${list.children.length} components`. Confirm it climbs 1, 2, 3.
  4. Give each row a low class when quantity is below 10, using row.classList.add("low"), and add a CSS rule for .low in your stylesheet. Add one low item and confirm it looks different.
  5. Add a second listener: nameInput.addEventListener("input", () => showMessage("")), so the error clears as soon as the person starts fixing it. Type one character and watch it clear.
  6. Break it on purpose: change #add-button to #add-btn in querySelector. Read the TypeError naming null, then fix it. That error will find you again.

You are done when

Adding a valid component appends a row and clears the inputs; each invalid case shows its own message and adds nothing; and the page never reloads.

Pair mode — while you work through steps 2 to 4

Today's mode is pair: define one small task, review the plan, then — before accepting any AI-generated change — inspect the diff, run your checks, and make sure you understand the behaviour. Never commit code you cannot narrate line by line.

"Help me connect this form to the DOM one step at a time. Explain each event and element reference."

After each suggestion, say out loud which element it queries and which event it listens for. If you cannot, delete it and ask again.

Common pitfalls

  • Querying before the element exists. A plain <script> in <head> runs before the body is parsed, so every querySelector returns null. <script type="module"> is deferred until the document is parsed, which is why the walkthrough works.
  • Treating input.value as a number. It is always a string: "42" + 1 is "421". Convert with Number(...) before doing arithmetic.
  • Number("") is 0, not an error. An empty quantity box silently becomes zero and passes a naive check. That is why the walkthrough tests for an empty string before converting.
  • Adding a listener inside a loop that also re-runs. Registering the same handler twice means one click does the work twice. Register once, at the top of the file.

Verify it yourself

Open today's reference, MDN's DOM scripting introduction.

  1. MDN describes the DOM as a representation the browser builds from the document. Find the sentence and compare it with this lesson's claim that the file and the tree are different things. Do they agree?
  2. This lesson used append. MDN also documents appendChild. Find both and write one sentence on how they differ — one accepts something the other does not.

Write both answers into notes/day-25.md. Knowing which of two similar methods to reach for is exactly the kind of detail the reference exists to settle.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Build an add-component form that appends new rows to an on-page inventory list.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

A page that updates without reloading and validates basic input.

Working with AI today

AI as pair programmer

Define one small task, review the plan, inspect the diff, run checks, and explain every changed section.

Help me connect this form to the DOM one step at a time. Explain each event and element reference.

References

End-of-day quiz

Q1 What triggers an event listener?
Q2 Which result best proves today’s work is complete?
Q3 Before accepting an AI-generated code change, what should you do?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.