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
Before adding another event listener, retrieve the three distinctions that matter most here:
- 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
These terms give you precise names for the browser behavior you will be working with:
- 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 an event based on the user's intent, not merely on the physical action that happened to produce it:
- 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".
The useful distinction is between an event that describes the behavior you care about and an event that describes one possible way to trigger it. Do not listen only for a submit button's click. A form can be submitted from the keyboard, through assistive technology, or by script. Listening for submit on the form covers the form-level behavior instead of one activation path.
preventDefault() is also not a mandatory first line in every handler. Use it when your JavaScript is replacing a browser default with behavior that actually works. It does not stop propagation; cancellation of the default action and travel through the DOM are separate concepts.
Self-Study Example: Live Task Form
Start with a complete page for entering tasks. The markup supplies labels, names for submission, native constraints, and a status region, so the JavaScript can build on the browser's existing form behavior rather than replacing it unnecessarily.
<!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 the behavior in order:
- Each user edit fires
input, so the character count changes immediately. - Clicking Add or pressing Enter requests form submission. These are different activation methods for the same form behavior.
- Native
requiredandmaxlengthconstraints are applied before a normalsubmitevent. 074 explores validation deeply. - This handler cancels navigation because the local application processes the data in JavaScript instead of sending it to a server.
new FormData(form)reads controls withnameattributes.data.get()can produce a string, a file, ornull, so the title is normalized explicitly.- The state changes before
render()rebuilds the list. form.reset()restores the controls' initial values. Programmatic reset does not cause yourinputhandler to run, so the count is synchronized explicitly.- A checkbox's
changeevent reports its committed checked state. Assigningtask.completed = checkbox.checkedis safer than simply inverting an old boolean, particularly if state was restored or synchronized elsewhere.
input Versus change
For text controls, input fires for each user edit. change generally waits until that edit is committed, often when focus leaves the control. For checkboxes and radios, change fires when checkedness changes; for selects, it fires when the selection is committed.
There is a related edge case: assigning .value or .checked in JavaScript does not automatically dispatch these user-interaction events. Your code already knows that it changed the state, so call render() or update the relevant display directly. Do not synthesize an event just to make internal logic run.
Form Submission and Default Behavior
If JavaScript does not cancel submission, the browser follows the form's action and method. That often means navigating or reloading. This is useful progressive enhancement when a real server endpoint exists. In this browser-only exercise there is no endpoint to handle the request, so the submit handler calls preventDefault() before processing the form locally.
With addEventListener(), return false does not cancel anything; the listener's return value is ignored. Call event.preventDefault() explicitly. While debugging, event.cancelable tells you whether cancellation is allowed and event.defaultPrevented tells you whether it has already happened.
Use type="submit" for the button that submits a form. Buttons serving another purpose should usually declare type="button", because an untyped button inside a form defaults to submit.
Intermediate Example: Multiple Submit Intentions
A form can expose more than one submission intention, such as "Add" and "Add another high priority":
<button type="submit" name="intent" value="add">Add task</button>
<button type="submit" name="intent" value="add-high">Add as high priority</button>
Handle that distinction from the submit event itself with its submitter property:
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);
});
Where the current HTML API supports it, new FormData(form, event.submitter) includes the button that initiated submission. The more general lesson is not to infer intent from whichever element currently has focus. Focus can change independently of which submit control initiated the request.
Optional Advanced Example: Escape to Clear
Keyboard events are appropriate when the behavior is explicitly keyboard-oriented. Here Escape clears a draft, while ordinary typing continues to behave normally:
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.";
});
There is no need for preventDefault() here because Escape has no relevant default action in this input. Use event.key for semantic keys such as Escape or Enter. event.code describes the physical key position, so it is a different tool and is often the wrong choice for international keyboard layouts.
Do not submit on arbitrary keypress; that event is legacy/deprecated. Let the form's native submission behavior produce the submit event.
Bubbling in Practice
The events used in this example mostly bubble, which is why one change listener on the list can handle checkboxes that render() creates later. The listener belongs to the stable list element, while event.target identifies the particular checkbox that changed.
Not every event bubbles in the same way. focus and blur do not bubble ordinarily; focusin and focusout do. mouseenter does not bubble, while mouseover does. Verify the event's official documentation instead of relying on a remembered rule.
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 sparingly. They can hide the event from unrelated code and create coupling that is difficult to discover later.
Mistakes and Debugging
- Listening to button click instead of form submit: pressing Enter may bypass the handler. Listen on the form.
- Calling
preventDefault()everywhere: it can break links, scrolling, and controls. Cancel only the behavior your code replaces. - Using
keypressor numeric key codes: usekeydown/keyupandevent.keyfor keyboard-specific features. - Using
inputfor every announcement: a live region that speaks every character count can become noisy. A visible count is often enough; test the experience with users. - Forgetting
name:FormDataomits unnamed controls. - Assuming
FormData.get()is always a string: normalize and validate the type and value you expect. - Inverting checkbox state: use
checkbox.checked, especially when state may be restored or synchronized. - Forgetting button type: a secondary button inside a form may unexpectedly submit.
When debugging, use the Network panel to detect accidental navigation. Log event.type, inspect defaultPrevented, and inspect Array.from(new FormData(form).entries()). Exercise each activation path: click the submit button, press Enter, activate a checkbox with the keyboard, and use pointer input. If one path behaves differently, the event you chose may describe only one hardware interaction rather than the underlying behavior.
Accessibility, Security, and Performance
Accessibility: Labels must be visibly and programmatically associated with their controls. Forms already provide familiar keyboard behavior, so do not replace it with pointer-only interactions. Move focus intentionally: returning focus to the task input after a successful add supports rapid entry, while an invalid submission should focus the first field that needs attention. role="status" can announce concise results without moving focus. Avoid character-only shortcuts, and do not make completion depend on pointer input.
Security: Client input remains untrusted even after browser validation. FormData does not sanitize values. This example uses textContent when rendering titles, and a real application must validate again on the server before storing or acting on submitted data. Do not put passwords, tokens, or private notes in console logs or localStorage.
Performance: input may fire frequently, so keep its handler small and avoid a full-application render just to update a character count. The submit and change handlers each perform one state transition and one render. Delegation keeps checkbox handling stable as the list changes, rather than requiring a new listener for every checkbox after each render.
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. Handle its clicks through one delegated listener on the list, while continuing to handle checkbox changes with change.
Professional Extension
Add two submit buttons, "Add" and "Add and keep priority." Use event.submitter so the second button preserves the selected priority 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 for the form behavior rather than for only one way of activating a button.
preventDefault()cancels a default action; it does not stop bubbling.FormDatareads successful named controls, but its values still need normalization and validation.event.submitteridentifies the submission intent.- Use keyboard events only for behavior that is genuinely keyboard-specific.
