۱۰. فرم قابل استفاده؛ اعتبارسنجی، ارسال و بازیابی از خطا
فرم خوب فقط ورودی معتبر تولید نمیکند؛ به کاربر نشان میدهد چه چیزی اشتباه است، چطور آن را اصلاح کند و آیا عمل واقعاً انجام شده است. در این درس، از «گرفتن مقدار input» عبور میکنیم و قرارداد ارسال را طراحی میکنیم. مثال ما ثبت هزینه است، اما همین اصول در ساخت تیکت، ویرایش پروفایل و فرم سفارش کاربرد دارند.
از HTML معنادار شروع کنید
label، نوع درست دکمه و پیام توضیحی را قبل از کدنویسی مشخص کنید. به placeholder بهعنوان تنها نام ورودی تکیه نکنید؛ با تایپ کاربر ناپدید میشود. برای مبلغ فارسی، inputmode="numeric" فقط راهنمای صفحهکلید است و اعتبارسنجی محسوب نمیشود. همچنین input نوع number الزاماً تجربهٔ مناسبی برای رقم فارسی یا قوانین دقیق مبلغ نمیسازد.
مثال زیر را در یک فایل HTML مستقل بگذارید و با سرور آزمایشگاه باز کنید. required و minlength خطای ساده را در مرورگر پوشش میدهند. برای مبلغ از قانون برنامه استفاده میکنیم؛ در این نمونه فقط عدد لاتین بدون جداکننده پذیرفته میشود تا تفاوت مسئولیتها روشن بماند. پروژهٔ همراه parseAmount را برای پذیرش رقم فارسی نیز دارد.
<form id="demo-expense">
<label>عنوان<input name="title" required minlength="2"></label>
<label>مبلغ به تومان
<input name="amount" required inputmode="numeric"
aria-describedby="amount-help amount-error">
</label>
<small id="amount-help">عدد صحیح مثبت بدون جداکننده</small>
<p id="amount-error"></p>
<button type="submit">ثبت</button>
<p id="form-status" role="status" aria-live="polite"></p>
</form>مثال اول؛ خطا نزدیک ورودی، فوکوس روی محل اصلاح
کد زیر را در انتهای همان فایل، داخل script قرار دهید. برای نمونهٔ کاملاً مستقل import لازم نیست. رویداد submit بهجای کلیک دکمه انتخاب شده تا Enter در ورودی هم همان مسیر را طی کند. پیش از هر تلاش، خطای سفارشی قبلی پاک میشود؛ خطای قدیمی نباید پس از اصلاح ورودی باقی بماند.
const demoForm = document.querySelector('#demo-expense');
const amountField = demoForm.elements.amount;
const amountError = document.querySelector('#amount-error');
const status = document.querySelector('#form-status');
amountField.addEventListener('input', () => {
amountField.setCustomValidity('');
amountField.removeAttribute('aria-invalid');
amountError.textContent = '';
});
demoForm.addEventListener('submit', event => {
event.preventDefault();
amountField.setCustomValidity('');
const raw = amountField.value.trim();
const amount = Number(raw);
const valid = /^\d+$/.test(raw)
&& Number.isSafeInteger(amount) && amount > 0 && amount <= 1e12;
if (!valid) {
const message = 'مبلغ را بهصورت عدد صحیح مثبت وارد کنید.';
amountField.setCustomValidity(message);
amountField.setAttribute('aria-invalid', 'true');
amountError.textContent = message;
amountField.reportValidity();
amountField.focus();
return;
}
status.textContent = 'اعتبارسنجی موفق؛ هنوز ذخیرهای انجام نشده است.';
});خروجی مورد انتظار برای «12.5» خطا و برای «12000» پیام موفقیت اعتبارسنجی است. این پیام عمداً ادعای ذخیره ندارد. اعتبارسنجی سمت کاربر تجربه را بهتر میکند، ولی در برنامهٔ متصل به سرور، مرز اعتماد نیست؛ همان قوانین باید روی سرور اجرا شوند. ابزارهای مرورگر و درخواست مستقیم میتوانند فرم را دور بزنند.
مثال دوم؛ جلوگیری از ارسال همزمان و حفظ ورودی
تابع زیر یک الگوی مستقل برای عمل غیرهمزمان است. آن را در همان script تعریف کنید و بهجای پیام پایانی نمونهٔ اول فراخوانی کنید. action باید Promise برگرداند. تابع آزمایشی هیچ شبکه یا فایل واقعی را تغییر نمیدهد؛ هدف دیدن شکست و تلاش دوباره است.
let submitting = false;
async function submitOnce(form, action) {
if (submitting) return;
submitting = true;
const button = form.querySelector('button[type="submit"]');
button.disabled = true;
form.setAttribute('aria-busy', 'true');
status.textContent = 'در حال ثبت…';
try {
await action(Object.fromEntries(new FormData(form)));
status.textContent = 'عمل آزمایشی موفق بود.';
form.reset();
form.elements.title.focus();
} catch {
status.textContent = 'ثبت نشد؛ ورودیها باقی ماندهاند. دوباره تلاش کنید.';
} finally {
submitting = false;
button.disabled = false;
form.removeAttribute('aria-busy');
}
}
// آزمایش شکست از Console همان صفحه:
// submitOnce(demoForm, async () => { throw Error('demo failure'); });finally برای بازگرداندن کنترلهاست، نه اعلام موفقیت. در شکست، reset نمیکنیم تا متن کاربر از دست نرود. متغیر submitting جلوی دو اجرای همزمان در همین رابط را میگیرد؛ جلوگیری از تکرار عملیات روی سرور مسئلهٔ دیگری است. اگر اتصال بعد از ثبت قطع شود، کاربر ممکن است نداند ثبت انجام شده؛ طراحی idempotency و شناسهٔ عملیات در سرویس واقعی لازم است.
تمرین مستقل و پاسخ تشریحی
محدودیت طول عنوان را ۲ تا ۱۰۰ نویسه کنید و آزمایش کنید که خطای مبلغ و عنوان همزمان چگونه قابل تشخیصاند. بهعنوان چالش، پس از شکست، پیام را به اولین ورودی نامعتبر مرتبط کنید؛ رنگ قرمز بهتنهایی کافی نیست. پاسخ راهنما: از constraintهای HTML برای قانون ساده، setCustomValidity برای قانون برنامه و متن توضیحی قابل مشاهده استفاده کنید. novalidate را صرفاً برای حذف پیام مرورگر اضافه نکنید؛ در آن صورت مسئول ساخت تجربهٔ کامل اعتبارسنجی میشوید.
معیار تحویل: ارسال با Enter کار کند، کلیک سریع عمل را دو بار آغاز نکند، شکست دادهها را پاک نکند، دکمه دوباره فعال شود و مسیر اصلاح با صفحهکلید واضح باشد. رفتار را هم در حالت موفق و هم با action شکستخورده بررسی کنید.
منابع انگلیسی برای مطالعهٔ تکمیلی
https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reportValidity
https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Constraint_validation
https://www.w3.org/WAI/tutorials/forms/notifications/
تألیف و مثالهای مستقل: تحریریه درسآوند.