0 / 91
Week 4 · Day 26 of 91

Forms, state, and rendering

JavaScript Foundations II and the Browser

Objective

Keep application data separate from the HTML representation.

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

  • single source of truth
  • render functions
  • preventDefault

Why this matters

Yesterday's page works, and it has a flaw that will sink it the moment you add search or delete: the only place your components exist is the HTML. To count them you count <li> elements; to find one you read text back out of a row. The page is both the display and the database, and those two jobs pull in opposite directions.

Today you separate them. One JavaScript array holds the truth; a single render function draws the page from it. Every feature for the rest of the week — filtering, deleting, saving — becomes easy because of this one change.

The problem with storing data in the page

Suppose a row reads Resistor 10k — 42 left. To find every component below ten units you would have to split that string, pull 42 back out of it, and convert it to a number. You put a number in, and you are parsing text to get it back.

It gets worse when the same fact appears twice. Yesterday's step 3 set a count element from list.children.length. That is two places holding "how many components exist". Add a delete button that removes a row but forgets the count, and the page now confidently displays a number that is wrong. Nothing crashed. That is a logic error from Day 24, and it is the everyday kind.

The whiteboard and the spreadsheet

A stockroom keeps a spreadsheet and also a whiteboard by the door. Someone takes ten resistors and updates the whiteboard. Someone else reads the spreadsheet. Both are "the stock level" and they no longer agree, and there is no way to tell which is right. The fix is not to be more careful. The fix is to make the whiteboard something you print from the spreadsheet.

One source of truth

A single source of truth means one location holds each fact, and everything else is derived from it. You keep conflicting state from arising rather than trying to keep two copies in step.

Here, the truth is a plain array of the objects you have used all week:

let components = [
  { id: "R1", name: "Resistor 10k",    category: "resistor",  quantity: 42, unitPrice: 0.02 },
  { id: "C1", name: "Capacitor 100nF", category: "capacitor", quantity: 8,  unitPrice: 0.05 },
];

Everything on screen — the rows, the count, a low-stock badge, a total value — is computed from that array. None of it is stored anywhere else. Ask "how many components?" and the answer is components.length, never the number of <li> elements.

The rule that follows: change the array, then redraw. Never edit a row directly and hope the array agrees.

The register and the display

A panel meter does not remember the measurement. A register holds the value; the display is driven from it, and refreshing the display cannot change what was measured. If the display had its own memory, a missed update would leave it showing a stale reading with no way to tell. Your array is the register. The DOM is the display.

Render functions

A render function takes the current state and makes the page match it. One function, one job, called after every change.

const list = document.querySelector("#component-list");

function render() {
  list.textContent = "";                       // clear the old rows
  for (const item of components) {
    const row = document.createElement("li");
    row.textContent = `${item.name} — ${item.quantity} left`;
    list.append(row);
  }
  document.querySelector("#count").textContent = `${components.length} components`;
}

Setting textContent to an empty string removes every child, so you always rebuild from scratch rather than trying to work out which rows changed. That sounds wasteful and, at this size, is not — and it removes an entire category of bug, because there is no "which rows are stale?" question to get wrong. (list.replaceChildren() does the same clearing job and reads more explicitly.)

Now every action follows the same three-step shape:

function addComponent(item) {
  components.push(item);   // 1. change the state
  render();                // 2. redraw from the state
}

Step 3 is the discipline: nothing else in the program touches the DOM's rows. If a row is wrong, either the array is wrong or render is wrong, and you can tell which in seconds by typing components into the Console.

preventDefault and why forms reload

Yesterday you used <button type="button"> to dodge a problem. Here it is.

A <form> has a default action, built into browsers since before JavaScript existed: on submit, package the fields and send them to a URL, replacing the current page. That is why a plain button inside a form appears to wipe everything — the page genuinely reloaded, and your array, which lived only in memory, is gone.

You want the form and not the reload. Forms give you keyboard submit with Enter, native validation, and the correct announcement to screen readers, all things a lone button does not. So listen for submit and cancel the default:

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

form.addEventListener("submit", (event) => {
  event.preventDefault();   // stop the browser navigating
  // ...read the fields, update the array, render
});

event.preventDefault() cancels the browser's built-in response to that event while your handler still runs. It must be called inside the handler — the event object is the only thing that has it.

Overriding the default state of a circuit

A reset line has a defined default: pull it and the device restarts, no software involved. A supervisor can hold that line to prevent the reset when it knows a shutdown is in progress. The default is not a bug — it is what happens when nobody intervenes. preventDefault() is that hold: the browser's built-in behaviour is real and correct, and today you are the supervisor saying "not this time, I am handling it."

Deleting: one listener for many rows

Rows do not exist when your code first runs, and render replaces them constantly, so attaching a listener to each button is fragile. Instead attach one listener to the list that never gets replaced, and work out which row was clicked from the event:

list.addEventListener("click", (event) => {
  const button = event.target.closest("button[data-id]");
  if (!button) return;                 // clicked the row text, not a button
  deleteComponent(button.dataset.id);
});

