{"id":"l-js-5-4","courseId":"javascript-fundamentals","lang":"en","kind":"lesson","summary":"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.","html":"<p>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.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The shape: one state, one way to change it</h2>\n<p>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 <strong>state</strong>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/main.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> state</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  expenses: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">loadExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  filter: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"all\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">};</span></span></code></pre></div>\n<p>Then make one function the only way to change it. It applies the change, saves, and re-renders:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/main.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> update</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">changes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">assign</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(state, changes);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  saveExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(state.expenses);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">Object.assign(state, changes)</code> copies the properties of <code dir=\"ltr\">changes</code> onto <code dir=\"ltr\">state</code>, so <code dir=\"ltr\">update({ filter: &quot;food&quot; })</code> changes only the filter. Every event handler now ends the same way: work out the new values, then call <code dir=\"ltr\">update</code>. You never need to remember to save or redraw, because it is impossible not to.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 220\" role=\"img\" aria-labelledby=\"l-js-5-4-t1\"><defs><marker id=\"arrow-l-js-5-4-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-js-5-4-t1\">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.</title>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"20\" width=\"140\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"90\" y=\"53\" text-anchor=\"middle\">user event</text>\n  <rect class=\"d-box\" x=\"200\" y=\"20\" width=\"160\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"280\" y=\"44\" text-anchor=\"middle\">handler</text>\n  <text class=\"d-label-muted\" x=\"280\" y=\"64\" text-anchor=\"middle\">uses pure functions</text>\n  <rect class=\"d-box-primary\" x=\"400\" y=\"20\" width=\"140\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"470\" y=\"53\" text-anchor=\"middle\">update()</text>\n  <rect class=\"d-box-success\" x=\"400\" y=\"140\" width=\"140\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"470\" y=\"173\" text-anchor=\"middle\">render()</text>\n  <rect class=\"d-box\" x=\"580\" y=\"20\" width=\"90\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"625\" y=\"53\" text-anchor=\"middle\">save</text>\n  <rect class=\"d-box\" x=\"200\" y=\"140\" width=\"160\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"280\" y=\"173\" text-anchor=\"middle\">the page</text>\n  <path class=\"d-arrow\" d=\"M160 48 L196 48\" marker-end=\"url(#arrow-l-js-5-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M360 48 L396 48\" marker-end=\"url(#arrow-l-js-5-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M540 48 L576 48\" marker-end=\"url(#arrow-l-js-5-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M470 76 L470 136\" marker-end=\"url(#arrow-l-js-5-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M400 168 L364 168\" marker-end=\"url(#arrow-l-js-5-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M200 168 Q90 168 90 80\" marker-end=\"url(#arrow-l-js-5-4-1)\"/>\n</svg><figcaption>The loop every event goes through</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">Render derives everything</h2>\n<p><code dir=\"ltr\">render</code> 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.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/ui.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> visible</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    state.filter </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"all\"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> state.expenses</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      :</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> state.expenses.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> e.category </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> state.filter);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  list.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">replaceChildren</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (visible.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> empty</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">createElement</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"li\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    empty.className </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"empty\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    empty.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"No expenses yet. Add your first one above.\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    list.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">append</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(empty);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> expense</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> visible) {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    list.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">append</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">renderItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expense));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  totalEl.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `Total: ${</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">formatMoney</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">totalCents</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">visible</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">))</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> renderItem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">expense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> li</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">createElement</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"li\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  li.dataset.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expense.id;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> text</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">createElement</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"span\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  text.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expense</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">label</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}: ${</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">formatMoney</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expense</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">amount</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} (${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expense</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">category</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">})`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> button</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">createElement</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  button.type </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"button\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  button.className </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"delete\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  button.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"Delete\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"aria-label\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`Delete ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">expense</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">label</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  li.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">append</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(text, button);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> li;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Because <code dir=\"ltr\">render</code> 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 <code dir=\"ltr\">&lt;li&gt;</code> elements and recalculate a number; it simply draws a different picture of the same state.</p>\n<p>Two details worth copying. The <strong>empty state</strong> turns a blank list into guidance. And the delete button’s <code dir=\"ltr\">aria-label</code> tells screen-reader users <em>which</em> expense it deletes, since ten buttons all called “Delete” are useless to someone who cannot see the row.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Handlers translate events into updates</h2>\n<p>With the state, <code dir=\"ltr\">update</code> and <code dir=\"ltr\">render</code> in place, each feature is a few lines:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/main.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">form.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"submit\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  event.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">preventDefault</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> result</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> readExpenseForm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(form);          </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// { values } or { errors }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (result.errors) {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">    showErrors</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(form, result.errors);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  update</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ expenses: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addExpense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(state.expenses, { id: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">nextId</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(), </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">result.values }) });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  form.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">reset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  form.elements.label.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">list.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"click\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> button</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> event.target.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">closest</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\".delete\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">button) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">closest</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"li\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).dataset.id;</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  update</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ expenses: state.expenses.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> e.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id) });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">filterSelect.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"change\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  update</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ filter: filterSelect.value });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">render</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span></code></pre></div>\n<p><code dir=\"ltr\">readExpenseForm</code> and <code dir=\"ltr\">showErrors</code> 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 <code dir=\"ltr\">aria-invalid</code>. Giving each a name turns the submit handler into a summary you can read in five seconds.</p>\n<p>Notice what the handlers do <em>not</em> contain: no DOM building, no saving, no totals. They read the event, call pure functions such as <code dir=\"ltr\">addExpense</code> from section 3, and hand the result to <code dir=\"ltr\">update</code>. The rules live in <code dir=\"ltr\">expenses.js</code>, where you can test them without a browser; the handlers are glue.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Ids that repeat after a reload</span></p>\n<p>A counter that starts at 1 every time the page loads will hand out <code dir=\"ltr\">exp-1</code> again while an old <code dir=\"ltr\">exp-1</code> is still in storage, and then deleting one deletes both. Start the counter after the highest saved id, or use <code dir=\"ltr\">crypto.randomUUID()</code>, which all current browsers provide on secure (https or localhost) pages and returns a unique id like <code dir=\"ltr\">&quot;3b241101-e2bb-4255-8caf-4136c566a962&quot;</code>.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">A definition of done</h2>\n<p>“Adding an expense works” is where testing starts, not where it ends. Before calling Pocket finished, run through this list by hand:</p>\n<ul>\n<li><strong>Empty</strong>: with no data, the page explains what to do.</li>\n<li><strong>Bad input</strong>: an empty label, an amount of <code dir=\"ltr\">abc</code>, <code dir=\"ltr\">0</code> or <code dir=\"ltr\">-5</code> each show a clear message and add nothing.</li>\n<li><strong>Reload</strong>: everything you added is still there, and the filter starts at “All”.</li>\n<li><strong>Keyboard only</strong>: you can tab to every field and button, submit with Enter, and delete with Enter or Space.</li>\n<li><strong>Odd data</strong>: a label like <code dir=\"ltr\">&lt;b&gt;hi&lt;/b&gt;</code> shows as text, and a very long label does not break the layout.</li>\n<li><strong>Console</strong>: no errors or warnings while you do all of the above.</li>\n</ul>\n<p>Every item comes from a lesson in this course, and each catches a real class of bugs that users would otherwise find for you.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Commit at each working step</span></p>\n<p>Build features one at a time and save a working version after each: add, then delete, then filter, then storage. If something breaks, you only have one step to undo. The <a href=\"/courses/git-github\">Git &amp; GitHub course</a> shows how to do this properly with commits.</p>\n</aside>\n<p>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.</p>\n","toc":[{"id":"sec-1","text":"The shape: one state, one way to change it","level":2},{"id":"sec-2","text":"Render derives everything","level":2},{"id":"sec-3","text":"Handlers translate events into updates","level":2},{"id":"sec-4","text":"A definition of done","level":2}],"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":[{"title":"Document.createElement() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement"},{"title":"HTMLElement: change event (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event"}],"quiz":[{"q":"<p>Pocket shows expenses filtered to “food” with a total at the bottom. Where should the filtered list come from?</p>","options":[{"text":"A second array, <code dir=\"ltr\">foodExpenses</code>, updated whenever an expense is added or deleted.","why":"Two arrays that must be kept in step will eventually disagree, for example after a delete that only updates one."},{"text":"Reading the <code dir=\"ltr\">&lt;li&gt;</code> elements currently on the page.","why":"The page is a picture of the data. Reading data back out of it couples your logic to markup and breaks easily."},{"text":"Computed inside <code dir=\"ltr\">render</code> from <code dir=\"ltr\">state.expenses</code> and <code dir=\"ltr\">state.filter</code> every time.","why":"Correct. Derived data that is recomputed from the single source of truth can never go stale."}],"answer":2},{"q":"<p>Every handler in Pocket ends with the same three steps. Which order is right?</p>","options":[{"text":"Update the state, save it, render from it.","why":"Correct. The state changes first; saving and rendering both read from the new state, so both reflect the same data."},{"text":"Render, then update the state, then save.","why":"Rendering before the state changes draws the old data, so the screen is always one step behind."},{"text":"Save, then update the state, then render.","why":"Saving before updating stores the old data, so a reload loses the latest change."}],"answer":0},{"q":"<p>Which of these is part of a reasonable definition of done for Pocket, beyond “adding an expense works”?</p>","options":[{"text":"Every function is written as a class method.","why":"Classes are a tool, not a goal. Plain functions are often the better choice for pure logic."},{"text":"The app has at least one animation.","why":"Polish is nice, but it says nothing about whether the app works for real use."},{"text":"The code uses no <code dir=\"ltr\">if</code> statements.","why":"Conditions are essential to real programs. Avoiding them is not a quality measure."},{"text":"The list shows a helpful message when there are no expenses, and the data survives a reload.","why":"Correct. Empty states and persistence are part of what users experience, so they belong in “done”."}],"answer":3}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<h1>Pocket</h1>\n<form id=\"expense-form\" novalidate>\n  <label for=\"label\">What</label>\n  <input id=\"label\" name=\"label\">\n  <label for=\"amount\">Amount ($)</label>\n  <input id=\"amount\" name=\"amount\" type=\"number\" step=\"0.01\">\n  <label for=\"category\">Category</label>\n  <select id=\"category\" name=\"category\">\n    <option value=\"food\">Food</option>\n    <option value=\"travel\">Travel</option>\n    <option value=\"fun\">Fun</option>\n  </select>\n  <p id=\"form-error\" role=\"alert\"></p>\n  <button type=\"submit\">Add expense</button>\n</form>\n\n<label for=\"filter\">Show</label>\n<select id=\"filter\">\n  <option value=\"all\">All</option>\n  <option value=\"food\">Food</option>\n  <option value=\"travel\">Travel</option>\n  <option value=\"fun\">Fun</option>\n</select>\n\n<ul id=\"expense-list\"></ul>\n<p id=\"total\"></p>\n","css":"form { display: grid; gap: 4px; max-width: 280px; margin-bottom: 16px; }\n#form-error { color: #b42318; min-height: 1.5em; margin: 0; }\nli { display: flex; gap: 8px; align-items: center; margin: 4px 0; }\n.empty { color: #6b6880; }\n","js":"// Storage. The playground starts every run with empty storage, so seed three\n// saved expenses (on a real page they would be there from earlier visits).\nconst STORAGE_KEY = \"pocket.expenses.v1\";\nif (localStorage.getItem(STORAGE_KEY) === null) {\n  localStorage.setItem(STORAGE_KEY, JSON.stringify([\n    { id: \"exp-1\", label: \"Coffee\", amount: 450, category: \"food\" },\n    { id: \"exp-2\", label: \"Train\", amount: 1220, category: \"travel\" },\n    { id: \"exp-3\", label: \"Lunch\", amount: 1350, category: \"food\" },\n  ]));\n}\n\nfunction loadExpenses() {\n  try {\n    const data = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? \"[]\");\n    return Array.isArray(data) ? data : [];\n  } catch {\n    return [];\n  }\n}\nfunction saveExpenses(expenses) {\n  localStorage.setItem(STORAGE_KEY, JSON.stringify(expenses));\n}\n\n// Pure helpers\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\nconst totalCents = (expenses) => expenses.reduce((sum, e) => sum + e.amount, 0);\nfunction validateExpense({ label, amount }) {\n  const errors = {};\n  if (label === \"\") errors.label = \"Enter what you spent money on.\";\n  if (!Number.isFinite(amount) || amount <= 0) errors.amount = \"Enter an amount greater than zero.\";\n  return errors;\n}\n\n// State\nconst state = { expenses: loadExpenses(), filter: \"all\" };\nlet idCounter = state.expenses.reduce((max, e) => Math.max(max, Number(e.id.split(\"-\")[1])), 0);\nconst nextId = () => `exp-${++idCounter}`;\n\nfunction update(changes) {\n  Object.assign(state, changes);\n  saveExpenses(state.expenses);\n  render();\n}\n\n// Rendering\nconst form = document.querySelector(\"#expense-form\");\nconst errorEl = document.querySelector(\"#form-error\");\nconst list = document.querySelector(\"#expense-list\");\nconst totalEl = document.querySelector(\"#total\");\nconst filterSelect = document.querySelector(\"#filter\");\n\n// 1. Return the expenses that match state.filter (\"all\" shows everything)\nfunction visibleExpenses() {\n  return state.expenses;\n}\n\nfunction render() {\n  const visible = visibleExpenses();\n  list.replaceChildren();\n  if (visible.length === 0) {\n    const empty = document.createElement(\"li\");\n    empty.className = \"empty\";\n    empty.textContent = \"No expenses yet. Add your first one above.\";\n    list.append(empty);\n  }\n  for (const expense of visible) {\n    const li = document.createElement(\"li\");\n    li.dataset.id = expense.id;\n    const text = document.createElement(\"span\");\n    text.textContent = `${expense.label}: ${formatMoney(expense.amount)} (${expense.category})`;\n    const button = document.createElement(\"button\");\n    button.type = \"button\";\n    button.className = \"delete\";\n    button.textContent = \"Delete\";\n    button.setAttribute(\"aria-label\", `Delete ${expense.label}`);\n    li.append(text, button);\n    list.append(li);\n  }\n  totalEl.textContent = `Total: ${formatMoney(totalCents(visible))}`;\n}\n\n// Events\nform.addEventListener(\"submit\", (event) => {\n  event.preventDefault();\n  const data = new FormData(form);\n  const values = {\n    label: data.get(\"label\").trim(),\n    amount: Math.round(Number(data.get(\"amount\")) * 100),\n    category: data.get(\"category\"),\n  };\n  const errors = validateExpense(values);\n  if (Object.keys(errors).length > 0) {\n    errorEl.textContent = Object.values(errors).join(\" \");\n    return;\n  }\n  errorEl.textContent = \"\";\n  update({ expenses: [...state.expenses, { id: nextId(), ...values }] });\n  form.reset();\n});\n\n// 2. When #filter changes, update the filter\n// 3. One delegated click listener on #expense-list that deletes an expense\n\nrender();\n"},"solution":{"html":"<h1>Pocket</h1>\n<form id=\"expense-form\" novalidate>\n  <label for=\"label\">What</label>\n  <input id=\"label\" name=\"label\">\n  <label for=\"amount\">Amount ($)</label>\n  <input id=\"amount\" name=\"amount\" type=\"number\" step=\"0.01\">\n  <label for=\"category\">Category</label>\n  <select id=\"category\" name=\"category\">\n    <option value=\"food\">Food</option>\n    <option value=\"travel\">Travel</option>\n    <option value=\"fun\">Fun</option>\n  </select>\n  <p id=\"form-error\" role=\"alert\"></p>\n  <button type=\"submit\">Add expense</button>\n</form>\n\n<label for=\"filter\">Show</label>\n<select id=\"filter\">\n  <option value=\"all\">All</option>\n  <option value=\"food\">Food</option>\n  <option value=\"travel\">Travel</option>\n  <option value=\"fun\">Fun</option>\n</select>\n\n<ul id=\"expense-list\"></ul>\n<p id=\"total\"></p>\n","css":"form { display: grid; gap: 4px; max-width: 280px; margin-bottom: 16px; }\n#form-error { color: #b42318; min-height: 1.5em; margin: 0; }\nli { display: flex; gap: 8px; align-items: center; margin: 4px 0; }\n.empty { color: #6b6880; }\n","js":"// Storage. The playground starts every run with empty storage, so seed three\n// saved expenses (on a real page they would be there from earlier visits).\nconst STORAGE_KEY = \"pocket.expenses.v1\";\nif (localStorage.getItem(STORAGE_KEY) === null) {\n  localStorage.setItem(STORAGE_KEY, JSON.stringify([\n    { id: \"exp-1\", label: \"Coffee\", amount: 450, category: \"food\" },\n    { id: \"exp-2\", label: \"Train\", amount: 1220, category: \"travel\" },\n    { id: \"exp-3\", label: \"Lunch\", amount: 1350, category: \"food\" },\n  ]));\n}\n\nfunction loadExpenses() {\n  try {\n    const data = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? \"[]\");\n    return Array.isArray(data) ? data : [];\n  } catch {\n    return [];\n  }\n}\nfunction saveExpenses(expenses) {\n  localStorage.setItem(STORAGE_KEY, JSON.stringify(expenses));\n}\n\n// Pure helpers\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\nconst totalCents = (expenses) => expenses.reduce((sum, e) => sum + e.amount, 0);\nfunction validateExpense({ label, amount }) {\n  const errors = {};\n  if (label === \"\") errors.label = \"Enter what you spent money on.\";\n  if (!Number.isFinite(amount) || amount <= 0) errors.amount = \"Enter an amount greater than zero.\";\n  return errors;\n}\n\n// State\nconst state = { expenses: loadExpenses(), filter: \"all\" };\nlet idCounter = state.expenses.reduce((max, e) => Math.max(max, Number(e.id.split(\"-\")[1])), 0);\nconst nextId = () => `exp-${++idCounter}`;\n\nfunction update(changes) {\n  Object.assign(state, changes);\n  saveExpenses(state.expenses);\n  render();\n}\n\n// Rendering\nconst form = document.querySelector(\"#expense-form\");\nconst errorEl = document.querySelector(\"#form-error\");\nconst list = document.querySelector(\"#expense-list\");\nconst totalEl = document.querySelector(\"#total\");\nconst filterSelect = document.querySelector(\"#filter\");\n\nfunction visibleExpenses() {\n  if (state.filter === \"all\") {\n    return state.expenses;\n  }\n  return state.expenses.filter((e) => e.category === state.filter);\n}\n\nfunction render() {\n  const visible = visibleExpenses();\n  list.replaceChildren();\n  if (visible.length === 0) {\n    const empty = document.createElement(\"li\");\n    empty.className = \"empty\";\n    empty.textContent = \"No expenses yet. Add your first one above.\";\n    list.append(empty);\n  }\n  for (const expense of visible) {\n    const li = document.createElement(\"li\");\n    li.dataset.id = expense.id;\n    const text = document.createElement(\"span\");\n    text.textContent = `${expense.label}: ${formatMoney(expense.amount)} (${expense.category})`;\n    const button = document.createElement(\"button\");\n    button.type = \"button\";\n    button.className = \"delete\";\n    button.textContent = \"Delete\";\n    button.setAttribute(\"aria-label\", `Delete ${expense.label}`);\n    li.append(text, button);\n    list.append(li);\n  }\n  totalEl.textContent = `Total: ${formatMoney(totalCents(visible))}`;\n}\n\n// Events\nform.addEventListener(\"submit\", (event) => {\n  event.preventDefault();\n  const data = new FormData(form);\n  const values = {\n    label: data.get(\"label\").trim(),\n    amount: Math.round(Number(data.get(\"amount\")) * 100),\n    category: data.get(\"category\"),\n  };\n  const errors = validateExpense(values);\n  if (Object.keys(errors).length > 0) {\n    errorEl.textContent = Object.values(errors).join(\" \");\n    return;\n  }\n  errorEl.textContent = \"\";\n  update({ expenses: [...state.expenses, { id: nextId(), ...values }] });\n  form.reset();\n});\n\nfilterSelect.addEventListener(\"change\", () => {\n  update({ filter: filterSelect.value });\n});\n\nlist.addEventListener(\"click\", (event) => {\n  const button = event.target.closest(\".delete\");\n  if (!button) return;\n  const id = button.closest(\"li\").dataset.id;\n  update({ expenses: state.expenses.filter((e) => e.id !== id) });\n});\n\nrender();\n"},"checks":[{"id":"initial","test":"return $$('#expense-list li[data-id]').length === 3 && text('#total') === 'Total: $30.20';\n"},{"id":"filter","test":"await input($('#filter'), 'food');\nconst ids = $$('#expense-list li[data-id]').map((li) => li.dataset.id).join();\nreturn ids === 'exp-1,exp-3' && text('#total') === 'Total: $18.00';\n"},{"id":"delete","test":"await click($('li[data-id=\"exp-3\"] .delete'));\nconst saved = JSON.parse(localStorage.getItem('pocket.expenses.v1'));\nreturn $$('#expense-list li[data-id]').length === 1 && !$('li[data-id=\"exp-3\"]') && saved.length === 2 && !saved.some((e) => e.id === 'exp-3');\n"},{"id":"empty-state","test":"await input($('#filter'), 'fun');\nreturn $$('#expense-list li[data-id]').length === 0 && exists('#expense-list .empty') && text('#total') === 'Total: $0.00';\n"},{"id":"add-and-all","test":"await input($('#filter'), 'all');\nawait input($('#label'), 'Museum');\nawait input($('#amount'), '12');\nawait input($('#category'), 'fun');\nawait click($('button[type=\"submit\"]'));\nconst items = $$('#expense-list li[data-id]');\nconst saved = JSON.parse(localStorage.getItem('pocket.expenses.v1'));\nreturn items.length === 3 && text(items[2]).startsWith('Museum: $12.00') && saved.length === 3 && new Set(saved.map((e) => e.id)).size === 3;\n"}],"title":"Finish Pocket","prompt":"<p>Pocket is almost done: the state, <code dir=\"ltr\">update</code>, <code dir=\"ltr\">render</code>, storage and the add form all work. Wire up the last three pieces:</p>\n<ol>\n<li><code dir=\"ltr\">visibleExpenses()</code> returns only the expenses whose <code dir=\"ltr\">category</code> matches <code dir=\"ltr\">state.filter</code>, or all of them when the filter is <code dir=\"ltr\">&quot;all&quot;</code>. <code dir=\"ltr\">render</code> already uses it for the list and the total.</li>\n<li>When the <code dir=\"ltr\">#filter</code> select changes, update the filter.</li>\n<li>Add <strong>one</strong> delegated click listener on <code dir=\"ltr\">#expense-list</code> that deletes the expense whose Delete button was clicked.</li>\n</ol>\n<p>Change data only through <code dir=\"ltr\">update(...)</code>, so every change is saved and re-rendered.</p>\n","hints":["<code dir=\"ltr\">visibleExpenses</code> is one <code dir=\"ltr\">if</code> and one <code dir=\"ltr\">filter</code>: <code dir=\"ltr\">state.expenses.filter((e) =&gt; e.category === state.filter)</code>.","<code dir=\"ltr\">filterSelect.addEventListener(&quot;change&quot;, () =&gt; update({ filter: filterSelect.value }));</code>","For delete: <code dir=\"ltr\">const button = event.target.closest(&quot;.delete&quot;); if (!button) return;</code> then read <code dir=\"ltr\">button.closest(&quot;li&quot;).dataset.id</code> and call <code dir=\"ltr\">update({ expenses: state.expenses.filter((e) =&gt; e.id !== id) })</code>."],"explanation":"<p>All three features are small because the structure does the heavy lifting. The filter is not stored anywhere except <code dir=\"ltr\">state.filter</code>; the visible list and its total are derived from it at render time, so they can never disagree. The filter and delete handlers only work out new values and call <code dir=\"ltr\">update</code>, which applies them, saves to storage and re-renders, so the saved data is always what the user sees. Delegation means the delete listener keeps working through every re-render and for newly added expenses. Because <code dir=\"ltr\">idCounter</code> starts after the highest saved id, new expenses never reuse an id that is already stored.</p>\n","checkLabels":{"initial":"Shows the three saved expenses and their total","filter":"Choosing Food shows only food expenses and their total","delete":"Deleting removes the expense from the page and from storage","empty-state":"A filter with no matches shows the empty message and $0.00","add-and-all":"Adding an expense with the filter on All shows it and saves it with a new id"}},"words":698,"readMinutes":3}