Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
20 minLesson 18 of 35
DOM & Browser APIs

Event Listeners & Event Delegation

Event Listeners & Event Delegation

Events are how web pages respond to user actions — clicks, keystrokes, form submissions, mouse movements. Understanding how events work under the hood, especially event bubbling and delegation, is what separates junior from senior web developers.

Adding Event Listeners

const button = document.querySelector("#submit");

// addEventListener is the modern approach
button.addEventListener("click", function(event) {
    console.log("Clicked!", event);
});

// Arrow function shorthand
button.addEventListener("click", (e) => handleClick(e));

// Separate handler (best for removing listeners later)
function handleClick(event) {
    console.log("Button clicked");
}
button.addEventListener("click", handleClick);

// Remove a listener — you need the exact same function reference
button.removeEventListener("click", handleClick);

The Event Object

button.addEventListener("click", (event) => {
    event.type;          // "click"
    event.target;        // the element that was clicked
    event.currentTarget; // the element the listener is on
    event.timeStamp;     // when it happened (milliseconds)
    
    // Mouse events
    event.clientX;       // cursor X relative to viewport
    event.clientY;       // cursor Y relative to viewport
    event.button;        // 0=left, 1=middle, 2=right
    
    // Keyboard events
    event.key;           // "Enter", "a", "ArrowUp"
    event.code;          // "KeyA", "Enter" (physical key)
    event.shiftKey;      // modifier key states
    event.ctrlKey;
    event.altKey;
    event.metaKey;       // Cmd on Mac, Windows key on PC
    
    // Prevent default browser behavior
    event.preventDefault();   // stop form submit, stop link navigation
    
    // Stop event from bubbling up
    event.stopPropagation();
});

Common Event Types

// Mouse
el.addEventListener("click", handler);
el.addEventListener("dblclick", handler);
el.addEventListener("mousedown", handler);
el.addEventListener("mouseup", handler);
el.addEventListener("mouseover", handler);   // fires on child elements too
el.addEventListener("mouseenter", handler);  // only fires on the element itself
el.addEventListener("mouseleave", handler);
el.addEventListener("mousemove", handler);

// Keyboard (on document or input elements)
document.addEventListener("keydown", handler);   // fires while held
document.addEventListener("keypress", handler);  // deprecated
document.addEventListener("keyup", handler);     // fires on release

// Form
form.addEventListener("submit", handler);
input.addEventListener("change", handler);       // after value changes & loses focus
input.addEventListener("input", handler);        // every keystroke
input.addEventListener("focus", handler);
input.addEventListener("blur", handler);         // loses focus

// Window/document
window.addEventListener("load", handler);        // page fully loaded
document.addEventListener("DOMContentLoaded", handler); // DOM ready (before images)
window.addEventListener("resize", handler);
window.addEventListener("scroll", handler);
window.addEventListener("beforeunload", handler); // user leaving page

Event Bubbling

Events bubble up through the DOM — from the target element all the way to the document:

// HTML: <div id="outer"><div id="inner"><button>Click</button></div></div>

document.querySelector("button").addEventListener("click", () => {
    console.log("button");  // fires first
});
document.querySelector("#inner").addEventListener("click", () => {
    console.log("inner");   // fires second
});
document.querySelector("#outer").addEventListener("click", () => {
    console.log("outer");   // fires third
});

// Clicking the button logs: "button", "inner", "outer"

// Stop bubbling
button.addEventListener("click", (e) => {
    e.stopPropagation();  // inner and outer don't receive the event
    handleButtonClick();
});

Event Delegation

Instead of adding listeners to every child, add ONE listener to a parent:

// ❌ Bad — listener on every item (slow, doesn't work for dynamic items)
document.querySelectorAll(".list-item").forEach(item => {
    item.addEventListener("click", handleItemClick);
});

// ✅ Good — one listener on the parent
const list = document.querySelector(".list");
list.addEventListener("click", (e) => {
    // Find the closest list-item ancestor of what was clicked
    const item = e.target.closest(".list-item");
    if (!item) return;  // click was on the list but not an item
    
    handleItemClick(item);
});

Why delegation is powerful:

  • Works for dynamically added elements
  • Uses far less memory
  • One place to handle all item events
// Full example: delegated actions
const table = document.querySelector("#users-table");
table.addEventListener("click", (e) => {
    const row = e.target.closest("tr[data-user-id]");
    if (!row) return;
    
    const userId = row.dataset.userId;
    
    if (e.target.matches(".edit-btn")) editUser(userId);
    if (e.target.matches(".delete-btn")) deleteUser(userId);
    if (e.target.matches(".view-btn")) viewUser(userId);
});

Preventing Default Behavior

// Prevent form submission
form.addEventListener("submit", (e) => {
    e.preventDefault();  // stops page reload
    validateAndSubmit(form);
});

// Prevent link navigation
link.addEventListener("click", (e) => {
    e.preventDefault();
    router.navigate(link.href);  // use custom router instead
});

// Prevent context menu on right-click
canvas.addEventListener("contextmenu", (e) => e.preventDefault());

Once and Passive Options

// Options object as third argument
button.addEventListener("click", handler, {
    once: true,      // auto-removes after first fire
    passive: true,   // tells browser the handler won't call preventDefault (perf boost)
    capture: true    // capture phase instead of bubble phase
});

// Passive listeners are especially important for scroll/touch
window.addEventListener("scroll", onScroll, { passive: true });  // 60fps!
window.addEventListener("touchstart", onTouch, { passive: false });  // can preventDefault

Performance: Throttle and Debounce

Some events fire constantly (scroll, resize, mousemove). Throttle/debounce limits how often your handler runs:

// Debounce — wait until the user stops
function debounce(fn, delay) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), delay);
    };
}

// Only search after user stops typing for 300ms
const search = debounce((query) => fetchResults(query), 300);
input.addEventListener("input", (e) => search(e.target.value));

// Throttle — fire at most once per interval
function throttle(fn, limit) {
    let inThrottle;
    return (...args) => {
        if (!inThrottle) {
            fn(...args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

// Handle scroll at most 60fps
const onScroll = throttle(() => updateScrollProgress(), 16);
window.addEventListener("scroll", onScroll, { passive: true });

Next lesson: Forms — validation, submission, and handling all input types in JavaScript.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!