Without notes, state yesterday’s main idea and one unresolved question.
Week 4 inventory application
JavaScript Foundations II and the Browser
Objective
Combine DOM, events, state, modules, array methods, and storage.
Events are interrupts, promises represent future results, and the DOM is the browser’s live model of the page.
- feature slicing
- manual test cases
- UI empty and error states
Why this matters
Six days of parts become one thing today. Array methods produce the reports, modules keep the files
apart, the DOM draws the page, events drive it, one array holds the truth, and localStorage makes
it survive a refresh. Nothing new is being introduced — today is about assembly, and about the two
habits that separate a demo from an application: building in thin slices, and testing what you
built on purpose rather than by poking at it.
By the end of the hour you have a component inventory you would actually use, and a written checklist that proves it works.
Feature slicing
The tempting way to build is by layer: write all the HTML, then all the state, then all the rendering. It fails at the end, all at once, and you have no idea which layer is wrong.
Feature slicing is the opposite. You take one feature and build it all the way through — input, state change, render, persistence, verify — before starting the next. Each slice is small, each one is testable the moment it lands, and when something breaks you know it was the slice you just finished.
Today's six slices, in a deliberate order:
- Add — form to state to list to storage. (You have this.)
- Delete — remove from state, redraw, save. (You have this.)
- Low-stock badge — derived in
render, no new state. - Search — filter the rendered list by name.
- Category filter — a second, combinable filter.
- Export to JSON — read state, produce a file.
Notice that 3, 4, and 5 add no new state at all. They are views over the array you already have, which is the payoff for yesterday's discipline.
Bring up one signal path first
You do not populate every component on a new board and then apply power. You bring up the supply rail, confirm it, add the MCU, confirm it enumerates, then blink one LED end to end. Once that single path works, every later stage is added against something known good. A feature slice is that first working path: thin, complete, and verified before you widen it.
Deriving the visible list
Search and filter must never change components. The array is the inventory; a search box is a
question about it. Delete an item while a search is active and the item must disappear from
storage; type in the search box and nothing must change in storage at all.
So keep the controls' values as small, separate UI state, and compute the visible list on each render:
function getVisible() {
const query = document.querySelector("#search-input").value.trim().toLowerCase();
const category = document.querySelector("#category-filter").value;
return components
.filter((item) => item.name.toLowerCase().includes(query))
.filter((item) => category === "all" || item.category === category);
}
includes returns true when one string contains another. Lowercasing both sides makes the search
case-insensitive. An empty query lowercases to "", and every string contains "", so an empty
box shows everything — no special case needed.
Then render draws getVisible() instead of components, and both controls fire it:
document.querySelector("#search-input").addEventListener("input", render);
document.querySelector("#category-filter").addEventListener("change", render);
The input event fires on every keystroke, so the list narrows as you type. change fires when a
<select> value is committed.
One count is now ambiguous
With a filter active, "3 components" could mean three in the inventory or three matching. Decide
which and say so: `${visible.length} of ${components.length} components` removes the
ambiguity entirely. A label that is right only sometimes is a bug you shipped on purpose.
Empty and error states
A list with nothing in it is not one situation, it is three, and they need three different messages. Showing a blank area for all of them is the single most common way a working app looks broken.
| Situation | What the person should see |
|---|---|
| No components stored at all | "No components yet. Add your first one above." |
| Components exist, none match the filters | "No components match your search." plus a way to clear it |
| Stored data was unreadable | "Saved data could not be read, so the list was reset." |
The first is a beginning, the second is a dead end the person can escape, the third is a failure
they deserve to know about. Yesterday's loadComponents already returns [] when the stored value
is corrupt — today you surface that instead of hiding it, by returning a flag alongside the data or
setting a message when the catch block runs.
Three empty shelves
A shelf with a "new stock arriving Monday" card, a shelf whose label says "size 9 — try size 10 in aisle 4", and a shelf that is empty because the delivery van crashed. Identical from a distance. Completely different things to tell the person standing in front of it.
Manual test cases
A manual test case is three written lines: the steps, the expected result, and what actually happened. Written down, in advance, so that "it works" becomes a claim with evidence behind it.
They matter most where state, view, and storage can disagree. Deleting is the clearest example,
because one action has to land in four places, and all four need checking: the data state (is
the item gone from components?), the rendered list (is the row gone from the page?), the persisted
storage (is it gone after a refresh?), and the empty state (does the right message appear if that
was the last item?). Check only the screen and you can ship a delete that comes back on reload.
| # | Steps | Expected | Actual |
|---|---|---|---|
| 1 | Add "Resistor 10k", qty 42 | Row appears, count reads 1 of 1 | |
| 2 | Refresh the page | Row still present | |
| 3 | Delete it, then refresh | List empty, "No components yet" shown | |
| 4 | Add 3 items, search "cap" | Only matching rows, count reads 1 of 3 | |
| 5 | Search "zzz" | "No components match your search." | |
| 6 | Submit with empty name | Message shown, nothing added | |
| 7 | Add item with qty 3 | Row shows LOW badge | |
| 8 | Click Export | File downloads, opens as valid JSON | |
A test plan, not a poke
You do not certify a board by waving a probe at it. You write the procedure first — apply 5 V, measure this pin, expect 3.3 V ±5% — and record the reading in the actual column. The value of the document is that it was written before the measurement, so you cannot quietly change what you expected. A test case is that procedure.
Walkthrough
Build the last slice, export to JSON, which is the one mechanic you have not met.
Add a button to index.html:
<button id="export-button" type="button">Export JSON</button>
And in your module:
function exportJson() {
const text = JSON.stringify(components, null, 2);
const blob = new Blob([text], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "inventory.json";
link.click();
URL.revokeObjectURL(url);
}
document.querySelector("#export-button").addEventListener("click", exportJson);
Line by line: JSON.stringify(components, null, 2) produces indented text a human can read. A
Blob is a chunk of file-like data held in memory. URL.createObjectURL mints a temporary URL
pointing at that blob, which lasts until the page unloads — revokeObjectURL releases it as soon
as you are done. The <a> element is created, never added to the page, and clicked in code; its
download attribute is what makes the browser save the file instead of navigating to it.
Click it. Your browser downloads inventory.json. Open it in your editor: it is your array, and
because it is valid JSON, you could paste it straight back into localStorage.
Checkpoint
Export with an empty inventory. You get a file containing []. Decide whether that is acceptable
or whether the button should be disabled when components.length === 0 — and whichever you
choose, write it as test case 9.
Your turn
Build the deliverable: a usable browser inventory app and a short demo checklist.
- Write
test-plan.mdfirst, using the table above. Fill in expected results; leave Actual blank. Writing it before building is the whole exercise. - Add the low-stock badge in
render: quantity below 10 gets aLOWspan and alowclass. Derive it — noisLowfield in the array. - Add the search input and
getVisible, and changerenderto draw the visible list. Confirm typing narrows the list and storage is untouched (check the Application panel). - Add the category
<select id="category-filter">with analloption plus one option per category. Confirm search and filter combine rather than overriding each other. - Add the count line reading
X of Y components, and all three empty states from the table above. Trigger each one deliberately. - Add the export button from the walkthrough.
- Run every case in
test-plan.md, filling in the Actual column honestly. Any mismatch is a bug: fix it, then rerun that case and the two around it. - Commit with Day 13's habits — small, described commits — and update your README with what the
app does and how to run it (
npx serve).
You are done when
Every row of test-plan.md has an Actual column matching Expected, the app survives a refresh,
and you can demo add, search, filter, delete, and export in under two minutes without touching
the Console.
Reviewer mode — after your test plan is filled in
Today's mode is reviewer: hand over finished work and ask what is wrong with it. What a code review should primarily produce is specific, actionable findings with evidence — not general praise, not a complete rewrite every time, and not hidden changes.
"Review this app for state bugs, unclear names, inaccessible controls, and missing edge cases."
Turn each finding into a new numbered test case that reproduces it. If you cannot write the reproduction steps, you have not understood the finding well enough to accept it.
Common pitfalls
- Filtering the array instead of the view.
components = components.filter(...)in a search handler deletes everything that did not match, and one save later it is permanent. Search builds a new list to draw; it never reassigns the state. - One empty state for three situations. "No results" shown to a brand-new user reads as broken. Distinguish empty inventory, empty search, and failed load.
- Testing only the screen. After deleting, also check
componentsin the Console and refresh the page. State, view, and storage each need their own confirmation. - Widening before a slice works. Adding search while delete is half-finished means two unverified features and no idea which one broke. Finish and test each slice, then start the next.
Verify it yourself
Open today's reference, MDN's DOM scripting introduction, and its surrounding pages.
- Your delete buttons are icons or short labels in a list. Find MDN's guidance on accessible names
for buttons and check whether a screen reader would know which component each button deletes.
If not, fix it —
aria-labelwith the component name is the usual answer — and write down what you changed. - This lesson used
inputfor the search box andchangefor the select. Find both events in MDN and confirm, in one sentence each, when they fire. Wouldchangeon the search box behave differently?
Add both answers to notes/day-28.md. Week 5 moves to build tools and TypeScript, and it starts
from this app — an application you have tested is a much better thing to build on than one you have
merely finished.
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
Finish search, category filter, add, delete, low-stock badge, and export-to-JSON.
- 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 usable browser inventory app and a short demo checklist.
Working with AI today
Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.
Review this app for state bugs, unclear names, inaccessible controls, and missing edge cases.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.