الوحدة 4 · JavaScript في المتصفّح
النماذج والتحقّق من المدخلات
17 دقراءة 3 دبيئة تجريب الكود
الدرس في لمحة
تتعامل مع حدث submit في النموذج دون إعادة تحميل الصفحة، وتقرأ قيمه وتحوّلها، وتتحقّق منها بسمات HTML وبدالة تكتبها بنفسك، وتعرض رسائل خطأ واضحة ويسهل الوصول إليها.
- 1النموذج وحدث submit
- 2قراءة القيم
- 3التحقّق في دالة نقية
- 4جمع كل شيء معًا
- 5مسح الأخطاء أثناء كتابة المستخدم
- 6التحقّق في HTML: الطبقة الأولى
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
يحتاج Pocket إلى طريقة لإضافة المصاريف غير زر “عيّنة”: نموذج (form) فيه تسمية ومبلغ وفئة. تبدو النماذج بسيطة، لكنها المكان الذي يعيش فيه كثير من الأخطاء الحقيقية، لأنها تقف على الحدّ الفاصل بين البشر، الذين يكتبون أي شيء، وكودك، الذي يتوقّع بيانات نظيفة.
النموذج وحدث submit
ابدأ بـ HTML سليم. لكل حقل <label>، والزر زر إرسال داخل <form>:
<form id="expense-form" novalidate>
<label for="label">What</label>
<input id="label" name="label" required>
<label for="amount">Amount ($)</label>
<input id="amount" name="amount" type="number" step="0.01" min="0.01" required>
<label for="category">Category</label>
<select id="category" name="category">
<option value="food">Food</option>
<option value="travel">Travel</option>
<option value="fun">Fun</option>
</select>
<p id="form-error" class="error" role="alert"></p>
<button type="submit">Add expense</button>
</form>يمكن إرسال النموذج بالنقر على الزر، أو بالضغط على Enter داخل حقل، أو عبر التقنيات المساعدة. وكل ذلك يُطلق حدثًا واحدًا على النموذج: submit. استمع إليه، لا إلى click على الزر:
const form = document.querySelector("#expense-form");
form.addEventListener("submit", (event) => {
event.preventDefault();
// read, validate, add, render
});ردّ الفعل الافتراضي للمتصفّح على الإرسال هو أن يبعث البيانات إلى خادم ويحمّل صفحة جديدة. ومع تطبيق يعمل داخل الصفحة، تمسح إعادة التحميل هذه كل شيء. وevent.preventDefault() تلغي السلوك الافتراضي وتُبقيك في الصفحة.
قراءة القيم
يمكنك قراءة value لكل حقل، أو جمعها كلها مرة واحدة باستخدام FormData، التي تعتمد على سمة name لكل حقل:
const data = new FormData(form);
const rawLabel = data.get("label"); // " Coffee with Sam "
const rawAmount = data.get("amount"); // "4.50": a string!
const category = data.get("category"); // "food"كل قيمة نص، حتى لو جاءت من حقل أرقام. هذه مشكلة “المدخلات المكتوبة” من القسم 1، لكنها الآن حقيقية. نظّف القيم فورًا: شذّب التسمية بـ trim()، وحوّل المبلغ مرة واحدة، إلى سنتات:
const label = rawLabel.trim();
const amount = Math.round(Number(rawAmount) * 100);التحقّق في دالة نقية
ضع القواعد في دالة واحدة تأخذ القيم وتُرجع ما فيها من خطأ. إنها لا تمسّ الصفحة، لذلك يسهل قراءتها ويسهل اختبارها:
function validateExpense({ label, amount }) {
const errors = {};
if (label.trim() === "") {
errors.label = "Enter what you spent money on.";
}
if (!Number.isFinite(amount) || amount <= 0) {
errors.amount = "Enter an amount greater than zero.";
}
return errors;
}
console.log(validateExpense({ label: " ", amount: Number("") }));
console.log(validateExpense({ label: "Coffee", amount: 4.5 }));الكائن الفارغ يعني “صالح”، وتفحص ذلك بـ Object.keys(errors).length === 0. لاحظ أن Number("") تساوي 0، وهو فخّ المدخلات الفارغة من القسم 1، لذلك تفشل حالة “لم يُكتب شيء” في قاعدة amount <= 0 كما ينبغي، وNumber.isFinite ترفض NaN وInfinity في فحص واحد.
جمع كل شيء معًا
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
const values = {
label: data.get("label").trim(),
amount: Math.round(Number(data.get("amount")) * 100),
category: data.get("category"),
};
const errors = validateExpense(values);
const errorEl = document.querySelector("#form-error");
if (Object.keys(errors).length > 0) {
errorEl.textContent = Object.values(errors).join(" ");
const firstBad = form.elements[Object.keys(errors)[0]];
firstBad.setAttribute("aria-invalid", "true");
firstBad.focus();
return;
}
errorEl.textContent = "";
expenses = [...expenses, { id: nextId(), ...values }];
renderExpenses();
form.reset();
form.elements.label.focus();
});عند الفشل، تذهب الرسالة إلى عنصر يحمل role="alert"، تعلن عنه قارئات الشاشة بمجرّد أن يتغيّر نصّه؛ ويُعلَّم أول حقل غير صالح بـ aria-invalid وينتقل إليه التركيز، فيستطيع المستخدم إصلاحه فورًا. وعند النجاح، تتغيّر البيانات، ويُعاد عرض القائمة، وتُفرغ form.reset() الحقول، ويعود التركيز إلى الحقل الأول، جاهزًا للمصروف التالي. والتطبيق الحقيقي سيُزيل أيضًا aria-invalid بمجرّد إصلاح الحقل. أما form.elements.label فتبحث عن حقل باسمه name.
مسح الأخطاء أثناء كتابة المستخدم
عرض الخطأ نصف المهمة؛ والنصف الآخر إزالته في اللحظة المناسبة. فإن بقيت الرسالة على الشاشة والمستخدم يكتب الإصلاح بالفعل، شعر أن النموذج يوبّخه. استمع إلى أحداث input على النموذج (فهي تنتشر للأعلى، لذلك يغطّي مستمع واحد كل الحقول) وامسح حالة الخطأ في الحقل الذي تغيّر:
form.addEventListener("input", (event) => {
event.target.removeAttribute("aria-invalid");
document.querySelector("#form-error").textContent = "";
});وقاوِم الرغبة المعاكسة، أي التحقّق مع كل ضغطة مفتاح وعرض “Enter an amount” بينما الشخص في منتصف كتابته. والخيار الافتراضي الجيد: تحقّق عند الإرسال، وامسح عند الإدخال.
التحقّق في HTML: الطبقة الأولى
السمات required وtype="number" وmin وstep تتيح للمتصفّح فحص القواعد الأساسية وعرض رسائله الخاصة. والسمة novalidate على النموذج أعلاه توقف تلك الرسائل كي تُعرض رسائلك أنت بشكل متّسق، بينما تظل السمات توثّق النية وتعطي الهواتف لوحة المفاتيح المناسبة. ويمكنك أيضًا أن تسأل المتصفّح مباشرة عبر form.checkValidity().
صار نموذجك يضيف مصاريف حقيقية. لكن أعِد تحميل الصفحة وستختفي، لأنها لم تعش قط إلا في متغيّر. الدرس التالي يُصلح ذلك باستخدام JSON وlocalStorage.
أهم النقاط
- استمع إلى
submitعلى النموذج، لا إلىclickعلى الزر، كي تمرّ كل طرق الإرسال (الزر، ومفتاح Enter، والتقنيات المساعدة) عبر كودك، واستدعِevent.preventDefault()لتمنع إعادة تحميل الصفحة. - كل قيمة تُقرأ من نموذج هي نص؛ شذّب النصوص وحوّل الأرقام قبل استخدامها.
- أبقِ التحقّق من المدخلات في دالة نقية تأخذ القيم وتُرجع الأخطاء، كي تكون القواعد سهلة القراءة والاختبار.
- سمات HTML مثل
requiredوminطبقة أولى مفيدة، لكن يجب أن يتحقّق كود JavaScript أيضًا، وأن يتحقّق الخادم كذلك. - اعرض الأخطاء بجوار الحقل، وعلّمه بـ
aria-invalid، وانقل التركيز إليه كي ينتبه مستخدمو لوحة المفاتيح وقارئات الشاشة.