{"id":"l-js-4-2","courseId":"javascript-fundamentals","lang":"en","kind":"lesson","summary":"Respond to clicks and key presses with addEventListener, read the event object, understand bubbling, and handle a whole list with one delegated listener that keeps working as items come and go.","html":"<p>Pocket can draw its list. Now people want to do things to it: delete an expense, mark one as reviewed, filter by category. Each of those starts with the user doing something, such as clicking or typing, and your code reacting. The browser reports everything users do as <strong>events</strong>.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Listening for an event</h2>\n<p>You ask an element to tell you about an event with <code dir=\"ltr\">addEventListener</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">app.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\"> clearButton</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\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"#clear\"</span><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\">clearButton.</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:#1F2328;--shiki-dark:#E6EDF3\">  console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Clear was clicked\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, event.type);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>Read it as: “when a click happens on this button, call this function”. Your function does not run now. The browser stores it and calls it later, once per click, passing an <strong>event object</strong> with details about what happened. This is the callback idea from the functions lesson, and the browser is the code calling you back.</p>\n<p>The event types you will use most: <code dir=\"ltr\">click</code>, <code dir=\"ltr\">input</code> (a text field’s value changed), <code dir=\"ltr\">change</code> (a value was committed, for example a select), <code dir=\"ltr\">submit</code> (a form was sent), and <code dir=\"ltr\">keydown</code> (a key was pressed; <code dir=\"ltr\">event.key</code> says which, such as <code dir=\"ltr\">&quot;Enter&quot;</code> or <code dir=\"ltr\">&quot;Escape&quot;</code>).</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>Calling the handler instead of passing it</span></p>\n<p><code dir=\"ltr\">button.addEventListener(&quot;click&quot;, clearAll());</code> runs <code dir=\"ltr\">clearAll</code> once, right now, while the page loads, and registers whatever it returned, which is usually <code dir=\"ltr\">undefined</code>. Then clicks do nothing. Pass the function, <code dir=\"ltr\">clearAll</code>, or wrap a call that needs arguments in an arrow function: <code dir=\"ltr\">() =&gt; removeExpense(id)</code>.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Data first, then render</h2>\n<p>The most important habit with events: <strong>do not treat the page as the place your data lives</strong>. Keep the data in a variable, change the variable in your handler, and call the render function from the last lesson to redraw:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">app.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\">let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses </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\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"exp-1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, label: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Coffee\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amount: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">450</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"exp-2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, label: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Train\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amount: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1220</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\">clearButton.</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:#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:#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\">  renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>The handler never touches <code dir=\"ltr\">&lt;li&gt;</code> elements directly. It changes the data, and the render function makes the page match. However many buttons you add, there is still one place that draws the list, so the total and the list can never disagree.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Bubbling: events travel up the tree</h2>\n<p>When you click a button inside a list item inside a list, you are clicking all of them: the button, the <code dir=\"ltr\">&lt;li&gt;</code>, the <code dir=\"ltr\">&lt;ul&gt;</code>, the <code dir=\"ltr\">&lt;body&gt;</code>. The browser models this by delivering the event first to the element you clicked, the <strong>target</strong>, and then to each ancestor in turn, all the way up to <code dir=\"ltr\">document</code>. This is called <strong>bubbling</strong>.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 250\" role=\"img\" aria-labelledby=\"l-js-4-2-t1\"><defs><marker id=\"arrow-l-js-4-2-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-4-2-t1\">A click on a delete button starts at the button, the event target, then bubbles up to the li, then to the ul where a delegated listener handles it, then on to body and document.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"16\" width=\"600\" height=\"220\" rx=\"14\"/>\n  <text class=\"d-code\" x=\"40\" y=\"42\">body</text>\n  <rect class=\"d-box-accent\" x=\"50\" y=\"56\" width=\"540\" height=\"164\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"70\" y=\"82\">ul#expense-list   (listener here)</text>\n  <rect class=\"d-box\" x=\"80\" y=\"98\" width=\"480\" height=\"104\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"100\" y=\"124\">li data-id=\"l-js-4-2-exp-2\"</text>\n  <text class=\"d-label\" x=\"110\" y=\"168\">Train  $12.20</text>\n  <rect class=\"d-box-primary\" x=\"380\" y=\"140\" width=\"150\" height=\"46\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"455\" y=\"168\" text-anchor=\"middle\">button.delete</text>\n  <path class=\"d-arrow\" d=\"M455 140 L455 116 L330 116\" marker-end=\"url(#arrow-l-js-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M300 100 L300 78\" marker-end=\"url(#arrow-l-js-4-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"455\" y=\"230\" text-anchor=\"middle\">target: where the click started</text>\n</svg><figcaption>A click bubbles from the clicked element up through its ancestors</figcaption></figure>\n<p>Inside a handler, <code dir=\"ltr\">event.target</code> is the element where the event started, and <code dir=\"ltr\">event.currentTarget</code> is the element whose listener is running now. A listener on the <code dir=\"ltr\">&lt;ul&gt;</code> therefore hears clicks on every button inside it and can still find out which button it was.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Event delegation: one listener for the whole list</h2>\n<p>Suppose every expense has a delete button. You could attach a listener to each button, but the render function throws those buttons away and makes new ones every time the list changes, and the new buttons have no listeners. Instead, attach <strong>one</strong> listener to the list, which is never replaced, and work out what was clicked:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">app.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\"> list</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\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"#expense-list\"</span><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\">\"button.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>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// the click was somewhere else in the list</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\">  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:#1F2328;--shiki-dark:#E6EDF3\">  expenses </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> 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:#8250DF;--shiki-dark:#D2A8FF\">  renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p><code dir=\"ltr\">closest(selector)</code> starts at an element and walks up through its ancestors until it finds one that matches, or returns <code dir=\"ltr\">null</code>. That handles clicks on an icon inside the button, and the <code dir=\"ltr\">if (!button)</code> guard ignores clicks on the rest of the item. The id comes from the <code dir=\"ltr\">data-id</code> you set while rendering, and the data change is a <code dir=\"ltr\">filter</code>, exactly as in section 3.</p>\n<p>This pattern is called <strong>event delegation</strong>. It means fewer listeners, and it keeps working for items added after the listener was set up, because their clicks bubble to the same list.</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>Use real buttons for actions</span></p>\n<p>A clickable <code dir=\"ltr\">&lt;div&gt;</code> looks the same but cannot be reached with the Tab key and is not announced as a button by screen readers. A <code dir=\"ltr\">&lt;button type=&quot;button&quot;&gt;</code> gets keyboard support and accessibility for free, and pressing Enter or Space on it fires <code dir=\"ltr\">click</code>.</p>\n</aside>\n<p>To stop a click from bubbling further you can call <code dir=\"ltr\">event.stopPropagation()</code>, but you will rarely need it, and using it often breaks delegation somewhere else. <code dir=\"ltr\">event.preventDefault()</code> is different and far more common: it cancels the browser’s default action, such as following a link or submitting a form. That second one is the star of the next lesson.</p>\n","toc":[{"id":"sec-1","text":"Listening for an event","level":2},{"id":"sec-2","text":"Data first, then render","level":2},{"id":"sec-3","text":"Bubbling: events travel up the tree","level":2},{"id":"sec-4","text":"Event delegation: one listener for the whole list","level":2}],"takeaways":["<code dir=\"ltr\">addEventListener(type, handler)</code> registers a function the browser calls later, with an event object describing what happened.","Pass the handler itself, without parentheses; writing <code dir=\"ltr\">handler()</code> calls it immediately and registers its return value instead.","Events bubble from the element that was clicked up through its ancestors, so a listener on a parent hears clicks on its children.","With event delegation, one listener on a container uses <code dir=\"ltr\">event.target.closest(...)</code> to find which item was clicked, including items added later.","Keep data in variables, change the data in handlers, then call your render function; never treat the page as the source of truth."],"further":[{"title":"Introduction to events (MDN)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Events"},{"title":"EventTarget.addEventListener() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener"},{"title":"Event bubbling (MDN)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling"}],"quiz":[{"q":"<p>What is wrong with <code dir=\"ltr\">button.addEventListener(&quot;click&quot;, deleteExpense());</code>?</p>","options":[{"text":"Event names must be uppercase, as in “CLICK”.","why":"Event types are lowercase strings such as “click” and “keydown”."},{"text":"<code dir=\"ltr\">deleteExpense</code> runs once immediately, and its return value (probably <code dir=\"ltr\">undefined</code>) is registered instead of the function.","why":"Correct. Pass the function itself, <code dir=\"ltr\">deleteExpense</code>, or wrap it, <code dir=\"ltr\">() =&gt; deleteExpense(id)</code>, so the browser calls it on each click."},{"text":"Buttons cannot have click listeners; only links can.","why":"Any element can have a click listener, and buttons are the right element for actions."}],"answer":1},{"q":"<p>A list item contains <code dir=\"ltr\">&lt;span&gt;Coffee&lt;/span&gt; &lt;button class=&quot;delete&quot;&gt;×&lt;/button&gt;</code>. The listener is on the <code dir=\"ltr\">&lt;ul&gt;</code>. A user clicks the button. What are <code dir=\"ltr\">event.target</code> and <code dir=\"ltr\">event.currentTarget</code>?</p>","options":[{"text":"Both are the <code dir=\"ltr\">&lt;ul&gt;</code>.","why":"<code dir=\"ltr\">currentTarget</code> is the <code dir=\"ltr\">&lt;ul&gt;</code>, but <code dir=\"ltr\">target</code> is the element that was actually clicked."},{"text":"<code dir=\"ltr\">target</code> is the <code dir=\"ltr\">&lt;ul&gt;</code> and <code dir=\"ltr\">currentTarget</code> is the button.","why":"It is the other way round. <code dir=\"ltr\">target</code> is where the event started; <code dir=\"ltr\">currentTarget</code> is where the listener is."},{"text":"<code dir=\"ltr\">target</code> is the button and <code dir=\"ltr\">currentTarget</code> is the <code dir=\"ltr\">&lt;ul&gt;</code>.","why":"Correct. The click started on the button and bubbled up to the <code dir=\"ltr\">&lt;ul&gt;</code>, whose listener is now running."}],"answer":2},{"q":"<p>Why does a delegated listener keep working for expenses added after the page loaded?</p>","options":[{"text":"The browser copies listeners to every new child automatically.","why":"Listeners are never copied. Each one stays exactly where you attached it."},{"text":"The listener is on the container, which never changes, and clicks on any child bubble up to it.","why":"Correct. New items are inside the same container, so their clicks reach the same listener."},{"text":"Delegated listeners run again every time the DOM changes.","why":"Listeners only run when their event happens. Nothing re-runs on DOM changes."}],"answer":1},{"q":"<p>Inside a delegated click handler, why use <code dir=\"ltr\">event.target.closest(&quot;.delete&quot;)</code> rather than checking <code dir=\"ltr\">event.target.classList.contains(&quot;delete&quot;)</code>?</p>","options":[{"text":"<code dir=\"ltr\">closest</code> is faster than <code dir=\"ltr\">classList</code>.","why":"Speed is not the issue; both are instant. The issue is which element the click lands on."},{"text":"<code dir=\"ltr\">classList</code> does not work on elements inside a list.","why":"<code dir=\"ltr\">classList</code> works on every element. The problem is that the target may be a child of the button."},{"text":"The click may land on an element inside the button, such as an icon, and <code dir=\"ltr\">closest</code> walks up to find the button anyway.","why":"Correct. <code dir=\"ltr\">closest</code> checks the element itself and then each ancestor, returning the first match or <code dir=\"ltr\">null</code>."}],"answer":2}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<h1>Pocket</h1>\n<button type=\"button\" id=\"add-sample\">Add a sample expense</button>\n<ul id=\"expense-list\"></ul>\n<p id=\"total\"></p>\n","css":"li { display: flex; gap: 8px; align-items: center; }\n","js":"let expenses = [\n  { id: \"exp-1\", label: \"Coffee\", amount: 450 },\n  { id: \"exp-2\", label: \"Train\", amount: 1220 },\n  { id: \"exp-3\", label: \"Lunch\", amount: 1350 },\n];\nlet nextNumber = 4;\n\nconst list = document.querySelector(\"#expense-list\");\nconst totalEl = document.querySelector(\"#total\");\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\n\nfunction renderExpenses() {\n  list.replaceChildren();\n  for (const expense of expenses) {\n    const li = document.createElement(\"li\");\n    li.dataset.id = expense.id;\n    const label = document.createElement(\"span\");\n    label.textContent = `${expense.label}: ${formatMoney(expense.amount)}`;\n    const button = document.createElement(\"button\");\n    button.type = \"button\";\n    button.className = \"delete\";\n    button.textContent = \"Delete\";\n    li.append(label, button);\n    list.append(li);\n  }\n  const total = expenses.reduce((sum, e) => sum + e.amount, 0);\n  totalEl.textContent = `Total: ${formatMoney(total)}`;\n}\n\ndocument.querySelector(\"#add-sample\").addEventListener(\"click\", () => {\n  expenses = [...expenses, { id: `exp-${nextNumber}`, label: `Sample ${nextNumber}`, amount: 100 }];\n  nextNumber++;\n  renderExpenses();\n});\n\nrenderExpenses();\n\n// BUG: these listeners are lost every time renderExpenses rebuilds the list.\n// Replace this block with ONE delegated listener on the list.\nfor (const button of document.querySelectorAll(\".delete\")) {\n  button.addEventListener(\"click\", () => {\n    const id = button.closest(\"li\").dataset.id;\n    expenses = expenses.filter((e) => e.id !== id);\n    renderExpenses();\n  });\n}\n"},"solution":{"html":"<h1>Pocket</h1>\n<button type=\"button\" id=\"add-sample\">Add a sample expense</button>\n<ul id=\"expense-list\"></ul>\n<p id=\"total\"></p>\n","css":"li { display: flex; gap: 8px; align-items: center; }\n","js":"let expenses = [\n  { id: \"exp-1\", label: \"Coffee\", amount: 450 },\n  { id: \"exp-2\", label: \"Train\", amount: 1220 },\n  { id: \"exp-3\", label: \"Lunch\", amount: 1350 },\n];\nlet nextNumber = 4;\n\nconst list = document.querySelector(\"#expense-list\");\nconst totalEl = document.querySelector(\"#total\");\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\n\nfunction renderExpenses() {\n  list.replaceChildren();\n  for (const expense of expenses) {\n    const li = document.createElement(\"li\");\n    li.dataset.id = expense.id;\n    const label = document.createElement(\"span\");\n    label.textContent = `${expense.label}: ${formatMoney(expense.amount)}`;\n    const button = document.createElement(\"button\");\n    button.type = \"button\";\n    button.className = \"delete\";\n    button.textContent = \"Delete\";\n    li.append(label, button);\n    list.append(li);\n  }\n  const total = expenses.reduce((sum, e) => sum + e.amount, 0);\n  totalEl.textContent = `Total: ${formatMoney(total)}`;\n}\n\ndocument.querySelector(\"#add-sample\").addEventListener(\"click\", () => {\n  expenses = [...expenses, { id: `exp-${nextNumber}`, label: `Sample ${nextNumber}`, amount: 100 }];\n  nextNumber++;\n  renderExpenses();\n});\n\nrenderExpenses();\n\nlist.addEventListener(\"click\", (event) => {\n  const button = event.target.closest(\".delete\");\n  if (!button) {\n    return;\n  }\n  const id = button.closest(\"li\").dataset.id;\n  expenses = expenses.filter((e) => e.id !== id);\n  renderExpenses();\n});\n"},"checks":[{"id":"first-delete","test":"await click($('li[data-id=\"exp-2\"] .delete'));\nreturn $$('#expense-list li').length === 2 && !$('li[data-id=\"exp-2\"]') && text('#total') === 'Total: $18.00';\n"},{"id":"second-delete","test":"await click($('li[data-id=\"exp-1\"] .delete'));\nreturn $$('#expense-list li').length === 1 && !$('li[data-id=\"exp-1\"]') && text('#total') === 'Total: $13.50';\n"},{"id":"new-items","test":"await click($('#add-sample'));\nconst added = $('li[data-id=\"exp-4\"] .delete');\nif (!added) return false;\nawait click(added);\nreturn !$('li[data-id=\"exp-4\"]') && $$('#expense-list li').length === 1;\n"},{"id":"ignore-other-clicks","test":"await click($('#expense-list li span'));\nreturn $$('#expense-list li').length === 1;\n"},{"id":"data-updated","test":"return expenses.length === 1 && expenses[0].id === 'exp-3';\n"}],"title":"Delete with one delegated listener","prompt":"<p>Each expense has a Delete button. The starter attaches a listener to each button, but <code dir=\"ltr\">renderExpenses</code> replaces the buttons after the first delete, so the second delete does nothing, and new expenses can never be deleted.</p>\n<p>Replace the loop at the bottom with <strong>one</strong> click listener on <code dir=\"ltr\">#expense-list</code>. When a click lands on (or inside) a <code dir=\"ltr\">.delete</code> button, remove that expense from <code dir=\"ltr\">expenses</code> using the item’s <code dir=\"ltr\">data-id</code>, then call <code dir=\"ltr\">renderExpenses()</code>. Clicks elsewhere in the list must do nothing.</p>\n","hints":["Attach it to the list, which is never replaced: <code dir=\"ltr\">list.addEventListener(&quot;click&quot;, (event) =&gt; { … });</code>.","<code dir=\"ltr\">event.target.closest(&quot;.delete&quot;)</code> gives the button, or <code dir=\"ltr\">null</code> if the click was not on one. Return early when it is <code dir=\"ltr\">null</code>.","The id is on the <code dir=\"ltr\">&lt;li&gt;</code>: <code dir=\"ltr\">button.closest(&quot;li&quot;).dataset.id</code>. Then <code dir=\"ltr\">expenses = expenses.filter((e) =&gt; e.id !== id);</code> and <code dir=\"ltr\">renderExpenses();</code>."],"explanation":"<p>The list element is created once by the HTML and never replaced, so a listener on it survives every render. Clicks on any Delete button bubble up to it, including buttons for expenses that did not exist when the listener was added. <code dir=\"ltr\">closest(&quot;.delete&quot;)</code> finds the button even if the click landed on something inside it, and returns <code dir=\"ltr\">null</code> for clicks on the label, which the early <code dir=\"ltr\">return</code> ignores. The handler changes only the data and then re-renders, so the list and the total always agree with <code dir=\"ltr\">expenses</code>.</p>\n","checkLabels":{"first-delete":"Deleting an expense removes it and updates the total","second-delete":"A second delete still works after the list re-rendered","new-items":"Expenses added later can be deleted too","ignore-other-clicks":"Clicking an item’s label deletes nothing","data-updated":"The expenses array itself is updated, not just the page"}},"words":691,"readMinutes":3}