event.target is the exact element clicked. closest(selector) walks up from it to the nearest matching ancestor, or itself, and returns null if there is none — hence the guard. A data- attribute in HTML (data-id="R1") is readable in JavaScript as element.dataset.id, and it is how you carry an identifier from the DOM back to your array.

Walkthrough

Restructure yesterday's main.js around state. In index.html, change the button to <button type="submit">Add component</button> and add <p id="count"></p> above the list.

let components = [];

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

function render() {
  list.textContent = "";
  for (const item of components) {
    const row = document.createElement("li");
    const label = document.createElement("span");
    label.textContent = `${item.name} — ${item.quantity} left`;

    const removeButton = document.createElement("button");
    removeButton.type = "button";
    removeButton.textContent = "Delete";
    removeButton.dataset.id = item.id;

    row.append(label, removeButton);
    list.append(row);
  }
  document.querySelector("#count").textContent = `${components.length} components`;
}

function addComponent(item) {
  components.push(item);
  render();
}

function deleteComponent(id) {
  components = components.filter((item) => item.id !== id);
  render();
}

form.addEventListener("submit", (event) => {
  event.preventDefault();
  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.");
  }

  addComponent({ id: crypto.randomUUID(), name, quantity });
  showMessage("");
  form.reset();
  nameInput.focus();
});

list.addEventListener("click", (event) => {
  const button = event.target.closest("button[data-id]");
  if (!button) return;
  deleteComponent(button.dataset.id);
});

render();

Keep showMessage from yesterday. Three details worth naming: deleteComponent uses Day 22's filter to build a new array rather than mutating in place, form.reset() clears every field in one call, and crypto.randomUUID() generates a unique id — it is available on https pages and on localhost, which is why you serve with npx serve rather than opening the file.

The final render() is not decoration: it draws the initial state, so an empty array produces an empty list and 0 components rather than a page that looks broken until you type something.

Checkpoint

Add three components, delete the middle one, and check the count. Then type components in the Console: the array should have exactly two entries and match the screen item for item. If they disagree, something changed the DOM without going through render.

Your turn

Build the deliverable: an inventory whose UI is derived from one in-memory array.

  1. Get the walkthrough running. Confirm Enter in the name field submits without reloading — the address bar must not flicker and the Console history must survive.
  2. Temporarily comment out event.preventDefault(). Submit once, watch the page reload and the list empty itself, then put it back. That is what you are preventing.
  3. Add a category field and include it in the state object and the rendered label.
  4. Add a low-stock indicator inside render: if item.quantity < 10, add a low class to the row and append a <span> reading LOW. Do not store "is low" in the array — derive it.
  5. Add a total-value line above the list, computed inside render with reduce from Day 22. Delete an item and confirm the total drops in the same redraw.
  6. Prove the single source of truth. In the Console run components.push({ id: "X1", name: "Test", quantity: 1 }) — nothing changes on screen. Now run render(). The row appears. State and view are genuinely separate, and render is the only bridge.

You are done when

Adding and deleting both go through the array, render is the only function that writes to the list, and the count, the total, and the rows always agree because all three come from one array.

Reviewer mode — once your app works

Today's mode is reviewer: bring working code and ask for defects. A review should primarily produce specific, actionable findings with evidence — never general praise, never a complete rewrite, and never changes you did not see.

"Check whether my UI and data can become inconsistent. Identify duplicate sources of truth."

For each finding, reproduce it yourself before you accept it. A review you cannot reproduce is a claim, not a defect.

Common pitfalls

  • Updating the DOM and the array separately. The instant two lines of code both mean "there are now three components", they can disagree. Change the array, then call render.
  • Forgetting preventDefault. The symptom is dramatic and confusing: everything vanishes and the page looks reset. It did reset — the browser navigated.
  • Attaching a delete listener to each button inside render. Rows are rebuilt on every redraw, so the listeners are rebuilt too, and any stale reference points at an element no longer in the tree. One listener on the list avoids all of it.
  • Storing derived values in the state. An isLow field can go stale the moment quantity changes. Compute it in render from the value it depends on.

Verify it yourself

Open today's reference, MDN's DOM scripting introduction, and find its pages on events and on the submit event.

  1. MDN documents preventDefault alongside stopPropagation. Find both and write one sentence on the difference — one cancels a behaviour, the other stops a journey.
  2. Find MDN's dataset page and confirm how data-unit-price in HTML appears as a property name in JavaScript. It is not the same spelling. Write down the rule.

Add both answers to notes/day-26.md. Tomorrow you make this array survive a refresh, and the naming rule you just checked is about to matter.

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

    Store components in a JavaScript array, then re-render the list after add and delete actions.

  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

An inventory whose UI is derived from one in-memory array.

Working with AI today

AI as skeptical reviewer

Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.

Check whether my UI and data can become inconsistent. Identify duplicate sources of truth.

References

End-of-day quiz

Q1 Why keep a single source of truth?
Q2 Which result best proves today’s work is complete?
Q3 What should an AI code review primarily produce?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.