{"id":"l-js-4-3","courseId":"javascript-fundamentals","lang":"ar","kind":"lesson","summary":"تتعامل مع حدث submit في النموذج دون إعادة تحميل الصفحة، وتقرأ قيمه وتحوّلها، وتتحقّق منها بسمات HTML وبدالة تكتبها بنفسك، وتعرض رسائل خطأ واضحة ويسهل الوصول إليها.","html":"<p>يحتاج Pocket إلى طريقة لإضافة المصاريف غير زر “عيّنة”: نموذج (form) فيه تسمية ومبلغ وفئة. تبدو النماذج بسيطة، لكنها المكان الذي يعيش فيه كثير من الأخطاء الحقيقية، لأنها تقف على الحدّ الفاصل بين البشر، الذين يكتبون أي شيء، وكودك، الذي يتوقّع بيانات نظيفة.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">النموذج وحدث submit</h2>\n<p>ابدأ بـ HTML سليم. لكل حقل <code dir=\"ltr\">&lt;label&gt;</code>، والزر زر إرسال داخل <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=\"نسخ الكود\"><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>نسخ</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>يمكن إرسال النموذج بالنقر على الزر، أو بالضغط على Enter داخل حقل، أو عبر التقنيات المساعدة. وكل ذلك يُطلق حدثًا واحدًا على النموذج: <code dir=\"ltr\">submit</code>. استمع إليه، لا إلى <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=\"نسخ الكود\"><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>نسخ</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>ردّ الفعل الافتراضي للمتصفّح على الإرسال هو أن يبعث البيانات إلى خادم ويحمّل صفحة جديدة. ومع تطبيق يعمل داخل الصفحة، تمسح إعادة التحميل هذه كل شيء. و<code dir=\"ltr\">event.preventDefault()</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\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>نسيان preventDefault</span></p>\n<p>دون <code dir=\"ltr\">event.preventDefault()</code>، يُعاد تحميل الصفحة لحظة إرسال النموذج. يومض مصروفك الجديد لجزء من الثانية، وتبدأ الصفحة من جديد، ويُمسح الـ console، فلا تستطيع حتى رؤية أي خطأ. إن كان النموذج “لا يفعل شيئًا” وظهر فجأة في الرابط <code dir=\"ltr\">?label=…</code>، فهذا هو السبب.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">قراءة القيم</h2>\n<p>يمكنك قراءة <code dir=\"ltr\">value</code> لكل حقل، أو جمعها كلها مرة واحدة باستخدام <code dir=\"ltr\">FormData</code>، التي تعتمد على سمة <code dir=\"ltr\">name</code> لكل حقل:</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=\"نسخ الكود\"><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>نسخ</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>كل قيمة <strong>نص</strong>، حتى لو جاءت من حقل أرقام. هذه مشكلة “المدخلات المكتوبة” من القسم 1، لكنها الآن حقيقية. نظّف القيم فورًا: شذّب التسمية بـ <code dir=\"ltr\">trim()</code>، وحوّل المبلغ مرة واحدة، إلى سنتات:</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=\"نسخ الكود\"><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>نسخ</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\">التحقّق في دالة نقية</h2>\n<p>ضع القواعد في دالة واحدة تأخذ القيم وتُرجع ما فيها من خطأ. إنها لا تمسّ الصفحة، لذلك يسهل قراءتها ويسهل اختبارها:</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=\"شغّل هذا المثال\"><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>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><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>نسخ</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>الكائن الفارغ يعني “صالح”، وتفحص ذلك بـ <code dir=\"ltr\">Object.keys(errors).length === 0</code>. لاحظ أن <code dir=\"ltr\">Number(&quot;&quot;)</code> تساوي <code dir=\"ltr\">0</code>، وهو فخّ المدخلات الفارغة من القسم 1، لذلك تفشل حالة “لم يُكتب شيء” في قاعدة <code dir=\"ltr\">amount &lt;= 0</code> كما ينبغي، و<code dir=\"ltr\">Number.isFinite</code> ترفض <code dir=\"ltr\">NaN</code> و<code dir=\"ltr\">Infinity</code> في فحص واحد.</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\">عند الإرسال: امنع إعادة التحميل الافتراضية، واقرأ القيم وحوّلها، ثم تحقّق منها. إن وُجدت أخطاء، فاعرضها وانقل التركيز إلى أول حقل غير صالح. وإن كانت صالحة، فأضف المصروف، وأعد العرض، وأفرغ النموذج.</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\">اقرأ وحوّل</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\">تحقّق</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\">اعرض الأخطاء،</text>\n  <text class=\"d-label\" x=\"580\" y=\"58\" text-anchor=\"middle\">ركّز على الحقل</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\">أضف، اعرض،</text>\n  <text class=\"d-label\" x=\"580\" y=\"184\" text-anchor=\"middle\">أفرغ النموذج</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\">أخطاء</text>\n  <text class=\"d-label-muted\" x=\"455\" y=\"160\" text-anchor=\"middle\">صالح</text>\n</svg><figcaption>ماذا يحدث عند الإرسال</figcaption></figure>\n<h2 id=\"sec-4\" tabindex=\"-1\">جمع كل شيء معًا</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=\"نسخ الكود\"><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>نسخ</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>عند الفشل، تذهب الرسالة إلى عنصر يحمل <code dir=\"ltr\">role=&quot;alert&quot;</code>، تعلن عنه قارئات الشاشة بمجرّد أن يتغيّر نصّه؛ ويُعلَّم أول حقل غير صالح بـ <code dir=\"ltr\">aria-invalid</code> وينتقل إليه التركيز، فيستطيع المستخدم إصلاحه فورًا. وعند النجاح، تتغيّر البيانات، ويُعاد عرض القائمة، وتُفرغ <code dir=\"ltr\">form.reset()</code> الحقول، ويعود التركيز إلى الحقل الأول، جاهزًا للمصروف التالي. والتطبيق الحقيقي سيُزيل أيضًا <code dir=\"ltr\">aria-invalid</code> بمجرّد إصلاح الحقل. أما <code dir=\"ltr\">form.elements.label</code> فتبحث عن حقل باسمه <code dir=\"ltr\">name</code>.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">مسح الأخطاء أثناء كتابة المستخدم</h2>\n<p>عرض الخطأ نصف المهمة؛ والنصف الآخر إزالته في اللحظة المناسبة. فإن بقيت الرسالة على الشاشة والمستخدم يكتب الإصلاح بالفعل، شعر أن النموذج يوبّخه. استمع إلى أحداث <code dir=\"ltr\">input</code> على النموذج (فهي تنتشر للأعلى، لذلك يغطّي مستمع واحد كل الحقول) وامسح حالة الخطأ في الحقل الذي تغيّر:</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=\"نسخ الكود\"><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>نسخ</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>وقاوِم الرغبة المعاكسة، أي التحقّق مع كل ضغطة مفتاح وعرض “Enter an amount” بينما الشخص في منتصف كتابته. والخيار الافتراضي الجيد: تحقّق عند الإرسال، وامسح عند الإدخال.</p>\n<h2 id=\"sec-6\" tabindex=\"-1\">التحقّق في HTML: الطبقة الأولى</h2>\n<p>السمات <code dir=\"ltr\">required</code> و<code dir=\"ltr\">type=&quot;number&quot;</code> و<code dir=\"ltr\">min</code> و<code dir=\"ltr\">step</code> تتيح للمتصفّح فحص القواعد الأساسية وعرض رسائله الخاصة. والسمة <code dir=\"ltr\">novalidate</code> على النموذج أعلاه توقف تلك الرسائل كي تُعرض رسائلك أنت بشكل متّسق، بينما تظل السمات توثّق النية وتعطي الهواتف لوحة المفاتيح المناسبة. ويمكنك أيضًا أن تسأل المتصفّح مباشرة عبر <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\">لماذا يهمّ هذا</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>لا تثق بالمتصفّح وحده</span></p>\n<p>يستطيع أي شخص أن يفتح DevTools ويحذف <code dir=\"ltr\">required</code>، أو أن يرسل البيانات دون استخدام نموذجك أصلًا. التحقّق في الواجهة الأمامية موجود ليساعد المستخدمين الصادقين على إصلاح أخطائهم بسرعة. أما كل ما يجب أن يكون صحيحًا، مثل أن يكون المبلغ موجبًا قبل أن يصل إلى بنك، فيجب فحصه مجددًا على الخادم.</p>\n</aside>\n<p>صار نموذجك يضيف مصاريف حقيقية. لكن أعِد تحميل الصفحة وستختفي، لأنها لم تعش قط إلا في متغيّر. الدرس التالي يُصلح ذلك باستخدام JSON و<code dir=\"ltr\">localStorage</code>.</p>\n","toc":[{"id":"sec-1","text":"النموذج وحدث submit","level":2},{"id":"sec-2","text":"قراءة القيم","level":2},{"id":"sec-3","text":"التحقّق في دالة نقية","level":2},{"id":"sec-4","text":"جمع كل شيء معًا","level":2},{"id":"sec-5","text":"مسح الأخطاء أثناء كتابة المستخدم","level":2},{"id":"sec-6","text":"التحقّق في HTML: الطبقة الأولى","level":2}],"takeaways":["استمع إلى <code dir=\"ltr\">submit</code> على النموذج، لا إلى <code dir=\"ltr\">click</code> على الزر، كي تمرّ كل طرق الإرسال (الزر، ومفتاح Enter، والتقنيات المساعدة) عبر كودك، واستدعِ <code dir=\"ltr\">event.preventDefault()</code> لتمنع إعادة تحميل الصفحة.","كل قيمة تُقرأ من نموذج هي نص؛ شذّب النصوص وحوّل الأرقام قبل استخدامها.","أبقِ التحقّق من المدخلات في دالة نقية تأخذ القيم وتُرجع الأخطاء، كي تكون القواعد سهلة القراءة والاختبار.","سمات HTML مثل <code dir=\"ltr\">required</code> و<code dir=\"ltr\">min</code> طبقة أولى مفيدة، لكن يجب أن يتحقّق كود JavaScript أيضًا، وأن يتحقّق الخادم كذلك.","اعرض الأخطاء بجوار الحقل، وعلّمه بـ <code dir=\"ltr\">aria-invalid</code>، وانقل التركيز إليه كي ينتبه مستخدمو لوحة المفاتيح وقارئات الشاشة."],"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>تُلحق منطق “إضافة مصروف” بحدث <code dir=\"ltr\">click</code> على زر الحفظ. ما الذي يتعطّل؟</p>","options":[{"text":"معالج النقر لا يغطّي إلا طريقًا واحدًا للإرسال، ويظل النموذج يُرسَل بالطريقة الأصلية بعده فيُعاد تحميل الصفحة.","why":"صحيح. يمكن إرسال النماذج أيضًا عبر <code dir=\"ltr\">form.requestSubmit()</code>، أو ببعض التقنيات المساعدة، أو بالضغط على Enter في نموذج ليس فيه زر إرسال، ومعالج النقر لا يلغي الإرسال الأصلي. أما الاستماع إلى <code dir=\"ltr\">submit</code> على النموذج فيلتقط كل الطرق، و<code dir=\"ltr\">preventDefault()</code> هناك تمنع إعادة التحميل."},{"text":"لا شيء؛ <code dir=\"ltr\">click</code> و<code dir=\"ltr\">submit</code> قابلان للتبادل.","why":"إنهما حدثان مختلفان. <code dir=\"ltr\">submit</code> يقع على النموذج أيًّا كانت طريقة إرساله؛ أما النقر على الزر فليس إلا إحدى الطرق التي قد يبدأ بها الإرسال."},{"text":"لا يعود بالإمكان التركيز على الزر بلوحة المفاتيح.","why":"إضافة مستمع لا تغيّر سلوك التركيز. المشكلة في مسار الإرسال الذي لا تتعامل معه."}],"answer":0},{"q":"<p>حقل أرقام يحتوي على <code dir=\"ltr\">12.50</code>. ما قيمة <code dir=\"ltr\">input.value</code>؟</p>","options":[{"text":"الرقم <code dir=\"ltr\">12.5</code>.","why":"حتى مع <code dir=\"ltr\">type=&quot;number&quot;</code>، تكون <code dir=\"ltr\">value</code> نصًا دائمًا. استخدم <code dir=\"ltr\">Number(input.value)</code> أو <code dir=\"ltr\">input.valueAsNumber</code>."},{"text":"النص <code dir=\"ltr\">&quot;12.50&quot;</code>.","why":"صحيح. قيم النماذج نصوص، لذلك حوّلها قبل أي عملية حسابية، وإلا فستلصق <code dir=\"ltr\">+</code> النصوص."},{"text":"<code dir=\"ltr\">NaN</code>، إلى أن يُرسل النموذج.","why":"القيمة متاحة في أي وقت، وهي النص الموجود في الحقل، لا <code dir=\"ltr\">NaN</code>."}],"answer":1},{"q":"<p>في نموذجك سمتا <code dir=\"ltr\">required</code> و<code dir=\"ltr\">min=&quot;0.01&quot;</code> على حقل المبلغ. لماذا تتحقّق مجددًا في JavaScript؟</p>","options":[{"text":"المتصفّحات تتجاهل سمات التحقّق في HTML على حقول الأرقام.","why":"المتصفّحات تحترمها. والفحوص الإضافية تتعلّق بقواعد لا تستطيع HTML التعبير عنها، وبعدم الثقة بأي طبقة منفردة."},{"text":"التحقّق في HTML مهجور لصالح JavaScript.","why":"التحقّق المدمج حديث ومفيد. إنه طبقة أولى، لا بديل عن فحوصك الخاصة."},{"text":"بعض القواعد (مثل “لا أكثر من منزلتين عشريتين” أو “التسمية ليست مسافات فقط”) تحتاج إلى كود، ويمكن حذف السمات من DevTools.","why":"صحيح. عامل التحقّق في HTML على أنه وسيلة لراحة المستخدمين، واجعل الخادم المرجع النهائي لكل ما هو مهم."}],"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":"أضف المصاريف بنموذج يتحقّق من المدخلات","prompt":"<p>اجعل نموذج Pocket يضيف المصاريف بأمان.</p>\n<ol>\n<li>اكتب <code dir=\"ltr\">validateExpense({ label, amount })</code>. تُرجع كائنًا فيه رسالة <code dir=\"ltr\">label</code> حين تكون التسمية فارغة أو مسافات فقط، ورسالة <code dir=\"ltr\">amount</code> حين لا يكون المبلغ رقمًا منتهيًا (finite) أكبر من 0. والقيم الصالحة تعطي <code dir=\"ltr\">{}</code>.</li>\n<li>أصلح معالج الإرسال: امنع الصفحة من إعادة التحميل، وشذّب التسمية، وحوّل المبلغ من دولارات إلى سنتات صحيحة، ثم تحقّق.</li>\n<li>إن وُجدت أخطاء، فضع رسالة في <code dir=\"ltr\">#form-error</code>، واضبط <code dir=\"ltr\">aria-invalid=&quot;true&quot;</code> على أول حقل غير صالح، ولا تُضف شيئًا.</li>\n<li>إن كانت صالحة، فأضف المصروف، وأعد العرض، وامسح <code dir=\"ltr\">#form-error</code>، وأفرغ النموذج.</li>\n</ol>\n","hints":["يجب أن يكون السطر الأول في معالج الحدث <code dir=\"ltr\">event.preventDefault();</code>.","<code dir=\"ltr\">data.get(&quot;amount&quot;)</code> نص. و<code dir=\"ltr\">Math.round(Number(data.get(&quot;amount&quot;)) * 100)</code> تعطي السنتات؛ و<code dir=\"ltr\">Number(&quot;&quot;)</code> تساوي 0، وقاعدة المبلغ لديك ترفضها.","بعد التحقّق: <code dir=\"ltr\">const names = Object.keys(errors);</code>. إن كان <code dir=\"ltr\">names.length &gt; 0</code>، فاعرض الرسائل، واستخدم <code dir=\"ltr\">form.elements[names[0]].setAttribute(&quot;aria-invalid&quot;, &quot;true&quot;)</code> ثم <code dir=\"ltr\">return</code>. وإلا فأضف، واعرض، ثم <code dir=\"ltr\">form.reset()</code>."],"explanation":"<p>تلغي <code dir=\"ltr\">preventDefault()</code> الإرسال الخاص بالمتصفّح فتبقى الصفحة في مكانها. ويحوّل معالج الحدث القيم عند الحدود: تُشذَّب التسمية ويصبح المبلغ سنتات صحيحة فورًا، فيعمل كل ما بعد ذلك السطر مع بيانات نظيفة. و<code dir=\"ltr\">validateExpense</code> دالة نقية: قيم تدخل، ورسائل خطأ تخرج، ولا شيء في الصفحة يُمسّ، ولهذا يستطيع الفحص الأول استدعاءها مباشرة. ومعالج الحدث يقرّر فقط ما يُعرض. والإرجاع المبكر عند الأخطاء يُبقي مسار النجاح مسطّحًا ومقروءًا، و<code dir=\"ltr\">form.reset()</code> مع مسح الرسالة يجهّزان النموذج للمصروف التالي. ومسح <code dir=\"ltr\">aria-invalid</code> في بداية كل إرسال يعني أن الحقل الذي أُصلح لم يعد يُعلَن عنه على أنه غير صالح.</p>\n","checkLabels":{"validate-fn":"<code dir=\"ltr\">validateExpense</code> تُرجع الرسائل الصحيحة، و{} للقيم الصالحة","empty-blocked":"النموذج الفارغ يعرض خطأ، ويعلّم حقل التسمية بأنه غير صالح، ولا يضيف شيئًا","prevents-default":"معالج الإرسال يستدعي preventDefault()","adds-valid":"يُضاف الإدخال الصالح بتسمية مشذّبة والمبلغ بالسنتات","resets":"بعد الإضافة الناجحة، يُفرغ النموذج وتُمسح رسالة الخطأ"}},"words":588,"readMinutes":3}