Choose a feature with a visible contract
Add a saved-title filter to the app. It should change only the displayed Saved books list, preserve saved records, match without case sensitivity, and show all saved records for a blank filter. Changing the filter must not start a catalogue search. This is a small feature, but it crosses a model/UI boundary.
First write a pure selector: given saved books and filter text, return the matching array. A selector derives data for display without changing state. Test matching, a nonmatch, blank input, and unchanged input. Then add an input with a label in the HTML and register one input listener in app.js. Keep the filter value in UI state and rerender from the controller’s current state when it changes.
Do not clear saved just because a filter hides every row. No visible match means zero displayed records, not lost data. Likewise, keep the saved-count summary clear: does it describe all saved books or just visible books? Name the count accordingly.
Only after the first feature works should you consider storage. Inject a storage adapter, validate reads, and make failed writes visible, using Day 6’s contract. Neither a framework nor a class is required for these features. Modules, pure functions, and a controller give the code enough boundaries to change it safely.
A useful review asks: does the new behavior live in the smallest responsible layer, can its rule be tested without the browser, and did the keyboard journey remain usable? Resist rewriting the whole app while adding one filter.
Predict and trace
Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.
function visibleSaved(saved, text) {
const query = text.trim().toLowerCase();
return saved.filter(book => book.title.toLowerCase().includes(query));
}
const saved = [
{ id: "js", title: "JavaScript Notes" },
{ id: "web", title: "The Web" }
];
console.log(visibleSaved(saved, " WEB ")[0].id);
console.log(visibleSaved(saved, "").length);
console.log(saved.length);Check your prediction
web
2
2Every string includes the empty string, so a blank query preserves all saved records without a special mutation.
Plan a feature across its boundaries
Write one concrete user action and its expected next state before adding a control. Identify which layer owns validation, state transition, dependency calls and rendering. A new happy path is incomplete if it loses saved data, accepts stale work or keeps effects alive after disposal. Use two unequal records so accidental changes outside the target are visible.
| Boundary or case | What to inspect |
|---|---|
| Public action | Name its input and exactly which fields may change |
| Model | Preserve unrelated fields and specify missing-id behavior |
| Controller and UI | Observe call count, loading ownership and recovery |
Implement one small vertical slice and add a distinguishing test before generalizing. Re-run old failure and lifecycle tests. A new green assertion is insufficient if the existing guarantees changed.
Build it yourself
Extending the App: practise
Write savedCountLabel(saved, query), using the selector pattern to return “Showing X of Y saved”. Print Showing 1 of 2 saved for query web.
// Keep the full count separate from the filtered count.Hint 1: the idea
Compute the displayed subset without replacing saved.
Hint 2: a first step
Use saved.length for the total and filtered.length for the visible count.
Show one solution
function savedCountLabel(saved, query) {
const text = query.trim().toLowerCase();
const visible = saved.filter(book => book.title.toLowerCase().includes(text));
return `Showing ${visible.length} of ${saved.length} saved`;
}
console.log(savedCountLabel([{ title: "Notes" }, { title: "The Web" }], "web"));Showing 1 of 2 savedTry no matches, blank input, and no saved records. Implement the browser input in your downloaded copy, then use Tab to reach it and verify clearing it restores the rows.
Test a changed requirement
Try no matches, blank input, and no saved records. Implement the browser input in your downloaded copy, then use Tab to reach it and verify clearing it restores the rows.
Explain before moving on
Why is filtering different from removing? Which tests can run without a DOM?
Completion is saved in this browser only. It does not measure mastery.