073: Events II: Delegation, Default Actions, and Listener Lifecycle
Outcomes
By the end of this lesson, you can:
- choose between
input,change, andsubmitevents; - cancel a form's default navigation only when JavaScript handles submission;
- read named form controls with
FormData; - use
SubmitEvent.submitterwhen multiple submit buttons exist; - handle keyboard events by
keyonly for genuine keyboard-specific behavior; and - use bubbling to handle changing Todo controls.
Retrieval Warm-Up
- What is the difference between
event.targetandevent.currentTarget? - Why should listeners normally be registered outside
render()? - How does event delegation support controls created later?
Terms
- Default action: Built-in browser behavior for an event, cancellable via preventDefault(). — Source: MDN: Event.preventDefault()
preventDefault(): Cancels the event’s default action without stopping propagation. — Source: MDN: Event.preventDefault()inputevent: Fires immediately as editable control values change. — Source: MDN: input eventchangeevent: Fires when a control value is committed (blur/select). — Source: MDN: change eventsubmitevent: Fires when submission is requested; preventDefault() stops navigation. — Source: MDN: submit eventFormData: Interface collecting name/value entries for submissions/fetch bodies. — Source: MDN: FormDataSubmitEvent.submitter: Button that triggered submission, readable inside submit handlers. — Source: MDN: SubmitEvent.submitterKeyboardEvent.key: Logical key name for keyboard events ("Enter", "a"). — Source: MDN: KeyboardEvent.key- Propagation: Full event path: capture phase → target → bubble phase. — Source: WHATWG DOM: Dispatching events
- Default action (official): "The browser’s built-in behavior for an event, which can be prevented with preventDefault()." — Source: MDN: Event.preventDefault()
- Propagation (official): "Propagation is the process by which an event travels through capture, target, and bubble phases." — Source: WHATWG DOM: Events
Mental Model: Listen for Meaning, Not Hardware
Choose the event that matches the user's intention:
- Live character count or preview:
input. - Checkbox, radio, or committed selection:
change. - User wants to submit a form, including pressing Enter:
submiton the form. - Escape closes a temporary mode:
keydownandevent.key === "Escape".
Do not listen only for a submit button's click. Forms can submit from a keyboard, assistive technology, or script. Listening to the form's submit event covers the form behavior.
preventDefault() is not a standard first line in every event handler. Use it when replacing a browser default with working JavaScript behavior. It does not stop propagation; those are separate concepts.
Self-Study Example: Live Task Form
Create this complete page:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Todo form events</title>
<script src="app.js" defer></script>
</head>
<body>
<main>
<h1>My tasks</h1>
<form id="task-form">
<div>
<label for="task-title">Task</label>
<input
id="task-title"
name="title"
required
maxlength="80"
aria-describedby="title-help title-count">
<p id="title-help">Enter a short action, not private information.</p>
<p id="title-count">0 of 80 characters</p>
</div>
<div>
<label for="task-priority">Priority</label>
<select id="task-priority" name="priority">
<option value="normal">Normal</option>
<option value="high">High</option>
</select>
</div>
<button type="submit">Add task</button>
</form>
<p id="status" role="status"></p>
<ul id="task-list"></ul>
</main>
</body>
</html>
Add app.js:
const state = {
tasks: [],
};
const form = document.querySelector("#task-form");
const titleInput = document.querySelector("#task-title");
const titleCount = document.querySelector("#title-count");
const taskList = document.querySelector("#task-list");
const status = document.querySelector("#status");
function createTaskItem(task) {
const item = document.createElement("li");
item.dataset.taskId = task.id;
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = `task-${task.id}`;
checkbox.checked = task.completed;
checkbox.dataset.action = "toggle";
const label = document.createElement("label");
label.htmlFor = checkbox.id;
label.textContent = `${task.title} (${task.priority} priority)`;
item.append(checkbox, label);
return item;
}
function render() {
taskList.replaceChildren(...state.tasks.map(createTaskItem));
}
titleInput.addEventListener("input", () => {
titleCount.textContent = `${titleInput.value.length} of ${titleInput.maxLength} characters`;
});
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
const title = String(data.get("title") ?? "").trim();
const priority = data.get("priority") === "high" ? "high" : "normal";
if (title === "") {
status.textContent = "Enter a task before submitting.";
titleInput.focus();
return;
}
state.tasks.push({
id: crypto.randomUUID(),
title,
priority,
completed: false,
});
form.reset();
titleCount.textContent = `0 of ${titleInput.maxLength} characters`;
status.textContent = `Added task: ${title}`;
render();
titleInput.focus();
});
taskList.addEventListener("change", (event) => {
const checkbox = event.target;
if (!(checkbox instanceof HTMLInputElement)) {
return;
}
if (checkbox.dataset.action !== "toggle") {
return;
}
const item = checkbox.closest("[data-task-id]");
const task = state.tasks.find(
(task) => task.id === item?.dataset.taskId,
);
if (!task) {
return;
}
task.completed = checkbox.checked;
status.textContent = `${task.title} marked ${task.completed ? "complete" : "not complete"}.`;
render();
});
render();
Trace each event:
- Every user edit fires
input; the character count updates immediately. - Clicking Add or pressing Enter requests form submission.
- Native
requiredandmaxlengthconstraints apply before a normal submit event. 074 explores validation deeply. - The handler cancels navigation because this local app handles the data in JavaScript.
new FormData(form)reads controls that havenameattributes.data.get()can return a string, file, ornull, so title normalization is explicit.- State updates before
render(). form.reset()restores initial control values. Resetting programmatically does not make yourinputhandler run, so the count is synchronized explicitly.- A checkbox's
changeevent reports a committed checked state. Assigningtask.completed = checkbox.checkedis more robust than merely inverting an old boolean.
input Versus change
For text controls, input fires on each user edit while change generally fires when the edit is committed, often after focus leaves. For checkboxes and radios, change fires when checkedness changes. For selects, it fires when the selection is committed.
Programmatically assigning .value or .checked does not automatically dispatch these user interaction events. Your own code already knows it changed state and should call render() directly rather than synthesizing an event merely to trigger internal logic.
Form Submission and Default Behavior
Without JavaScript cancellation, a form submission follows its action and method, often navigating or reloading. That behavior is valuable progressive enhancement when a real server endpoint exists. In this browser-only exercise, there is no endpoint, so the handler calls preventDefault().
Do not use return false with addEventListener(); its return value is ignored. Call event.preventDefault() explicitly. You can inspect event.cancelable and event.defaultPrevented while debugging.
Prefer a submit button with type="submit". Buttons elsewhere should usually declare type="button", because the default type inside a form is submit.
Intermediate Example: Multiple Submit Intentions
A form may have "Add" and "Add another high priority" submit buttons:
<button type="submit" name="intent" value="add">Add task</button>
<button type="submit" name="intent" value="add-high">Add as high priority</button>
Use the modern submit event's submitter:
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form, event.submitter);
const intent = event.submitter?.value;
const priority = intent === "add-high" ? "high" : data.get("priority");
console.log(priority);
});
new FormData(form, event.submitter) includes the initiating submit button where supported by the current HTML API. More importantly, do not infer intent from whichever element currently has focus.
Optional Advanced Example: Escape to Clear
Keyboard events are appropriate for explicitly keyboard-oriented behavior. Let Escape clear a draft without hijacking ordinary typing:
titleInput.addEventListener("keydown", (event) => {
if (event.key !== "Escape" || titleInput.value === "") {
return;
}
titleInput.value = "";
titleCount.textContent = `0 of ${titleInput.maxLength} characters`;
status.textContent = "Draft task cleared.";
});
No preventDefault() is necessary here because Escape has no relevant default action in this input. Use event.key for meaning such as Escape or Enter. event.code represents a physical key position and is a different tool, often wrong for international keyboard layouts.
Never submit on arbitrary keypress; that event is legacy/deprecated. Let the form's native submission behavior produce submit.
Bubbling in Practice
Most events used here bubble, enabling the list's delegated change listener. focus and blur do not bubble in the ordinary way; focusin and focusout do. mouseenter does not bubble; mouseover does. Check official event documentation rather than guessing.
Propagation and default action are independent:
preventDefault()cancels an allowed default action.stopPropagation()stops travel to other nodes.stopImmediatePropagation()also blocks later listeners on the current node.
Use propagation-stopping methods rarely because they can create hidden coupling.
Mistakes and Debugging
- Listening to button click instead of form submit: pressing Enter may bypass your logic. Listen on the form.
- Calling
preventDefault()everywhere: it can break links, scrolling, and controls. Cancel only the behavior you replace. - Using
keypressor numeric key codes: usekeydown/keyupandevent.keyfor keyboard-specific features. - Using
inputfor every announcement: a live region that speaks each character count can become noisy. Describing a visible count is often enough; test with users. - Forgetting
name:FormDataomits unnamed controls. - Assuming
FormData.get()is always a string: normalize and validate the expected type/value. - Inverting checkbox state: use
checkbox.checked, especially when state could be restored or synchronized. - Forgetting button type: a secondary button inside a form may unexpectedly submit.
Debug with the Network panel to detect accidental navigation, log event.type, inspect defaultPrevented, and inspect Array.from(new FormData(form).entries()). Confirm the correct handler runs for click, Enter, keyboard checkbox activation, and pointer input.
Accessibility, Security, and Performance
Accessibility: labels must be visibly and programmatically associated. Forms provide familiar keyboard behavior; do not replace it. Move focus only intentionally: returning focus to the task input after adding supports rapid entry, while invalid submission should focus the first field needing attention. role="status" announces concise results without moving focus. Avoid character-only shortcuts and do not make completion depend on pointer input.
Security: client input is untrusted even after validation. FormData does not sanitize values. Use textContent when rendering titles and validate again on a server before storing or acting on submitted data. Do not include passwords, tokens, or private notes in console logs or localStorage.
Performance: input can fire frequently. Keep its handler small; avoid full-app renders for a character count. Submit and change handlers perform one state transition and one render. Delegation keeps dynamic checkbox handling stable.
Exercises
Core
Add an optional notes field with maxlength="120" and a live visible count updated by input.
Practice
Add a Delete button to each task and handle clicks through one delegated list listener while keeping checkbox changes on change.
Professional Extension
Add two submit buttons, "Add" and "Add and keep priority." Use event.submitter so the second preserves the priority selection after submission.
Core
<label for="task-notes">Notes (optional)</label>
<textarea id="task-notes" name="notes" maxlength="120" aria-describedby="notes-count"></textarea>
<p id="notes-count">0 of 120 characters</p>
const notes = document.querySelector("#task-notes");
const notesCount = document.querySelector("#notes-count");
notes.addEventListener("input", () => {
notesCount.textContent = `${notes.value.length} of ${notes.maxLength} characters`;
});
Also read String(data.get("notes") ?? "").trim() into the task object and reset the count after form.reset().
Practice
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.dataset.action = "delete";
deleteButton.textContent = `Delete ${task.title}`;
item.append(" ", deleteButton);
taskList.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) return;
const button = event.target.closest('button[data-action="delete"]');
const item = button?.closest("[data-task-id]");
if (!button || !item || !taskList.contains(item)) return;
const task = state.tasks.find((task) => task.id === item.dataset.taskId);
if (!task) return;
state.tasks = state.tasks.filter((item) => item.id !== task.id);
status.textContent = `Deleted task: ${task.title}`;
render();
});
Professional Extension
<button type="submit" value="reset-priority">Add</button>
<button type="submit" value="keep-priority">Add and keep priority</button>
const keepPriority = event.submitter?.value === "keep-priority";
const selectedPriority = String(data.get("priority") ?? "normal");
// Add the task, then:
form.reset();
if (keepPriority) {
form.elements.priority.value = selectedPriority;
}
Recap
- Use
inputfor immediate edits,changefor committed control changes, andsubmitfor form submission. - Listen to form behavior rather than one way of activating a button.
preventDefault()cancels a default action; it does not stop bubbling.FormDatareads successful named controls; values still require normalization and validation.event.submitteridentifies submission intent.- Use keyboard events only for behavior that is genuinely keyboard-specific.
