در این درس میخوانید
رابط وب باید به کلیک، تایپ و ارسال فرم پاسخ دهد. رویداد یا event خبر میدهد که اتفاقی افتاده و listener کدی است که برای پاسخ ثبت میکنیم. در درس یازدهم، یک فرم افزودن عنوان درس میسازیم. مثال از دکمه و فرم استاندارد استفاده میکند تا کاربر صفحهکلید نیز بتواند آن را اجرا کند؛ ساختن یک div قابل کلیک، بهتنهایی رفتار یک دکمهٔ واقعی را فراهم نمیکند.
HTML فرم را بسازید
ساختار زیر را در بخش body فایل HTML قرار دهید و app.js را در head با defer بارگذاری کنید. label به input متصل است، طول عنوان محدود شده و پیام وضعیت یک ناحیهٔ aria-live دارد. اعتبارسنجی داخلی مرورگر نقطهٔ شروع خوبی است، اما قواعد واقعی داده و مجوزهای امنیتی باید در سرور نیز بررسی شوند؛ کد مرورگر تحت کنترل کاربر است.
<form id="lesson-form">
<label for="lesson-title">عنوان درس</label>
<input id="lesson-title" name="title" required minlength="2" maxlength="100">
<button type="submit">افزودن درس</button>
</form>
<p id="message" role="status" aria-live="polite"></p>
<ul id="lessons"></ul>رویداد submit را مدیریت کنید
رویداد submit را روی خود فرم میگیریم، نه فقط click روی دکمه؛ به این ترتیب ارسال با Enter هم پوشش داده میشود. preventDefault رفتار پیشفرض همان رویداد، مانند ناوبری ارسال فرم، را متوقف میکند. این دستور جلوی انتشار رویداد را نمیگیرد و با stopPropagation یکسان نیست. ابتدا وجود عنصرها را بررسی میکنیم، سپس listener را یکبار ثبت میکنیم؛ ثبت دوبارهٔ آن در هر render میتواند چند بار اجرای یک عمل را ایجاد کند.
const form = document.querySelector("#lesson-form");
const input = document.querySelector("#lesson-title");
const list = document.querySelector("#lessons");
const message = document.querySelector("#message");
if (!form || !input || !list || !message) throw new Error("Form elements missing");
form.addEventListener("submit", event => {
event.preventDefault();
const title = input.value.trim();
if (title.length < 2 || title.length > 100) {
message.textContent = "عنوان باید بین ۲ تا ۱۰۰ نویسه باشد";
input.focus();
return;
}
const li = document.createElement("li");
const label = document.createElement("span");
label.textContent = title;
const remove = document.createElement("button");
remove.type = "button";
remove.dataset.action = "remove";
remove.textContent = "حذف";
remove.setAttribute("aria-label", `حذف درس ${title}`);
li.append(label, " ", remove);
list.append(li);
form.reset();
input.focus();
message.textContent = "درس به فهرست اضافه شد";
});این مثال به محیط مرورگر، پروژه یا Node.js نیاز دارد؛ طبق راهنمای درس اجرا کنید.یک عنوان دوحرفی یا بیشتر وارد کنید و هم با دکمه و هم با Enter فرم را بفرستید. هر بار باید فقط یک عضو ساخته شود. trim فاصلههای ابتدا و انتها را برای بررسی منطقی کنار میگذارد. داده با textContent درج شده است. form.reset ورودیها را به مقدار اولیه برمیگرداند؛ دادهٔ ذخیرهشدهٔ برنامه را پاک نمیکند. در این نمونهٔ کوتاه، فهرست در DOM نگه داشته میشود؛ پروژهٔ پایانی مدل داده را از نمایش جدا میکند.
Event Delegation برای دکمههای تازه
رویداد بسیاری از عنصرها در مرحلهٔ bubbling به والدها میرسد. میتوان یک listener روی ظرف گذاشت و تشخیص داد رویداد از کدام دکمه آمده است. target نقطهٔ آغاز رویداد است و currentTarget عنصری است که listener روی آن ثبت شده. با closest، حتی اگر داخل دکمه یک آیکون کلیک شود، دکمهٔ مربوط پیدا میشود. همهٔ رویدادها bubbling یکسان ندارند؛ این الگو را برای click این مثال به کار میبریم.
list.addEventListener("click", event => {
const button = event.target instanceof Element
? event.target.closest("button[data-action=remove]")
: null;
if (!button || !list.contains(button)) return;
button.closest("li")?.remove();
input.focus();
message.textContent = "درس از فهرست برداشته شد";
});این listener را پس از کد قبلی در همان فایل بگذارید. اکنون عضوهای تازه نیز دکمهٔ حذف فعال دارند، بدون اینکه برای هرکدام جدا listener بسازیم. بررسی contains مانع انتخاب یک دکمهٔ بیرون ظرفِ مورد انتظار میشود. نام dataset فقط یک نشانهٔ رابط است و مجوز امنیتی ایجاد نمیکند؛ اگر حذف قرار بود در سرور انجام شود، شناسه و اجازهٔ کاربر در آن سمت دوباره بررسی میشد.
اعتبارسنجی را با بازخورد روشن همراه کنید
پیام خطا باید بگوید چه چیزی قابل اصلاح است. عبارت کلی «خطا» برای عنوان کوتاه، راهنمای کافی نیست. محدودیت طول، خالینبودن و قرارداد داده را در یک مسیر روشن بررسی کنید. برای ورودی خاص میتوان از setCustomValidity و reportValidity استفاده کرد، اما خطای قبلی باید هنگام اصلاح ورودی پاک شود. اعتبارسنجی سمت کاربر برای تجربهٔ استفاده است و جای اعتبارسنجی سمت سرور را نمیگیرد.
تمرین: ارسال تکراری را بررسی کنید
فرم را سه بار با Enter ارسال کنید؛ باید سه عضو داشته باشید، نه شش عضو. سپس دو عضو را حذف کنید. برای توسعهٔ تمرین، پیش از درج، عنوان تکراری را رد کنید. در این پاسخ ساده، عنوانهای موجود از spanهای فهرست خوانده میشوند و مقایسه پس از trim انجام میشود؛ در برنامهٔ بزرگتر، این قاعده باید روی مدل داده پیاده شود.
// داخل submit، پس از اعتبارسنجی و پیش از ساخت li اضافه کنید:
const existingTitles = Array.from(list.querySelectorAll("li > span"))
.map(element => element.textContent?.trim());
if (existingTitles.includes(title)) {
message.textContent = "این عنوان قبلاً اضافه شده است";
return;
}اشتباههای رایج
- گرفتن click دکمه و فراموشکردن ارسال با Enter.
- نوشتن دکمهٔ حذف بدون type و ارسال ناخواستهٔ فرم.
- اشتباهگرفتن preventDefault با stopPropagation.
- ثبت چندبارهٔ listener هنگام بهروزرسانی رابط.
- اعتماد به اعتبارسنجی مرورگر برای مجوز و امنیت سرور.
منابع انگلیسی
- MDN — EventTarget.addEventListener
- https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
- MDN — Constraint validation
- https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Constraint_validation
- MDN — Event bubbling
- https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling
فرم و تمرین این درس برای درسآوند طراحی شدهاند. در ادامه، کد را به ماژولهای مستقل تقسیم میکنیم تا نگهداری و آزمون سادهتر شود.
پیشنیاز و محیط اجرای مثالها
درس 11 از ۲۰؛ پیشنیاز: درسهای قبلی مجموعه. در مرورگر: index.html و app.js بسازید. قطعههای JavaScript را به ترتیب در app.js بگذارید و فایل را با defer بارگذاری کنید.
پیشرفت روی همین مرورگر ذخیره میشود.