intermediate Step 11 of 20

Events and Event Handling

JavaScript Programming

Events and Event Handling

Events are the heartbeat of interactive web applications. When a user clicks a button, types in a form, scrolls the page, or moves the mouse, the browser fires events that your JavaScript code can listen for and respond to. The event system is built on a pattern called the observer pattern — you register listener functions that are called automatically when specific events occur. Understanding events, event propagation, and event delegation is essential for building responsive and efficient web applications.

Adding Event Listeners

// addEventListener (recommended approach)
const button = document.querySelector("#myButton");

button.addEventListener("click", function(event) {
    console.log("Button clicked!");
    console.log("Event type:", event.type);
    console.log("Target:", event.target);
});

// Arrow function handler
button.addEventListener("click", (e) => {
    e.target.textContent = "Clicked!";
});

// Named function (can be removed later)
function handleClick(e) {
    console.log("Clicked at:", e.clientX, e.clientY);
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);

// Common events
const input = document.querySelector("input");
input.addEventListener("input", (e) => {
    console.log("Value:", e.target.value);      // Every keystroke
});
input.addEventListener("change", (e) => {
    console.log("Changed to:", e.target.value);  // On blur/enter
});
input.addEventListener("focus", () => console.log("Focused"));
input.addEventListener("blur", () => console.log("Blurred"));

// Keyboard events
document.addEventListener("keydown", (e) => {
    console.log(`Key: ${e.key}, Code: ${e.code}`);
    if (e.key === "Escape") {
        closeModal();
    }
    if (e.ctrlKey && e.key === "s") {
        e.preventDefault();  // Prevent browser save dialog
        saveDocument();
    }
});

Event Propagation

// Events propagate in 3 phases:
// 1. Capturing — from window down to target
// 2. Target — at the target element
// 3. Bubbling — from target back up to window

// By default, listeners fire during BUBBLING phase
// Use { capture: true } or third arg for capturing phase

const parent = document.querySelector(".parent");
const child = document.querySelector(".child");

parent.addEventListener("click", () => {
    console.log("Parent clicked");
});

child.addEventListener("click", (e) => {
    console.log("Child clicked");
    // Without stopPropagation, both handlers fire
    // e.stopPropagation();  // Stops event from reaching parent
});

// Clicking child logs: "Child clicked" then "Parent clicked"

// preventDefault — stop the browser's default behavior
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
    e.preventDefault();  // Stop form from reloading the page
    const formData = new FormData(form);
    console.log("Name:", formData.get("name"));
    // Handle form submission with JavaScript instead
});

const link = document.querySelector("a");
link.addEventListener("click", (e) => {
    e.preventDefault();  // Stop navigation
    console.log("Link clicked but not followed");
});

Event Delegation

Event delegation uses event bubbling to handle events on multiple child elements with a single listener on the parent. This is more efficient and works with dynamically added elements.

// Instead of adding a listener to EVERY button:
// BAD — does not work for dynamically added items
// document.querySelectorAll(".item-btn").forEach(btn => {
//     btn.addEventListener("click", handleClick);
// });

// GOOD — event delegation on the parent
const list = document.querySelector("#todo-list");

list.addEventListener("click", (e) => {
    // Check what was actually clicked
    if (e.target.matches(".delete-btn")) {
        const item = e.target.closest(".todo-item");
        item.remove();
    }

    if (e.target.matches(".toggle-btn")) {
        const item = e.target.closest(".todo-item");
        item.classList.toggle("completed");
    }
});

// Now add items dynamically — they automatically have event handling!
function addTodo(text) {
    list.insertAdjacentHTML("beforeend", `
        
  • ${text}
  • `); } addTodo("Learn JavaScript"); addTodo("Build a project");

    Custom Events

    // Create and dispatch custom events
    const event = new CustomEvent("userLoggedIn", {
        detail: { userId: 123, name: "Alice" },
        bubbles: true
    });
    
    document.addEventListener("userLoggedIn", (e) => {
        console.log(`User logged in: ${e.detail.name}`);
        updateUI(e.detail);
    });
    
    // Dispatch from any element
    document.querySelector("#login-form").dispatchEvent(event);
    
    Pro tip: Always use event delegation for lists, tables, and any container with multiple interactive children. Attach one listener to the parent and use e.target.matches(selector) or e.target.closest(selector) to determine what was clicked. This is more performant, uses less memory, and automatically works with dynamically added elements.

    Key Takeaways

    • Use addEventListener() to attach event handlers — it supports multiple listeners and removal.
    • Events bubble up from the target element to the document root; use stopPropagation() to prevent this.
    • Use preventDefault() to stop default browser behavior like form submission or link navigation.
    • Event delegation attaches one listener to a parent to handle events on all children, including dynamically added ones.
    • Use e.target.closest(selector) in delegated handlers to find the relevant parent element of the clicked target.