Without notes, state yesterday’s main idea and one unresolved question.
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, ornullif 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 withforEach. If nothing matches, the list is empty — notnull.
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.
- Get the walkthrough page working. Add three components and confirm three
<li>elements exist in the Elements panel. - Add a category
<input id="category-input">with a label, and include it in the row text. - Add a
<p id="count">above the list. After each successful add, set itstextContentto`${list.children.length} components`. Confirm it climbs 1, 2, 3. - Give each row a
lowclass when quantity is below 10, usingrow.classList.add("low"), and add a CSS rule for.lowin your stylesheet. Add one low item and confirm it looks different. - 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. - Break it on purpose: change
#add-buttonto#add-btninquerySelector. Read theTypeErrornamingnull, 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 everyquerySelectorreturnsnull.<script type="module">is deferred until the document is parsed, which is why the walkthrough works. - Treating
input.valueas a number. It is always a string:"42" + 1is"421". Convert withNumber(...)before doing arithmetic. Number("")is0, 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.
- 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?
- This lesson used
append. MDN also documentsappendChild. 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
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Build an add-component form that appends new rows to an on-page inventory list.
- 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
A page that updates without reloading and validates basic input.
Working with AI today
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
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.