Without notes, state yesterday’s main idea and one unresolved question.
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.
- 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.
- 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. - Add a category field and include it in the state object and the rendered label.
- Add a low-stock indicator inside
render: ifitem.quantity < 10, add alowclass to the row and append a<span>readingLOW. Do not store "is low" in the array — derive it. - Add a total-value line above the list, computed inside
renderwithreducefrom Day 22. Delete an item and confirm the total drops in the same redraw. - Prove the single source of truth. In the Console run
components.push({ id: "X1", name: "Test", quantity: 1 })— nothing changes on screen. Now runrender(). The row appears. State and view are genuinely separate, andrenderis 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
isLowfield can go stale the momentquantitychanges. Compute it inrenderfrom 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.
- MDN documents
preventDefaultalongsidestopPropagation. Find both and write one sentence on the difference — one cancels a behaviour, the other stops a journey. - Find MDN's
datasetpage and confirm howdata-unit-pricein 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
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Store components in a JavaScript array, then re-render the list after add and delete actions.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
An inventory whose UI is derived from one in-memory array.
Working with AI today
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
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.