03 The Event Object, Input Handling, and Interactive Components
On this page (16 sections)
1. The event object#
Every event handler receives an event object as its first parameter. This object describes what happened, not just that something happened.
element.addEventListener("click", function (event) {
console.log(event);
});
Key properties used throughout this practice set:
| Property | Meaning |
|---|---|
event.currentTarget | The element the handler is attached to (also available as this inside a regular function) |
event.target | The actual element that triggered the event (may be a descendant of currentTarget) |
event.type | The event name as a string, such as "click" |
event.button | Which mouse button was pressed (0 = left, 1 = middle, 2 = right) |
event.clientX, event.clientY | Mouse position relative to the browser window |
event.pageX, event.pageY | Mouse position relative to the full document, including scroll |
currentTarget and target are the same element only when the click lands exactly on the element the handler is attached to. When the handler is attached to a container (like the <p> in the console-logging task) and the user clicks something inside it (a link, a span), target refers to that inner element while currentTarget stays fixed on the container. This distinction is the basis of event delegation, covered next.
2. Event delegation#
Rather than attaching a separate handler to every link and every span inside a paragraph, attach one handler to the shared parent and inspect event.target to determine what was actually clicked.
paragraph.addEventListener("click", function (event) {
const clicked = event.target;
console.log(clicked.tagName); // "A", "SPAN", "P", etc.
});
This is more efficient than attaching handlers one by one, and it automatically covers elements added later. Combine event.target.tagName with a conditional to branch behavior based on what kind of element was clicked:
if (clicked.tagName === "SPAN") {
console.log(clicked.textContent);
}
3. Preventing default behavior#
Some elements have a built-in default action when interacted with: a link navigates, a form submits, a right-click opens the browser’s context menu. Call event.preventDefault() inside the handler to stop that default action while still letting the rest of your handler code run.
link.addEventListener("click", function (event) {
if (link.textContent === "libero") {
event.preventDefault();
}
});
The check happens first (is this the condition under which we want to block navigation), then preventDefault() runs conditionally. Note this is different from removing the handler or disabling the element: the click event still fires, and any other logic in the handler (like logging) still executes. Only the browser’s default follow-the-link behavior is suppressed.
4. Restricting text input as the user types#
To filter what a user can type, listen for input and immediately strip out anything that does not match what you want to allow, writing the cleaned value back into the field.
numberInput.addEventListener("input", function () {
numberInput.value = numberInput.value.replace(/[^0-9]/g, "");
});
String.replace with a regular expression and the global flag (g) replaces every match, not just the first. The pattern [^0-9] means “any character that is not a digit.” Replacing every non-digit match with an empty string leaves only digits behind. Because this reassigns .value inside the same input handler, invalid characters are removed the instant they are typed.
To make this reusable across many inputs rather than one specific field, select by class instead of by ID, then attach the same handler to each match:
document.querySelectorAll("input.number").forEach(function (input) {
input.addEventListener("input", function () {
input.value = input.value.replace(/[^0-9]/g, "");
});
});
This pattern (select all elements sharing a class, loop, attach an identical handler to each) is the general solution any time a task says “make this work for any element with class X,” and it reappears later in this document.
5. Formatting input as the user types#
A related but distinct task: instead of removing characters, you reformat them, such as inserting spaces every four characters in a bank account number.
accountInput.addEventListener("input", function () {
const digitsOnly = accountInput.value.replace(/[^0-9]/g, "");
const groups = digitsOnly.match(/.{1,4}/g) || [];
accountInput.value = groups.join(" ");
});
Strip to digits first (section 4’s technique), then use .match(/.{1,4}/g) to split the digit string into chunks of up to four characters, and .join(" ") to reassemble them with spaces between. The || [] guards against .match returning null when the input is empty (an empty string has no matches, and null.join would throw an error).
6. Reading the resolved URL and comparing domains#
An anchor element’s .href property returns the fully resolved absolute URL, even if the href attribute in the HTML was written as a relative path. The built-in URL object parses that string into parts, including the domain.
const url = new URL(link.href);
url.hostname; // e.g. "www.elte.hu"
To check whether a hostname belongs to a given domain, including subdomains (webprogramozas.inf.elte.hu should count as part of elte.hu), check that it equals the domain or ends with . plus the domain:
function belongsToDomain(hostname, domain) {
return hostname === domain || hostname.endsWith("." + domain);
}
To disable a link so it can no longer be followed, you cannot use a disabled property (that only exists on form controls). Instead, remove or neutralize its href, or listen for click and call preventDefault() (section 3) when the domain check fails. Adding a visual style (like a CSS class) alongside this makes the disabled state visible as well as functional.
7. Hover events#
mouseenter (or mouseover) fires when the pointer enters an element; mouseleave (or mouseout) fires when it exits. Use these to show detail on hover.
titleElement.addEventListener("mouseenter", function () {
detailContainer.textContent = buildDetailText(movie);
});
mouseenter does not fire again for movement over child elements, which makes it the better default choice over mouseover for this kind of “show detail for this one element” behavior.
8. Combining filtering and hover: the movie list pattern#
This task combines three things already covered: an array of objects (Practice 02, section 14), filtering on the input event (section 4’s event, Practice 02’s .filter), and hover-to-show-detail (section 7).
const movies = [
{ title: "...", year: 2010, length: 120, director: "..." },
// ...
];
function renderList(filterText) {
list.innerHTML = "";
const matches = movies.filter(function (movie) {
return movie.title.includes(filterText);
});
matches.forEach(function (movie) {
const item = document.createElement("li");
item.textContent = movie.title;
item.addEventListener("mouseenter", function () {
detail.textContent = `${movie.title} (${movie.year}), directed by ${movie.director}, ${movie.length} min`;
});
list.appendChild(item);
});
}
filterBox.addEventListener("input", function () {
renderList(filterBox.value);
});
renderList(""); // initial full list
Wrapping the render logic in a named function that takes the filter text as a parameter, and calling that function both from the event handler and once up front, avoids duplicating the list-building code for the initial and updated states. This same shape (a render function called on every relevant event, plus once at setup) is worth reusing for the memory game and hangman game below.
9. Keyboard events#
keydown fires when a key is pressed; the event object describes which key.
document.addEventListener("keydown", function (event) {
event.key; // the character or key name, e.g. "a", "Enter", "Control"
event.ctrlKey; // true if Ctrl was held during this key press
event.shiftKey;
event.altKey;
});
event.key gives a human-readable key name directly, so there is no need to translate key codes manually. For a screencast-style display, build a label from the modifier flags plus the key:
document.addEventListener("keydown", function (event) {
const parts = [];
if (event.ctrlKey) parts.push("Ctrl");
if (event.shiftKey) parts.push("Shift");
if (event.altKey) parts.push("Alt");
parts.push(event.key);
display.textContent = parts.join(" + ");
});
Checking each modifier flag and only including it in the label when true produces combinations like “Ctrl + A” without hardcoding every possible combination.
10. The right-click context menu event#
The contextmenu event fires when the user right-clicks. Calling preventDefault() (section 3) stops the browser’s built-in menu from appearing, which is the first step to replacing it with your own.
document.addEventListener("contextmenu", function (event) {
event.preventDefault();
showCustomMenu(event.pageX, event.pageY);
});
event.pageX and event.pageY (section 1) give the position to place the custom menu at, typically by setting .style.left and .style.top on a positioned element. A click anywhere else afterward (a click listener on document) is the usual way to hide the menu again.
11. Dynamically inserting an element next to another#
insertAdjacentElement places a new element relative to an existing one without disturbing the existing one’s other siblings.
const valueDisplay = document.createElement("span");
valueDisplay.textContent = slider.value;
slider.insertAdjacentElement("afterend", valueDisplay);
The first argument is a position keyword:
| Position | Meaning |
|---|---|
"beforebegin" | Just before the element itself, as a preceding sibling |
"afterbegin" | Inside the element, before its first child |
"beforeend" | Inside the element, after its last child |
"afterend" | Just after the element itself, as a following sibling |
"afterend" places the new element as a sibling immediately following the target, which is the natural choice for “display the value next to the slider.” "beforebegin" is the natural choice for “display the value above the slider,” since that positions the new element as a preceding sibling.
12. The range input and its event#
A range slider fires input continuously while being dragged (not just when released), which makes input the right event for a live-updating display (section 4’s event, again).
slider.addEventListener("input", function () {
valueDisplay.textContent = slider.value;
});
To make this reusable for any slider sharing a class, combine this with the select-all-and-loop pattern from section 4, creating the display element (section 11) once per slider during setup rather than assuming it already exists in the HTML:
document.querySelectorAll("input.display-value").forEach(function (slider) {
const display = document.createElement("span");
display.textContent = slider.value;
slider.insertAdjacentElement("afterend", display);
slider.addEventListener("input", function () {
display.textContent = slider.value;
});
});
Declaring display inside the loop, scoped to that one slider, means each slider’s handler updates only its own adjacent display and not some other slider’s.
13. Toggling visibility on click: the FAQ pattern#
This combines event delegation (section 2), tagName checks, and the hidden property (Practice 02, section 5).
document.querySelector(".faq").addEventListener("click", function (event) {
if (event.target.tagName === "H2") {
const answer = event.target.nextElementSibling;
answer.hidden = !answer.hidden;
}
});
nextElementSibling refers to the next sibling element in the markup, which in this HTML structure is the <p> immediately following the clicked <h2>. !answer.hidden flips the current boolean value, so each click toggles between shown and hidden rather than only ever showing or only ever hiding. Since the click handler is attached once to the shared .faq container rather than to each question individually, it automatically covers every question without a separate listener per <h2>.
14. Managing state across many small interactions: the memory game#
A memory game needs: a shuffled board of paired values, a way to track which cards are currently face up, a way to compare two face-up cards, and a counter.
Set up the board data first, independent of the DOM:
const values = [1, 1, 2, 2, 3, 3, 4, 4]; // pairs
values.sort(function () { return Math.random() - 0.5; }); // shuffle
Sorting with a comparison function that returns a random positive or negative number produces an unpredictable order; this is a simple, common way to shuffle an array, though not a perfectly unbiased one.
Track game state in variables outside any single handler, since multiple clicks need to share this state:
let flippedCards = []; // currently face-up cards, waiting to be compared
let steps = 0;
let locked = false; // true while showing a mismatched pair, to block further clicks
Each card’s click handler:
function onCardClick(card, value) {
if (locked || card.classList.contains("matched") || flippedCards.includes(card)) {
return; // ignore clicks on already-matched or already-flipped cards, or while locked
}
reveal(card, value);
flippedCards.push(card);
if (flippedCards.length === 2) {
steps++;
if (flippedCards[0].dataset.value === flippedCards[1].dataset.value) {
flippedCards.forEach(function (c) { c.classList.add("matched"); });
flippedCards = [];
} else {
locked = true;
setTimeout(function () {
flippedCards.forEach(hide);
flippedCards = [];
locked = false;
}, 1000);
}
}
}
The early return guard prevents a card from being flipped a second time while it is already face up, and prevents any clicks at all while a mismatched pair is being shown (locked). setTimeout delays the hide-again action so the player has a moment to see the mismatched pair before it flips back. dataset.value reads a custom data-value attribute you set on each card element when building the board, letting you store each card’s identity directly on its DOM element.
For multiple players, add a currentPlayer variable, display it somewhere on the page, and advance it (looping back to the first player) whenever a pair does not match:
let currentPlayer = 0;
const playerCount = 2;
// inside the mismatch branch, before or after resetting:
currentPlayer = (currentPlayer + 1) % playerCount;
The remainder operator (%) wraps the player index back to 0 once it would otherwise exceed playerCount - 1, cycling through players in turn. A common house rule is that a correct match lets the same player go again, so only advance currentPlayer in the mismatch branch, not the match branch.
15. Managing state across many small interactions: the hangman game#
Hangman needs similar state management to the memory game: a target word chosen once, a record of which letters have been guessed, and counters for mistakes.
const words = ["banana", "elephant", "keyboard"];
const word = words[Math.floor(Math.random() * words.length)];
let guessedLetters = new Set();
let wrongCount = 0;
const maxWrong = 6;
A Set is a convenient structure here because it automatically prevents duplicate entries and offers a fast .has() check, useful since a player might click the same letter button twice.
Build one button per letter of the alphabet, and a display cell per letter of the word:
letterButtons.forEach(function (button) {
button.addEventListener("click", function () {
const letter = button.textContent.toLowerCase();
if (guessedLetters.has(letter)) return;
guessedLetters.add(letter);
button.disabled = true;
if (word.includes(letter)) {
render();
checkWin();
} else {
wrongCount++;
updateHangmanDrawing();
if (wrongCount >= maxWrong) {
showLoseMessage();
}
}
});
});
button.disabled = true (Practice 02, section 12) prevents a guessed letter from being clicked again, which is a simpler safeguard than only relying on the Set check. The render function should loop over each letter of word and display it if guessedLetters.has(thatLetter), or a blank otherwise, rebuilding the letter-box row from scratch each time (Practice 02, section 2’s “clear and rebuild” idea).
checkWin compares whether every letter of word is present in guessedLetters:
function checkWin() {
const solved = word.split("").every(function (letter) {
return guessedLetters.has(letter);
});
if (solved) {
showWinMessage();
}
}
.every returns true only if the given condition holds for all elements, which is exactly “every letter of the word has been guessed.”
For the SVG hangman drawing, prepare separate SVG shapes (or groups of shapes) for each stage, and toggle their visibility (hidden, Practice 02 section 5, or an SVG visibility attribute) based on the current wrongCount, revealing one more piece of the drawing per wrong guess.
Summary: how the pieces combine#
Practice 03 layers three new ideas on top of Practice 02’s foundation:
- The event object (section 1) tells you what happened and where.
targetversuscurrentTarget(section 2) is the key distinction for handling clicks on a container’s children. - Controlling default behavior and input content (sections 3, 4, 5) lets you block navigation, strip invalid characters, or reformat text as it is typed.
- State that persists across multiple events (sections 12 to 15) requires variables declared outside the handler, updated inside it, and a
render-style function that keeps the displayed DOM consistent with that state after every change.
Every exercise in this set reduces to identifying which event fits (click, input, mouseenter, keydown, contextmenu), what information that event’s object provides, and whether the task needs a single reaction to that event or ongoing state tracked across many of them.