{"id":"l-js-4-3","courseId":"javascript-fundamentals","lang":"en","kind":"lesson","summary":"Handle a form’s submit event without reloading the page, read and convert its values, validate them with HTML attributes and your own function, and show clear, accessible error messages.","html":"<p>Pocket needs a way to add expenses that is not a “sample” button: a form with a label, an amount and a category. Forms look simple and are where many real bugs live, because they sit on the border between people, who type anything, and your code, which expects clean data.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The form and the submit event</h2>\n<p>Start with proper HTML. Every input has a <code dir=\"ltr\">&lt;label&gt;</code>, and the button is a submit button inside the <code dir=\"ltr\">&lt;form&gt;</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">index.html</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\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">form</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"expense-form\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> novalidate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"label\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>What&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">input</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"label\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"label\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> required</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\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"amount\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Amount ($)&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">input</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"amount\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"amount\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"number\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> step</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"0.01\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> min</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"0.01\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> required</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\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"category\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Category&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">select</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"category\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"category\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">option</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"food\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Food&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">option</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">option</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"travel\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Travel&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">option</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">option</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> value</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"fun\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Fun&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">option</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">select</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\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"form-error\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"error\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"alert\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</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\">>Add expense&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">form</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>A form can be submitted by clicking the button, by pressing Enter in a field, or by assistive technology. All of these fire one event on the form: <code dir=\"ltr\">submit</code>. Listen to that, not to the button’s <code dir=\"ltr\">click</code>:</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\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> form</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-form\"</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\">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:#6E7781;--shiki-dark:#8B949E\">  // read, validate, add, render</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>The browser’s default reaction to a submit is to send the data to a server and load a new page. For an app that runs in the page, that reload wipes out everything. <code dir=\"ltr\">event.preventDefault()</code> cancels the default and keeps you on the page.</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>Forgetting preventDefault</span></p>\n<p>Without <code dir=\"ltr\">event.preventDefault()</code>, the page reloads the moment the form is submitted. Your new expense flashes for a split second, the page starts over, and the console is cleared, so you cannot even see an error. If a form “does nothing” and the URL suddenly gains <code dir=\"ltr\">?label=…</code>, this is why.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">Reading the values</h2>\n<p>You can read each field’s <code dir=\"ltr\">value</code>, or collect them all at once with <code dir=\"ltr\">FormData</code>, which uses each field’s <code dir=\"ltr\">name</code> attribute:</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\"> data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> FormData</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(form);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> rawLabel</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> data.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"label\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);     </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// \"  Coffee with Sam \"</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> rawAmount</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> data.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"amount\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);   </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// \"4.50\": a string!</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> category</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> data.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"category\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);  </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// \"food\"</span></span></code></pre></div>\n<p>Every value is a <strong>string</strong>, even from a number input. This is the “typed input” problem from section 1, now for real. Clean the values up immediately: <code dir=\"ltr\">trim()</code> the label, and convert the amount once, into cents:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"3\"><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\"> label</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> rawLabel.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</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\"> amount</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Math.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(rawAmount) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span></code></pre></div>\n<h2 id=\"sec-3\" tabindex=\"-1\">Validation in a pure function</h2>\n<p>Put the rules in one function that takes values and returns what is wrong with them. It does not touch the page, so it is easy to read and easy to test:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"4\" aria-label=\"Run this example\"><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=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><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\"> validateExpense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">amount</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\"> errors</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\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (label.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</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\">    errors.label </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"Enter what you spent money on.\"</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:#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\">Number.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">isFinite</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(amount) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> amount </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;=</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:#1F2328;--shiki-dark:#E6EDF3\">    errors.amount </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> \"Enter an amount greater than zero.\"</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:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> errors;</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\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">validateExpense</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\">, amount: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</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\">console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">validateExpense</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\">4.5</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }));</span></span></code></pre><div class=\"run-output\" data-run-output=\"4\" hidden></div></div>\n<p>An empty object means “valid”, which you test with <code dir=\"ltr\">Object.keys(errors).length === 0</code>. Notice <code dir=\"ltr\">Number(&quot;&quot;)</code> is <code dir=\"ltr\">0</code>, the empty-input trap from section 1, so “nothing typed” correctly fails the <code dir=\"ltr\">amount &lt;= 0</code> rule, and <code dir=\"ltr\">Number.isFinite</code> rejects <code dir=\"ltr\">NaN</code> and <code dir=\"ltr\">Infinity</code> in one check.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 210\" role=\"img\" aria-labelledby=\"l-js-4-3-t1\"><defs><marker id=\"arrow-l-js-4-3-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-3-t1\">On submit: prevent the default reload, read and convert the values, then validate. If there are errors, show them and focus the first invalid field. If valid, add the expense, render, and reset the form.</title>\n  <rect class=\"d-box-accent\" x=\"10\" y=\"80\" width=\"120\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"70\" y=\"110\" text-anchor=\"middle\">submit</text>\n  <rect class=\"d-box\" x=\"160\" y=\"80\" width=\"150\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"235\" y=\"103\" text-anchor=\"middle\">preventDefault,</text>\n  <text class=\"d-label\" x=\"235\" y=\"121\" text-anchor=\"middle\">read + convert</text>\n  <rect class=\"d-box-primary\" x=\"340\" y=\"80\" width=\"110\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"395\" y=\"110\" text-anchor=\"middle\">validate</text>\n  <rect class=\"d-box-warn\" x=\"490\" y=\"14\" width=\"180\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"580\" y=\"38\" text-anchor=\"middle\">show errors,</text>\n  <text class=\"d-label\" x=\"580\" y=\"58\" text-anchor=\"middle\">focus the field</text>\n  <rect class=\"d-box-success\" x=\"490\" y=\"140\" width=\"180\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"580\" y=\"164\" text-anchor=\"middle\">add, render,</text>\n  <text class=\"d-label\" x=\"580\" y=\"184\" text-anchor=\"middle\">reset the form</text>\n  <path class=\"d-arrow\" d=\"M130 105 L156 105\" marker-end=\"url(#arrow-l-js-4-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M310 105 L336 105\" marker-end=\"url(#arrow-l-js-4-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M450 95 L486 50\" marker-end=\"url(#arrow-l-js-4-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M450 115 L486 160\" marker-end=\"url(#arrow-l-js-4-3-1)\"/>\n  <text class=\"d-label-muted\" x=\"455\" y=\"62\" text-anchor=\"middle\">errors</text>\n  <text class=\"d-label-muted\" x=\"455\" y=\"160\" text-anchor=\"middle\">valid</text>\n</svg><figcaption>What happens on submit</figcaption></figure>\n<h2 id=\"sec-4\" tabindex=\"-1\">Putting it together</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"5\"><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:#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\"> data</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> FormData</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(form);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> values</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\">    label: data.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"label\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    amount: Math.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">round</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(data.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"amount\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    category: data.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"category\"</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\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> validateExpense</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(values);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> errorEl</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\">\"#form-error\"</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\"> (Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">keys</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(errors).</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:#1F2328;--shiki-dark:#E6EDF3\">    errorEl.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">values</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(errors).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">join</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</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\"> firstBad</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> form.elements[Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">keys</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(errors)[</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:#1F2328;--shiki-dark:#E6EDF3\">    firstBad.</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-invalid\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"true\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    firstBad.</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:#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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  errorEl.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\">;</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">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\">values }];</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</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></code></pre></div>\n<p>On failure, the message goes in an element with <code dir=\"ltr\">role=&quot;alert&quot;</code>, which screen readers announce as soon as its text changes; the first invalid field is marked with <code dir=\"ltr\">aria-invalid</code> and receives focus, so the user can fix it straight away. On success, the data changes, the list re-renders, <code dir=\"ltr\">form.reset()</code> empties the fields, and focus returns to the first field, ready for the next expense. A real app would also clear <code dir=\"ltr\">aria-invalid</code> once a field is fixed. <code dir=\"ltr\">form.elements.label</code> looks up a field by its <code dir=\"ltr\">name</code>.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Clearing errors as the user types</h2>\n<p>Showing an error is half the job; the other half is taking it away at the right moment. If the message stays on screen while the user is already typing the fix, the form feels like it is scolding them. Listen for <code dir=\"ltr\">input</code> events on the form (they bubble, so one listener covers every field) and clear the error state of whichever field changed:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"6\"><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:#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\">\"input\"</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.target.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">removeAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"aria-invalid\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><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\">\"#form-error\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).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\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>Resist the opposite urge, validating every keystroke and showing “Enter an amount” while someone is halfway through typing it. A good default: validate on submit, clear on input.</p>\n<h2 id=\"sec-6\" tabindex=\"-1\">HTML validation: the first layer</h2>\n<p>The <code dir=\"ltr\">required</code>, <code dir=\"ltr\">type=&quot;number&quot;</code>, <code dir=\"ltr\">min</code> and <code dir=\"ltr\">step</code> attributes let the browser check basic rules and show its own messages. The <code dir=\"ltr\">novalidate</code> attribute on the form above turns those messages off so yours are shown consistently, while the attributes still document intent and give phones the right keyboard. You can also ask the browser directly with <code dir=\"ltr\">form.checkValidity()</code>.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Never trust only the browser</span></p>\n<p>Anyone can open DevTools and delete <code dir=\"ltr\">required</code>, or send data without using your form at all. Front-end validation exists to help honest users fix mistakes quickly. Anything that must be true, such as an amount being positive before it reaches a bank, has to be checked again on the server.</p>\n</aside>\n<p>Your form now adds real expenses. Reload the page, though, and they are gone, because they only ever lived in a variable. Next lesson fixes that with JSON and <code dir=\"ltr\">localStorage</code>.</p>\n","toc":[{"id":"sec-1","text":"The form and the submit event","level":2},{"id":"sec-2","text":"Reading the values","level":2},{"id":"sec-3","text":"Validation in a pure function","level":2},{"id":"sec-4","text":"Putting it together","level":2},{"id":"sec-5","text":"Clearing errors as the user types","level":2},{"id":"sec-6","text":"HTML validation: the first layer","level":2}],"takeaways":["Listen for <code dir=\"ltr\">submit</code> on the form, not <code dir=\"ltr\">click</code> on the button, so every way of submitting (button, Enter key, assistive technology) runs your code, and call <code dir=\"ltr\">event.preventDefault()</code> to stop the page reloading.","Every value read from a form is a string; trim text and convert numbers before using them.","Keep validation in a pure function that takes values and returns errors, so the rules are easy to read and test.","HTML attributes such as <code dir=\"ltr\">required</code> and <code dir=\"ltr\">min</code> are a useful first layer, but your JavaScript must still validate, and the server must too.","Show errors next to the field, mark it with <code dir=\"ltr\">aria-invalid</code>, and move focus so keyboard and screen-reader users notice."],"further":[{"title":"Client-side form validation (MDN)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Form_validation"},{"title":"FormData (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/FormData"},{"title":"HTMLFormElement: submit event (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit_event"}],"quiz":[{"q":"<p>You attach your “add expense” logic to the Save button’s <code dir=\"ltr\">click</code> event. What breaks?</p>","options":[{"text":"The click handler covers only one way in, and the form still submits natively afterwards, reloading the page.","why":"Correct. Forms can also be submitted by <code dir=\"ltr\">form.requestSubmit()</code>, by some assistive technology, or by Enter in a form with no submit button, and a click handler does not cancel the native submission. Listening for <code dir=\"ltr\">submit</code> on the form catches every path, and <code dir=\"ltr\">preventDefault()</code> there stops the reload."},{"text":"Nothing; <code dir=\"ltr\">click</code> and <code dir=\"ltr\">submit</code> are interchangeable.","why":"They are different events. <code dir=\"ltr\">submit</code> fires on the form however it is submitted; a click on the button is only one of the ways a submission can start."},{"text":"The button can no longer be focused with the keyboard.","why":"Adding a listener does not change focus behaviour. The problem is the submission path you are not handling."}],"answer":0},{"q":"<p>A number input contains <code dir=\"ltr\">12.50</code>. What is <code dir=\"ltr\">input.value</code>?</p>","options":[{"text":"The number <code dir=\"ltr\">12.5</code>.","why":"Even for <code dir=\"ltr\">type=&quot;number&quot;</code>, <code dir=\"ltr\">value</code> is always a string. Use <code dir=\"ltr\">Number(input.value)</code> or <code dir=\"ltr\">input.valueAsNumber</code>."},{"text":"The string <code dir=\"ltr\">&quot;12.50&quot;</code>.","why":"Correct. Form values are strings, so convert before doing maths, or <code dir=\"ltr\">+</code> will join text."},{"text":"<code dir=\"ltr\">NaN</code>, until the form is submitted.","why":"The value is available at any time, and it is the text in the field, not <code dir=\"ltr\">NaN</code>."}],"answer":1},{"q":"<p>Your form has <code dir=\"ltr\">required</code> and <code dir=\"ltr\">min=&quot;0.01&quot;</code> on the amount field. Why validate again in JavaScript?</p>","options":[{"text":"Browsers ignore HTML validation attributes on number fields.","why":"Browsers do honour them. The extra checks are about rules HTML cannot express and about not trusting any single layer."},{"text":"HTML validation is deprecated in favour of JavaScript.","why":"Built-in validation is current and useful. It is a first layer, not a replacement for your own checks."},{"text":"Some rules (such as “no more than two decimals” or “label not only spaces”) need code, and attributes can be removed in DevTools.","why":"Correct. Treat HTML validation as a convenience for users, and the server as the final authority for anything that matters."}],"answer":2}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<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  <p id=\"form-error\" role=\"alert\"></p>\n  <button type=\"submit\">Add expense</button>\n</form>\n<ul id=\"expense-list\"></ul>\n","css":"form { display: grid; gap: 4px; max-width: 280px; }\n#form-error { color: #b42318; min-height: 1.5em; margin: 0; }\n[aria-invalid=\"true\"] { outline: 2px solid #b42318; }\n","js":"let expenses = [{ id: \"exp-1\", label: \"Train\", amount: 1220 }];\nlet count = 1;\nconst nextId = () => `exp-${++count}`;\n\nconst form = document.querySelector(\"#expense-form\");\nconst errorEl = document.querySelector(\"#form-error\");\nconst list = document.querySelector(\"#expense-list\");\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\n\nfunction renderExpenses() {\n  list.replaceChildren();\n  for (const e of expenses) {\n    const li = document.createElement(\"li\");\n    li.textContent = `${e.label}: ${formatMoney(e.amount)}`;\n    list.append(li);\n  }\n}\nrenderExpenses();\n\n// Return an object of error messages: { label: \"...\", amount: \"...\" }.\n// An empty object means the values are valid.\nfunction validateExpense({ label, amount }) {\n  return {};\n}\n\nform.addEventListener(\"submit\", (event) => {\n  const data = new FormData(form);\n  const values = { label: data.get(\"label\"), amount: data.get(\"amount\") };\n  expenses = [...expenses, { id: nextId(), ...values }];\n  renderExpenses();\n});\n"},"solution":{"html":"<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  <p id=\"form-error\" role=\"alert\"></p>\n  <button type=\"submit\">Add expense</button>\n</form>\n<ul id=\"expense-list\"></ul>\n","css":"form { display: grid; gap: 4px; max-width: 280px; }\n#form-error { color: #b42318; min-height: 1.5em; margin: 0; }\n[aria-invalid=\"true\"] { outline: 2px solid #b42318; }\n","js":"let expenses = [{ id: \"exp-1\", label: \"Train\", amount: 1220 }];\nlet count = 1;\nconst nextId = () => `exp-${++count}`;\n\nconst form = document.querySelector(\"#expense-form\");\nconst errorEl = document.querySelector(\"#form-error\");\nconst list = document.querySelector(\"#expense-list\");\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\n\nfunction renderExpenses() {\n  list.replaceChildren();\n  for (const e of expenses) {\n    const li = document.createElement(\"li\");\n    li.textContent = `${e.label}: ${formatMoney(e.amount)}`;\n    list.append(li);\n  }\n}\nrenderExpenses();\n\nfunction validateExpense({ label, amount }) {\n  const errors = {};\n  if (label.trim() === \"\") {\n    errors.label = \"Enter what you spent money on.\";\n  }\n  if (!Number.isFinite(amount) || amount <= 0) {\n    errors.amount = \"Enter an amount greater than zero.\";\n  }\n  return errors;\n}\n\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  };\n\n  for (const field of form.querySelectorAll(\"input\")) {\n    field.removeAttribute(\"aria-invalid\");\n  }\n\n  const errors = validateExpense(values);\n  const names = Object.keys(errors);\n  if (names.length > 0) {\n    errorEl.textContent = Object.values(errors).join(\" \");\n    const firstBad = form.elements[names[0]];\n    firstBad.setAttribute(\"aria-invalid\", \"true\");\n    firstBad.focus();\n    return;\n  }\n\n  errorEl.textContent = \"\";\n  expenses = [...expenses, { id: nextId(), ...values }];\n  renderExpenses();\n  form.reset();\n  form.elements.label.focus();\n});\n"},"checks":[{"id":"validate-fn","test":"const bad = validateExpense({ label: '   ', amount: 0 });\nconst nan = validateExpense({ label: 'Tea', amount: NaN });\nconst good = validateExpense({ label: 'Tea', amount: 300 });\nreturn 'label' in bad && 'amount' in bad && 'amount' in nan && !('label' in nan) && Object.keys(good).length === 0;\n"},{"id":"empty-blocked","test":"await input($('#label'), '   ');\nawait input($('#amount'), '');\nawait click($('button[type=\"submit\"]'));\nreturn $$('#expense-list li').length === 1 && text('#form-error') !== '' && $('#label').getAttribute('aria-invalid') === 'true';\n"},{"id":"prevents-default","test":"let prevented = null;\nconst spy = (event) => { prevented = event.defaultPrevented; };\ndocument.addEventListener('submit', spy);\nawait click($('button[type=\"submit\"]'));\ndocument.removeEventListener('submit', spy);\nreturn prevented === true;\n"},{"id":"adds-valid","test":"await input($('#label'), '  Coffee with Sam ');\nawait input($('#amount'), '4.50');\nawait click($('button[type=\"submit\"]'));\nconst items = $$('#expense-list li');\nconst last = expenses[expenses.length - 1];\nreturn items.length === 2 && items[1].textContent.trim() === 'Coffee with Sam: $4.50' && last.amount === 450 && last.label === 'Coffee with Sam';\n"},{"id":"resets","test":"return $('#label').value === '' && $('#amount').value === '' && text('#form-error') === '';\n"}],"title":"Add expenses with a validated form","prompt":"<p>Make Pocket’s form add expenses safely.</p>\n<ol>\n<li>Write <code dir=\"ltr\">validateExpense({ label, amount })</code>. It returns an object with a <code dir=\"ltr\">label</code> message when the label is empty or only spaces, and an <code dir=\"ltr\">amount</code> message when the amount is not a finite number greater than 0. Valid values give <code dir=\"ltr\">{}</code>.</li>\n<li>Fix the submit handler: stop the page from reloading, trim the label and convert the amount from dollars to whole cents, then validate.</li>\n<li>If there are errors, put a message in <code dir=\"ltr\">#form-error</code>, set <code dir=\"ltr\">aria-invalid=&quot;true&quot;</code> on the first invalid field and add nothing.</li>\n<li>If valid, add the expense, re-render, clear <code dir=\"ltr\">#form-error</code> and reset the form.</li>\n</ol>\n","hints":["The first line of the handler should be <code dir=\"ltr\">event.preventDefault();</code>.","<code dir=\"ltr\">data.get(&quot;amount&quot;)</code> is a string. <code dir=\"ltr\">Math.round(Number(data.get(&quot;amount&quot;)) * 100)</code> gives cents; <code dir=\"ltr\">Number(&quot;&quot;)</code> is 0, which your amount rule rejects.","After validating: <code dir=\"ltr\">const names = Object.keys(errors);</code>. If <code dir=\"ltr\">names.length &gt; 0</code>, show the messages, use <code dir=\"ltr\">form.elements[names[0]].setAttribute(&quot;aria-invalid&quot;, &quot;true&quot;)</code> and <code dir=\"ltr\">return</code>. Otherwise add, render, <code dir=\"ltr\">form.reset()</code>."],"explanation":"<p><code dir=\"ltr\">preventDefault()</code> cancels the browser’s own submission so the page stays put. The handler converts at the border: the label is trimmed and the amount becomes whole cents straight away, so everything after that line works with clean data. <code dir=\"ltr\">validateExpense</code> is a pure function: values in, error messages out, nothing on the page touched, which is why the first check can call it directly. The handler only decides what to show. Returning early on errors keeps the success path flat and readable, and <code dir=\"ltr\">form.reset()</code> plus clearing the message gets the form ready for the next expense. Clearing <code dir=\"ltr\">aria-invalid</code> at the start of each submit means a fixed field stops being announced as invalid.</p>\n","checkLabels":{"validate-fn":"<code dir=\"ltr\">validateExpense</code> returns the right messages, and {} for valid values","empty-blocked":"An empty form shows an error, marks the label field invalid and adds nothing","prevents-default":"The submit handler calls preventDefault()","adds-valid":"A valid entry is added with a trimmed label and the amount in cents","resets":"After a successful add, the form and error message are cleared"}},"words":698,"readMinutes":3}