The previous lesson left the Nómada Tasks page showing real data, but completely mute: a pretty poster you cannot interact with. An application is something else. It is a program that waits and reacts: somebody clicks, types into a field, presses a key, and the program responds. That working model is called event-driven programming, and it is the natural way of programming in the browser. In this lesson you will learn to register handlers with addEventListener, to read the information carried by the Event object, to remove handlers when they are no longer needed, to tell apart the events you will use most, and to avoid the most common trap of all: building controls with <div>s that leave out anyone who does not use a mouse. By the end, Marta will be able to mark a task as done with a click and Lucía will be able to see only her own.
Contents
- What an event is and what event-driven programming is
- The three ways of registering a handler
addEventListenerin depth- The options:
once,passive,capture,signal removeEventListenerand the reference problem- The
Eventobject preventDefaultand default actions- Event reference table
- Keyboard events and shortcuts
thisinside a handler- Accessibility: why a
<div>with aclickis not enough - Nómada Tasks: mark as done and filter by assignee
- Common Mistakes and Tips
- Exercises
- Conclusion
- What an event is and what event-driven programming is
An event is a notification that something has happened. The browser generates it and it can come from a person (a click, a key, a scroll), from the browser itself (the page finished loading, an image failed) or from your code (the custom events of 06-04).
A handler (or listener) is a function you register so that it runs when a specific event happens on a specific element. And here is the conceptual key: you never call that function. You register it and forget about it. It is the browser that calls it.
This fits exactly with the event loop from 05-07. When you register a click handler on a button, nothing happens; the main thread stays free. When somebody clicks, the browser queues your function as a macrotask and the event loop will run it as soon as the call stack is empty.
flowchart LR
A["Marta clicks<br/>a button"] --> B["The browser creates<br/>an Event object"]
B --> C["It queues the handler<br/>in the task queue"]
C --> D{"Call stack<br/>empty?"}
D -- "No" --> D
D -- "Yes" --> E["The event loop<br/>runs your function"]
E --> F["The function modifies<br/>the model and the DOM"]
Out of this comes a practical consequence you already know from Module 5 but which now becomes tangible: if your handler takes a long time, the interface freezes. While your function runs, the thread is busy, and the browser can neither repaint nor attend to other clicks. A handler must do little work and do it fast; if it needs something expensive, that work is split up or deferred (Module 9).
- The three ways of registering a handler
Historically there have been three ways of connecting a function to an event. Only one is acceptable today, but it is worth recognizing the other two because they are everywhere.
Way 1: the HTML attribute onclick.
The browser takes that string and evaluates it as code. It mixes markup with behavior, it forces markDone to be a global function (which breaks with the modules of 05-04, whose scope is not global), it is impossible to maintain and modern content security policies block it outright.
Way 2: the element.onclick property.
// ✗ Better than the previous one, but still limited
const button = document.querySelector('#mark-done');
button.onclick = () => console.log('click');
button.onclick = () => console.log('another click'); // ← it overwrites the previous one!
// Only the second one runs.It is in JavaScript, which is already an improvement, but it allows only one handler per event and element. If your code registers one and a library registers another, one of the two silently disappears.
Way 3: addEventListener.
// ✓ The only correct way
const button = document.querySelector('#mark-done');
button.addEventListener('click', () => console.log('click'));
button.addEventListener('click', () => console.log('another click'));
// BOTH run, in the order they were registered.| Way | Separates markup and logic | Several handlers | Options (once, capture…) |
Works with modules | Verdict |
|---|---|---|---|---|---|
onclick="…" attribute |
No | No | No | No (requires a global function) | Never |
elem.onclick property |
Yes | No | No | Yes | Only in very old code |
addEventListener |
Yes | Yes | Yes | Yes | Always |
addEventListener in depth
addEventListener in depthThe signature is simple and always the same:
type: the name of the event as a string, without theonprefix:'click', not'onclick'.handler: the function that will run. It receives one argument: theEventobject.options: an optional object we will see in the next section.
const button = document.querySelector('#mark-done');
button.addEventListener('click', function (event) {
console.log('A click happened on', event.currentTarget.textContent);
});The most repeated syntax mistake of the whole module is this one:
The parentheses call the function immediately and register whatever it returns (normally undefined) as the handler. What you want is to pass the function without calling it:
And what if you need to pass it arguments? Wrap it in an arrow, as you learned in 03-06:
- The options:
once, passive, capture, signal
once, passive, capture, signalThe third parameter accepts an object with these properties:
| Option | Type | What it does |
|---|---|---|
once |
boolean | The handler runs only once and removes itself |
passive |
boolean | Promises the handler will not call preventDefault, which lets the browser scroll without waiting |
capture |
boolean | Registers the handler in the capture phase instead of the bubbling phase (lesson 06-04) |
signal |
AbortSignal |
Lets you remove the handler by aborting a signal (lesson 06-04) |
once is the most useful of the four day to day, and it replaces a pattern that used to be written by hand:
// Show the help only the first time the form is opened
form.addEventListener('focusin', showHelp, { once: true });
// Before 'once' you had to do this:
function showHelpOnce(event) {
showHelp(event);
event.currentTarget.removeEventListener('focusin', showHelpOnce);
}passive matters in scroll and touch events (scroll, touchstart, wheel). Without it, the browser has to wait for your handler to finish to know whether it is going to cancel the scroll, which feels sluggish. By promising you will not, scrolling stays smooth:
In modern browsers, touchstart, touchmove and wheel on document are already passive by default.
capture and signal belong squarely to the next lesson; they are mentioned here so the table is complete.
removeEventListener and the reference problem
removeEventListener and the reference problemTo remove a handler you use removeEventListener with exactly the same three pieces of information it was registered with: the same type, the same function and the same capture value.
function onClick() { console.log('click'); }
button.addEventListener('click', onClick);
button.removeEventListener('click', onClick); // ✓ removedAnd here is the trap that costs hours:
button.addEventListener('click', () => console.log('click'));
button.removeEventListener('click', () => console.log('click'));
// ✗ It removes NOTHING. And it gives no error.The two arrows have the same code, but they are two different function objects, exactly like {} !== {} in 01-07. removeEventListener compares by identity, finds no match and does nothing, silently.
The same trap shows up with bind, which returns a new function every time (04-02):
button.addEventListener('click', this.onClick.bind(this));
button.removeEventListener('click', this.onClick.bind(this)); // ✗ a different function
// ✓ Store the reference just once
this.boundOnClick = this.onClick.bind(this);
button.addEventListener('click', this.boundOnClick);
button.removeEventListener('click', this.boundOnClick);When you have to remove a handler. If the element is removed from the DOM and nobody else holds a reference to it, the browser collects the element and its handlers: there is nothing to do. But if you registered the handler on window, on document or on an element that is still alive, and you no longer need it, remove it; otherwise your function and everything its closure captures (03-04) stay retained in memory. It is one of the classic memory leaks, and it has a lesson of its own: Memory Management.
- The
Event object
Event objectEvery time an event fires, the browser builds an object with all the information and passes it as the first argument to your handler. These are its essential properties:
| Property | What it holds |
|---|---|
type |
The event type: 'click', 'keydown', 'submit'… |
target |
The element where the event originated (the deepest one) |
currentTarget |
The element you registered the handler on |
timeStamp |
Milliseconds since the document was created |
isTrusted |
true if the user generated it; false if your code fired it |
defaultPrevented |
true if somebody has already called preventDefault() |
The distinction between target and currentTarget is the most important one in the table, and it is best seen with an example. This <li> contains a <button>, and the handler is on the <li>:
<li class="task" data-id="6">
<span class="task__title">Carpentry workshop quote</span>
<button class="task__action">Mark done</button>
</li>const li = document.querySelector('[data-id="6"]');
li.addEventListener('click', (event) => {
console.log('target: ', event.target.tagName);
console.log('currentTarget:', event.currentTarget.tagName);
});
// If you click the BUTTON:
// target: BUTTON ← where it really happened
// currentTarget: LI ← where you are listening
// If you click the TITLE:
// target: SPAN
// currentTarget: LIThat a click on the button triggers the <li>'s handler has an explanation —the event bubbles upward— that is the central topic of the next lesson. For now, hold on to the rule: currentTarget is your element; target is where the user actually clicked.
A warning: currentTarget only has a value during the execution of the handler. If you store the event and read it later (inside a setTimeout, for instance), it will be null.
button.addEventListener('click', (event) => {
const element = event.currentTarget; // ✓ store it now
setTimeout(() => {
console.log(event.currentTarget); // null ✗
console.log(element.textContent); // ✓ works
}, 100);
});On top of that, each type of event contributes its own properties: a MouseEvent brings clientX, clientY and button; a KeyboardEvent brings key, code, ctrlKey and shiftKey; an InputEvent brings data. All of them inherit from Event, in a prototype hierarchy that is the one from 05-01 applied to the browser APIs.
preventDefault and default actions
preventDefault and default actionsMany elements have behavior of their own built into the browser: a link navigates, a form submits and reloads the page, the space bar scrolls the document, the right button opens the context menu. preventDefault() cancels that behavior.
const link = document.querySelector('#help');
link.addEventListener('click', (event) => {
event.preventDefault(); // ✓ the browser does NOT follow the link
showHelpPanel();
});The case you will use most is in 06-07: intercepting a form submission to process it with JavaScript instead of reloading the page.
form.addEventListener('submit', (event) => {
event.preventDefault(); // without this, the page reloads and you lose everything
// … create the task with the form's data
});Two important limits:
- Not every event is cancelable. If
event.cancelableisfalse,preventDefault()does nothing. Custom events are only cancelable if you create them with{ cancelable: true }. - If you registered the handler with
{ passive: true },preventDefault()is ignored and the browser prints a warning in the console. It is logical: you promised you would not call it.
And do not confuse preventDefault() with stopPropagation(): the first cancels what the browser would do, the second stops the event's journey through the tree. They are completely different things, and the table that separates them is in lesson 06-04.
- Event reference table
These are the events that cover practically the whole of an application like Nómada Tasks:
| Category | Event | When it fires | Notes |
|---|---|---|---|
| Mouse | click |
Press and release on the element | Also fired by Enter/Space on a <button> |
dblclick |
Two quick clicks | Arrives after two clicks |
|
mousedown / mouseup |
Press / release of the button | Before the click |
|
mouseenter / mouseleave |
The pointer enters/leaves the element | They do not fire when moving onto a child | |
mouseover / mouseout |
The pointer enters/leaves, children included | They fire many more times | |
contextmenu |
Right button | Cancelable | |
| Keyboard | keydown |
A key is pressed | Repeats if held down |
keyup |
A key is released | Only once | |
| Focus | focus / blur |
The element gains/loses focus | They do not bubble (see 06-04) |
focusin / focusout |
The same, but they do bubble | Useful with delegation | |
| Form | input |
The value changes, on every keystroke | Ideal for validating on the fly |
change |
The value changes and is confirmed | For text, on losing focus; for a select, on choosing |
|
submit |
The form is submitted | Fires on the <form>, not on the button |
|
reset |
The form is reset | Cancelable | |
| Window | DOMContentLoaded |
The DOM is built | On document |
load / resize / scroll |
Everything loaded / size change / scrolling | On window |
The difference between input and change deserves a demonstration, because they get confused daily:
const field = document.querySelector('#title');
field.addEventListener('input', (e) => console.log('input:', e.target.value));
field.addEventListener('change', (e) => console.log('change:', e.target.value));
// Marta types "Web" and clicks outside:
// input: W
// input: We
// input: Web
// change: Web ← only once, on confirmingAnd that of mouseenter versus mouseover:
li.addEventListener('mouseenter', () => console.log('enter')); // once, on entering the <li>
li.addEventListener('mouseover', () => console.log('over')); // again on moving onto the <span>,
// again on moving onto the <button>…For effects along the lines of "the mouse is over this card", always use mouseenter/mouseleave.
- Keyboard events and shortcuts
The KeyboardEvent object brings two similar properties that are not the same thing:
event.key: the character produced. It depends on the keyboard layout and on whether Shift is held:'a','A','Enter','Escape','ArrowDown',' '(space).event.code: the physical key pressed, independent of the layout:'KeyA','Enter','Space'.
For shortcuts and commands use key; code only makes sense in video games, where the physical position of the key matters.
document.addEventListener('keydown', (event) => {
// Escape closes the filter panel
if (event.key === 'Escape') {
closeFilters();
return;
}
// Ctrl+K (or Cmd+K on a Mac) focuses the search field
if (event.key === 'k' && (event.ctrlKey || event.metaKey)) {
event.preventDefault(); // the browser has its own Ctrl+K
document.querySelector('#search').focus();
}
});The available modifiers are ctrlKey, shiftKey, altKey and metaKey (the Command key on macOS and Windows on a PC). Checking ctrlKey || metaKey is the usual way of supporting both systems.
A very important accessibility detail: a global shortcut must not fire while the user is typing in a field. If Iván is typing the title of a task and presses k, he does not want the search to open:
document.addEventListener('keydown', (event) => {
const typing = event.target.matches('input, textarea, select, [contenteditable]');
if (typing) return; // guard: shortcuts do not apply inside a field
// … shortcuts
});
this inside a handler
this inside a handlerHere what you learned in 04-02 applies directly. When you register a traditional function as a handler, the browser invokes it with this pointing at the element where you registered the handler, that is, the same as event.currentTarget:
button.addEventListener('click', function (event) {
console.log(this === event.currentTarget); // true
this.disabled = true;
});With an arrow function, this is not reassigned: an arrow has no this of its own and takes the one from the scope where it was written (03-02). In a top-level module, that this is undefined:
button.addEventListener('click', (event) => {
console.log(this); // undefined in a module
console.log(event.currentTarget); // ✓ the button, always
});This is not a defect of arrows: it is exactly what makes them useful inside a class, where losing this in callbacks was the problem of 04-02.
class BoardController {
constructor(board, container) {
this.board = board;
this.container = container;
}
connect() {
// ✗ With a traditional function, 'this' becomes the button and this.board is undefined
// button.addEventListener('click', function () { this.board.summary(); });
// ✓ With an arrow, 'this' is still the controller
this.container.addEventListener('click', (event) => {
console.log(this.board.total); // 6 ✓
console.log(event.currentTarget.id); // 'task-list' ✓
});
}
}Practical rule: use arrows and event.currentTarget. That way this always means the same thing (your object) and you get the element from the event, which never lies. Save traditional functions for the specific case where you want the element's this and you are not inside a class.
- Accessibility: why a
<div> with a click is not enough
<div> with a click is not enoughThis works with a mouse:
And it is broken for everything else. A <div>:
- Does not receive focus with the Tab key. Anyone navigating by keyboard cannot reach it, and therefore can never activate it.
- Does not respond to Enter or Space, which is how controls are activated with the keyboard.
- Has no role. A screen reader announces "Mark done", without saying it is a button or that it can be activated.
- Has no state: it cannot be
disabledin a way that assistive technologies understand.
A <button> brings all of that out of the box:
<!-- ✓ A correct control -->
<button type="button" class="task__action" data-action="done">Mark done</button>button.addEventListener('click', markDone);
// This handler fires with the mouse, with Enter and with Space. For free.A direct comparison:
| Requirement | <div> with click |
<button> |
|---|---|---|
| Receives focus with Tab | No (needs tabindex="0") |
Yes |
| Activates with Enter/Space | No (has to be programmed) | Yes |
| Announced as a button | No (needs role="button") |
Yes |
Supports disabled |
No | Yes |
| Style controllable with CSS | Yes | Yes |
The only argument in favor of the <div> was styling, and for many years now a <button> has been fully stylable (all: unset, appearance: none, or simply rewriting background, border and padding).
Two more rules for the rest of the module:
- Every interactive element uses the tag that corresponds to it:
<button>for actions,<a href>for navigating,<input>/<select>for entering data. If a click takes you to another page, it is a link; if it runs an action on this one, it is a button. - If the button only has an icon, it needs an accessible name:
aria-label="Mark as done", or visually hidden text. A button with no text is a button with no name.
<button type="button" class="task__action" data-action="done"
aria-label="Mark "Carpentry workshop quote" as done">✓</button>
- Nómada Tasks: mark as done and filter by assignee
Let's make the page interactive. We extend the HTML with a filter bar and one button per task:
<section class="board" aria-labelledby="board-title">
<h2 id="board-title">Backlog</h2>
<div class="filters" role="group" aria-label="Filter by assignee">
<button type="button" class="filter" data-assignee="">All</button>
<button type="button" class="filter" data-assignee="Marta">Marta</button>
<button type="button" class="filter" data-assignee="Iván">Iván</button>
<button type="button" class="filter" data-assignee="Lucía">Lucía</button>
</div>
<p id="summary" class="summary" role="status"></p>
<ul id="task-list" class="task-list">
<li class="task" data-id="1">
<span class="task__title"></span>
<span class="task__meta"></span>
<button type="button" class="task__action" data-action="advance"></button>
</li>
<li class="task" data-id="3">
<span class="task__title"></span>
<span class="task__meta"></span>
<button type="button" class="task__action" data-action="advance"></button>
</li>
<li class="task" data-id="6">
<span class="task__title"></span>
<span class="task__meta"></span>
<button type="button" class="task__action" data-action="advance"></button>
</li>
</ul>
</section>The CSS needed, briefly:
.filters { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.filter, .task__action {
font: inherit; padding: 0.3rem 0.7rem; cursor: pointer;
border: 1px solid var(--border); border-radius: 0.35rem; background: #fff;
}
.filter[aria-pressed="true"] { background: #1f2933; color: #fff; }
.task__action[disabled] { opacity: 0.45; cursor: not-allowed; }
.task[hidden] { display: none; }And the controller. The key point of section 12 is that the model decides and the view obeys: the handler calls board.changeStatus(...), which applies rule R6 for transitions, and then repaints.
// js/view/controller.js
import { TODAY } from '../util/dates.js';
import { paintTask, paintSummary } from './paint.js';
/** Next status according to R6; null if the task is already finished. */
const NEXT = Object.freeze({ pending: 'in-progress', 'in-progress': 'done', done: null });
const LABEL = Object.freeze({
pending: 'Start', 'in-progress': 'Mark done', done: 'Completed'
});
/** Refreshes the action button of an <li> according to the status of its task. */
function refreshButton(li, task) {
const button = li.querySelector('.task__action');
const next = NEXT[task.status];
button.textContent = LABEL[task.status];
button.disabled = next === null; // ✓ property, not setAttribute
button.setAttribute('aria-label', `${LABEL[task.status]}: ${task.title}`);
}
export function connectActions(container, board, today = TODAY) {
for (const li of container.querySelectorAll('li[data-id]')) {
const task = board.findById(Number(li.dataset.id));
if (task === null) continue;
paintTask(li, task, today);
refreshButton(li, task);
const button = li.querySelector('.task__action');
button.addEventListener('click', (event) => {
const next = NEXT[task.status];
if (next === null) return;
try {
board.changeStatus(task.id, next); // the model validates (R6)
} catch (error) {
console.error(error.describe?.() ?? error.message);
return;
}
paintTask(li, task, today);
refreshButton(li, task);
paintSummary(document.querySelector('#summary'), board, today);
console.log(`${task.title} → ${task.status}`, event.timeStamp.toFixed(0), 'ms');
});
}
}
export function connectFilters(bar, container) {
for (const button of bar.querySelectorAll('.filter')) {
button.setAttribute('aria-pressed', button.dataset.assignee === '' ? 'true' : 'false');
button.addEventListener('click', (event) => {
const chosen = event.currentTarget.dataset.assignee;
// Visual state of the buttons (aria-pressed communicates which one is active)
for (const other of bar.querySelectorAll('.filter')) {
other.setAttribute('aria-pressed', String(other === event.currentTarget));
}
// Hide or show each task. 'hidden' takes it out of the accessibility tree.
for (const li of container.querySelectorAll('li[data-id]')) {
li.hidden = chosen !== '' && li.dataset.assignee !== chosen;
}
});
}
}// js/app.js
import { Board } from './model/board.js';
import { createBacklog } from './data/backlog.js';
import { TODAY } from './util/dates.js';
import { paintSummary } from './view/paint.js';
import { connectActions, connectFilters } from './view/controller.js';
const board = new Board('Taller Nómada', createBacklog());
const list = document.querySelector('#task-list');
const bar = document.querySelector('.filters');
const summary = document.querySelector('#summary');
connectActions(list, board, TODAY);
connectFilters(bar, list);
paintSummary(summary, board, TODAY);Try it: clicking "Start" on the carpentry task moves it to in-progress, the button changes to "Mark done", and the summary updates the open hours. On the second click it moves to done, the title gets a strikethrough, the button is disabled and the 45 open h drop to 40. All of it with rule R6 applied by the model, not by the view.
And now the snag. This code registers one handler for every button. With three tasks that is three; with the complete backlog of six it is six; with two hundred, two hundred. Worse still: in lesson 06-05 you will start creating the <li>s from JavaScript, and those new elements will have no handler, because connectActions has already run. You would have to reconnect after every change, with the risk of duplicating handlers.
There is a much better solution: a single handler on the <ul> that attends to the clicks of every button, present and future. It is called delegation, it rests on bubbling and on the closest() from 06-02, and it is the first thing you will see in the next lesson.
Common Mistakes and Tips
- Calling the function when registering it:
addEventListener('click', doIt())registers the returned value, not the function. No parentheses, or wrapped in an arrow if you need arguments. - Writing the type with
on:addEventListener('onclick', …)gives no error and never works, because there is no event calledonclick. The type is'click'. - Trying to remove an anonymous arrow.
removeEventListenercompares by identity: if you did not store the reference, you cannot remove it. Store the function in a constant, or use{ once: true }orAbortController(06-04). - Confusing
targetwithcurrentTarget.currentTargetis your element;targetis where the click happened, which may be a child. If the button contains a<span>with the icon,targetwill be the<span>. - Storing the event to use it later.
currentTargetisnulloutside the handler. Pull out what you need into variables before anysetTimeoutorawait. - Forgetting
preventDefault()on asubmit. The page reloads, the state is lost and it looks as though "nothing works". It is mistake number one of lesson 06-07. - Confusing
inputwithchange.inputfires on every keystroke;changeonly on confirming. To validate on the fly you wantinput; to react to a<select>, either of the two. - Using a
<div>as a button. It breaks the keyboard and screen readers.<button type="button">always; and with an explicittype, because inside a form the default value issubmit. - Tip: name your handlers.
onAdvanceClickas a named function shows up in stack traces and in the DevTools Event Listeners panel; an anonymous arrow shows up as(anonymous). - Tip: inspect the registered handlers. In the DevTools Elements panel there is an Event Listeners tab that shows every handler on an element and on its ancestors. It is the fastest way to discover duplicated handlers.
Exercises
Exercise 1 · A click counter with once
Write a snippet that registers two handlers on the "All" filter button: a normal one that counts the clicks and prints them, and another with { once: true } that prints "first click" and disappears. Check with four clicks that the first runs four times and the second once. Then add a third handler that you can remove from the console, and remove it.
Exercise 2 · Accessible keyboard shortcuts
Add these global shortcuts to the page, respecting the rule of not firing them while typing in a field:
1,2,3filter by Marta, Iván and Lucía respectively.0clears the filter.Escapeclears the filter and returns focus to the first button in the bar.
Reuse the existing buttons instead of duplicating the filtering logic.
Exercise 3 · Preview on hover
When the pointer enters a task <li>, show in the summary paragraph a line with the title, the reviewer and the days left until the due date; on leaving, restore the general summary. Use the right events so that the message does not flicker when the mouse passes over the inner button, and explain why that pair of events is the right one.
Solutions
Exercise 1
const allButton = document.querySelector('.filter[data-assignee=""]');
let clicks = 0;
allButton.addEventListener('click', () => {
clicks += 1;
console.log('clicks:', clicks);
});
allButton.addEventListener('click', () => console.log('first click'), { once: true });
// Third handler, removable: the reference has to be stored
function notify(event) {
console.log('notifying from', event.currentTarget.textContent);
}
allButton.addEventListener('click', notify);
// Later, from the console:
allButton.removeEventListener('click', notify); // ✓ works: same referenceWith four clicks the console shows clicks: 1, first click, clicks: 2, clicks: 3, clicks: 4. The order within a single click is the order of registration. The counter works because clicks lives in the handler's closure (03-04): every call sees and updates the same variable.
Exercise 2
const bar = document.querySelector('.filters');
const BY_KEY = { '1': 'Marta', '2': 'Iván', '3': 'Lucía', '0': '' };
function pressFilter(assignee) {
bar.querySelector(`.filter[data-assignee="${assignee}"]`)?.click();
}
document.addEventListener('keydown', (event) => {
// Guard: shortcuts do not apply while typing
if (event.target.matches('input, textarea, select, [contenteditable]')) return;
if (event.ctrlKey || event.metaKey || event.altKey) return; // do not override system shortcuts
if (Object.hasOwn(BY_KEY, event.key)) {
event.preventDefault();
pressFilter(BY_KEY[event.key]);
return;
}
if (event.key === 'Escape') {
pressFilter('');
bar.querySelector('.filter').focus();
}
});The elegant part of this solution is button.click(): instead of duplicating the filtering logic, it simulates the click on the button that already has it, and so the visual state (aria-pressed) stays consistent without writing another line. The event generated by click() has isTrusted: false, but otherwise it is identical. The BY_KEY object is the lookup dictionary from 02-03, much cleaner than a chain of ifs. And returning focus after Escape is an essential courtesy: without it, anyone navigating by keyboard is left with no starting point.
Exercise 3
import { readableDate } from './util/dates.js';
const summary = document.querySelector('#summary');
for (const li of list.querySelectorAll('li[data-id]')) {
const task = board.findById(Number(li.dataset.id));
li.addEventListener('mouseenter', () => {
summary.textContent =
`${task.title} · reviewed by ${task.reviewer ?? 'nobody'} · ` +
`due ${readableDate(task.dueDate)} (${task.daysLeft} days)`;
});
li.addEventListener('mouseleave', () => {
paintSummary(summary, board, TODAY);
});
}The correct pair is mouseenter/mouseleave because they do not fire when moving between the element's children. With mouseover/mouseout the message would flicker: moving from the title <span> to the <button> would fire a mouseout (which would restore the summary) followed by a mouseover (which would put it back), dozens of times per second. mouseenter only fires when crossing the outer boundary of the <li>.
An accessibility note: this preview only exists for people using a mouse. To make it complete you would have to add the keyboard equivalents (focusin/focusout on a focusable element), and that fits with the events that do bubble, from the next lesson.
Conclusion
You now know how to make the page respond. An event is a notification the browser generates, a handler is a function you register so that it gets called for you, and the mechanism that makes it possible is the same event loop from 05-07: your handler is queued as a task and runs when the stack is free, from which follows the rule that a handler must be brief, because while it runs the interface is frozen.
Of the three historical ways of registering handlers —the onclick attribute, the element.onclick property and addEventListener— only the last is acceptable: it separates markup and logic, allows several handlers per event, works with modules and accepts options. You know those options: once for things that only happen once, passive for not blocking scrolling, and capture and signal, which unfold in the next lesson. And you know the removeEventListener trap: it compares by identity, so an anonymous arrow or an inline bind is impossible to remove; you have to store the reference, and you have to remove the handlers on window and document that are no longer used so as not to retain memory.
Of the Event object you have mastered the essentials: type, timeStamp, isTrusted, the crucial difference between target (where it happened) and currentTarget (where you are listening) —with the warning that the latter is emptied when you leave the handler—, and preventDefault() for canceling the browser's default action, which will be essential in the form's submit. You have the reference table of mouse, keyboard, focus and form events, with the distinctions that get confused most: input versus change, mouseenter versus mouseover, key versus code. You know that this in a traditional handler is the element and in an arrow is the one from the outer scope, and that the sensible rule inside a class is to use arrows and read the element from event.currentTarget. And you are clear, with no excuses, that a control is written with <button>: a <div> with a click receives no focus, does not respond to Enter or Space and is not announced as a button.
In Nómada Tasks that has produced js/view/controller.js with connectActions and connectFilters: each task advances pending → in-progress → done with rule R6 validated by the model, the button changes its text and is disabled once finished, the summary updates, and the filter bar uses aria-pressed and hidden to communicate the state to everybody.
And a problem has appeared that cannot be ignored: there is one handler per button. Six tasks, six handlers; and as soon as you start creating the <li>s from JavaScript, the new elements will be born with no handler. The solution is not to reconnect every time, but to understand how an event travels through the tree —capture, target and bubbling— and to place a single handler on the <ul> that attends to every button that exists now and in the future. That pattern, together with the tools for stopping an event's journey and with the custom events that will let you decouple the view from the model, is Propagation, Delegation and Custom Events.
JavaScript Course: From Beginner to Advanced
Module 1: Introduction to JavaScript
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
