EduFlow
JavaScript Fundamentals
0%

Section 5 · Writing Real Programs

Building Pocket

22 min3 min readCode playground

Lesson at a glance

Assemble Pocket into a complete browser app with one state object, pure logic, a single render function, delegated events, filtering and saved data, and check it against a definition of done.

  1. 1The shape: one state, one way to change it
  2. 2Render derives everything
  3. 3Handlers translate events into updates
  4. 4A definition of done

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

You have all the pieces: money helpers, validation, rendering, events, a form, storage, modules and classes. This lesson puts them together into the finished Pocket, and, more importantly, shows the shape that holds a small app together so it stays easy to change.

The shape: one state, one way to change it

Every interactive app has data that changes. In Pocket that is the list of expenses and the category filter the user picked. Put all of it in one object, the state:

src/main.js
const state = {
  expenses: loadExpenses(),
  filter: "all",
};

Then make one function the only way to change it. It applies the change, saves, and re-renders:

src/main.js
function update(changes) {
  Object.assign(state, changes);
  saveExpenses(state.expenses);
  render();
}

Object.assign(state, changes) copies the properties of changes onto state, so update({ filter: "food" }) changes only the filter. Every event handler now ends the same way: work out the new values, then call update. You never need to remember to save or redraw, because it is impossible not to.

A user event goes to a handler, which computes new values with pure functions and calls update. update changes the state, saves it to storage, and calls render, which draws the page the user sees, ready for the next event. user event handler uses pure functions update() render() save the page
The loop every event goes through

Render derives everything

render reads the state and draws the whole interface. Anything that can be computed from the state is computed here, not stored: the visible list depends on the filter, and the total depends on the visible list.

src/ui.js
function render() {
  const visible =
    state.filter === "all"
      ? state.expenses
      : state.expenses.filter((e) => e.category === state.filter);

  list.replaceChildren();
  if (visible.length === 0) {
    const empty = document.createElement("li");
    empty.className = "empty";
    empty.textContent = "No expenses yet. Add your first one above.";
    list.append(empty);
  }
  for (const expense of visible) {
    list.append(renderItem(expense));
  }
  totalEl.textContent = `Total: ${formatMoney(totalCents(visible))}`;
}

function renderItem(expense) {
  const li = document.createElement("li");
  li.dataset.id = expense.id;
  const text = document.createElement("span");
  text.textContent = `${expense.label}: ${formatMoney(expense.amount)} (${expense.category})`;
  const button = document.createElement("button");
  button.type = "button";
  button.className = "delete";
  button.textContent = "Delete";
  button.setAttribute("aria-label", `Delete ${expense.label}`);
  li.append(text, button);
  return li;
}

Because render always starts from the state, the filter, the list and the total can never contradict each other. Switching the filter to “food” does not hide some <li> elements and recalculate a number; it simply draws a different picture of the same state.

Two details worth copying. The empty state turns a blank list into guidance. And the delete button’s aria-label tells screen-reader users which expense it deletes, since ten buttons all called “Delete” are useless to someone who cannot see the row.

Handlers translate events into updates

With the state, update and render in place, each feature is a few lines:

src/main.js
form.addEventListener("submit", (event) => {
  event.preventDefault();
  const result = readExpenseForm(form);          // { values } or { errors }
  if (result.errors) {
    showErrors(form, result.errors);
    return;
  }
  update({ expenses: addExpense(state.expenses, { id: nextId(), ...result.values }) });
  form.reset();
  form.elements.label.focus();
});

list.addEventListener("click", (event) => {
  const button = event.target.closest(".delete");
  if (!button) return;
  const id = button.closest("li").dataset.id;
  update({ expenses: state.expenses.filter((e) => e.id !== id) });
});

filterSelect.addEventListener("change", () => {
  update({ filter: filterSelect.value });
});

render();

readExpenseForm and showErrors are the form code from section 4, moved into small named functions: one reads, trims, converts and validates, returning either clean values or an errors object; the other writes the messages and sets aria-invalid. Giving each a name turns the submit handler into a summary you can read in five seconds.

Notice what the handlers do not contain: no DOM building, no saving, no totals. They read the event, call pure functions such as addExpense from section 3, and hand the result to update. The rules live in expenses.js, where you can test them without a browser; the handlers are glue.

A definition of done

“Adding an expense works” is where testing starts, not where it ends. Before calling Pocket finished, run through this list by hand:

  • Empty: with no data, the page explains what to do.
  • Bad input: an empty label, an amount of abc, 0 or -5 each show a clear message and add nothing.
  • Reload: everything you added is still there, and the filter starts at “All”.
  • Keyboard only: you can tab to every field and button, submit with Enter, and delete with Enter or Space.
  • Odd data: a label like <b>hi</b> shows as text, and a very long label does not break the layout.
  • Console: no errors or warnings while you do all of the above.

Every item comes from a lesson in this course, and each catches a real class of bugs that users would otherwise find for you.

The exercise below hands you a nearly finished Pocket with the filter and delete still to wire up. After that, one short lesson on where to go from here.

Key takeaways

  • Keep all changing data in one place and change it through one function that saves and re-renders, so the page, the data and storage never disagree.
  • Derive everything you can, such as the visible list and the totals, from the state at render time instead of storing it separately.
  • Pure functions hold the rules; handlers stay short and only translate events into state changes.
  • An app is done when it handles the empty state, bad input, a reload and keyboard-only use, not just the happy path.

Further reading

Check your understanding

3 questions

Question 1 of 3

Pocket shows expenses filtered to “food” with a total at the bottom. Where should the filtered list come from?

Question 2 of 3

Every handler in Pocket ends with the same three steps. Which order is right?

Question 3 of 3

Which of these is part of a reasonable definition of done for Pocket, beyond “adding an expense works”?

Practice

Code playground

Finish Pocket

Pocket is almost done: the state, update, render, storage and the add form all work. Wire up the last three pieces:

  1. visibleExpenses() returns only the expenses whose category matches state.filter, or all of them when the filter is "all". render already uses it for the list and the total.
  2. When the #filter select changes, update the filter.
  3. Add one delegated click listener on #expense-list that deletes the expense whose Delete button was clicked.

Change data only through update(...), so every change is saved and re-rendered.

<h1>Pocket</h1>
<form id="expense-form" novalidate>
  <label for="label">What</label>
  <input id="label" name="label">
  <label for="amount">Amount ($)</label>
  <input id="amount" name="amount" type="number" step="0.01">
  <label for="category">Category</label>
  <select id="category" name="category">
    <option value="food">Food</option>
    <option value="travel">Travel</option>
    <option value="fun">Fun</option>
  </select>
  <p id="form-error" role="alert"></p>
  <button type="submit">Add expense</button>
</form>

<label for="filter">Show</label>
<select id="filter">
  <option value="all">All</option>
  <option value="food">Food</option>
  <option value="travel">Travel</option>
  <option value="fun">Fun</option>
</select>

<ul id="expense-list"></ul>
<p id="total"></p>
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Preview

Console

(no output)

Checks

  • Shows the three saved expenses and their total
  • Choosing Food shows only food expenses and their total
  • Deleting removes the expense from the page and from storage
  • A filter with no matches shows the empty message and $0.00
  • Adding an expense with the filter on All shows it and saves it with a new id