Skip to main content

πŸ–±οΈ Event Listeners and Handlers

A web page that can't respond to the user is just a poster. Events are how JavaScript listens for the things people (and browsers) do β€” clicks, keystrokes, scrolls, form submissions β€” and runs your code in response. Master events and static pages become living applications.

Week 2 · Day 3 (Wednesday: Events and Event Handling) · Lecture 1

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain what an event is and trace the flow from user action to handler
  • Attach and remove handlers with addEventListener and know why it beats onclick
  • Read the event object to learn what happened, where, and how
  • Handle the most common mouse, keyboard, form, and window events
  • Use preventDefault() and the options object (once, passive, capture)
  • Create and dispatch custom events, and clean up listeners to avoid memory leaks

Estimated Time: 65 minutes

Practice: Build a live character counter and a keyboard-driven shortcut handler.

In This Lesson

What Is an Event?

An event is a signal that something happened. Think of a doorbell: someone presses the button (the action), the bell rings (the event fires), and you decide what to do β€” answer, ignore, or peek through the window (your handler). The browser is constantly firing events as the user interacts with the page; your job is to listen for the ones you care about and respond.

Three pieces make this work, and it helps to name them precisely:

  • Event target β€” the element the event happens on (a button, an input, the whole window).
  • Event listener β€” the registration that says "when this event happens on this target, call that function."
  • Event handler β€” the function that actually runs and does the work.
graph LR A[User Action
click, keypress, scroll] --> B[Browser Fires Event] B --> C[Listener Registered
with addEventListener] C --> D[Handler Function Runs] D --> E[Page Updates / Response]

Everything interactive you'll ever build β€” a menu that opens, a form that validates, a game that responds to arrow keys β€” is this loop repeated thousands of times a second. Let's learn how to hook into it.

Three Ways to Handle Events

Historically there have been three ways to attach a handler. Modern code uses exactly one of them β€” but you should recognize all three, because you'll meet them in the wild.

1. Inline HTML attributes (avoid)

<!-- Behavior tangled into markup β€” hard to reuse, hard to test -->
<button onclick="alert('Hello!')">Click me</button>

πŸ‘Ž This mixes JavaScript into your HTML, can only hold one handler, and is a security and maintainability headache. Fine for a five-second demo, wrong for real projects.

2. DOM event properties (onclick)

const button = document.querySelector('#save');
button.onclick = function () {
    console.log('Button clicked!');
};

// The catch: assigning again OVERWRITES the first handler.
button.onclick = () => console.log('Only this one survives');

πŸ‘ Better than inline, but each element can hold only one onclick. A second assignment silently erases the first.

3. addEventListener (the modern standard)

const button = document.querySelector('#save');

button.addEventListener('click', () => {
    console.log('Button clicked!');
});

// You can attach as MANY listeners as you like β€” they all run:
button.addEventListener('click', () => console.log('First handler'));
button.addEventListener('click', () => console.log('Second handler'));

πŸ‘πŸ‘ This is the one to use. It supports multiple handlers per event, works with every event type, can be removed later, and accepts an options object for finer control.

The options object

function handleClick() { console.log('clicked once'); }

button.addEventListener('click', handleClick, {
    once: true,      // auto-remove the listener after it fires one time
    capture: false,  // listen during the bubbling phase (the default)
    passive: true    // promise you won't call preventDefault() β€” speeds up scroll/touch
});

πŸ“– Why addEventListener wins

It separates behavior from markup, lets multiple independent features respond to the same event without stepping on each other, and β€” crucially β€” pairs with removeEventListener so you can tear listeners down when a component goes away. Reach for it every time.

The Event Object

Every handler automatically receives one argument: the event object. It's a detailed report of what just happened β€” which element, where the mouse was, which key was pressed, and methods to change the browser's default reaction.

button.addEventListener('click', (event) => {
    // What kind of event and where it came from
    console.log(event.type);          // "click"
    console.log(event.target);        // the exact element that was clicked
    console.log(event.currentTarget); // the element the listener is attached to
    console.log(event.timeStamp);     // ms since the page loaded

    // Mouse position (for mouse events)
    console.log(event.clientX, event.clientY); // relative to the viewport
    console.log(event.pageX, event.pageY);     // relative to the whole document

    // Modifier keys held during the event
    console.log(event.shiftKey, event.ctrlKey, event.altKey, event.metaKey);
});

πŸ’‘ target vs currentTarget

event.target is where the event originated β€” the exact thing clicked, even if it's a child. event.currentTarget is the element whose listener is running. When you click an icon inside a button, target is the icon but currentTarget is the button. This distinction becomes essential for event delegation (next lesson).

Changing the browser's default: preventDefault()

Many elements have built-in behavior: links navigate, form submit buttons reload the page, right-clicks open a context menu. preventDefault() cancels that default so you can supply your own.

const link = document.querySelector('#details-link');

link.addEventListener('click', (event) => {
    event.preventDefault();          // stop the browser from navigating away
    console.log('Loading details without leaving the page…');
    loadDetailsInPlace(link.href);   // your custom behavior instead
});

⚠️ preventDefault() is not stopPropagation()

preventDefault() cancels the browser's built-in action but the event still travels through the DOM. stopPropagation() stops the event from reaching other elements but does not cancel default behavior. They solve different problems β€” we cover propagation in depth in the next lesson.

Mouse & Keyboard Events

These are the events you'll reach for constantly. Here are the ones worth memorizing.

Mouse events

EventFires when…
clickThe element is pressed and released (also fires on keyboard "activation")
dblclickTwo quick clicks land on the element
contextmenuThe user right-clicks (call preventDefault() to replace the menu)
mouseenter / mouseleaveThe pointer enters/leaves the element (does not bubble β€” clean for hover)
mousemoveThe pointer moves over the element (fires a lot β€” throttle it)
mousedown / mouseupA button is pressed / released (the two halves of a click)
const box = document.querySelector('.interactive');

box.addEventListener('click', (e) => {
    console.log('Clicked at', e.clientX, e.clientY);
});

box.addEventListener('mouseenter', () => box.classList.add('is-hovered'));
box.addEventListener('mouseleave', () => box.classList.remove('is-hovered'));

// Right-click: replace the native menu with your own
box.addEventListener('contextmenu', (e) => {
    e.preventDefault();
    openCustomMenu(e.pageX, e.pageY);
});

Keyboard events

Use keydown for shortcuts and navigation. The modern property to read is event.key β€” a human-readable string like "Enter", "Escape", "ArrowUp", or "a". (Avoid the deprecated event.keyCode.)

document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') closeModal();

    // A "save" shortcut: Ctrl+S (or Cmd+S on Mac)
    if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        e.preventDefault();          // stop the browser's Save dialog
        saveDocument();
    }

    // Arrow-key navigation
    switch (e.key) {
        case 'ArrowUp':   moveSelection(-1); break;
        case 'ArrowDown': moveSelection(1);  break;
    }
});

πŸ’‘ input vs keydown for text

To react to what a user is typing, prefer the input event β€” it fires after the field's value actually changes, and it also catches paste, autofill, and mobile keyboards. Save keydown for shortcuts and navigation, where you need the specific key.

A practical example: live password strength

const password = document.querySelector('#password');
const meter = document.querySelector('#strength');

password.addEventListener('input', (e) => {
    const value = e.target.value;
    let score = 0;
    if (value.length >= 8) score++;
    if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score++;
    if (/[0-9]/.test(value)) score++;
    if (/[^a-zA-Z0-9]/.test(value)) score++;

    const labels = ['Too weak', 'Weak', 'Fair', 'Good', 'Strong'];
    meter.textContent = labels[score];
    meter.className = `strength-${score}`;
});

Form & Window Events

Forms and the window itself fire their own useful events. We'll go deep on forms next lesson; here's the essential vocabulary.

Form events

EventFires when…
submitThe form is submitted (attach to the <form>, not the button)
inputA field's value changes, on every keystroke
changeA field's value is committed (e.g. blur, or a select choice)
focus / blurA field gains / loses keyboard focus
const form = document.querySelector('#signup');

form.addEventListener('submit', (e) => {
    e.preventDefault();                    // don't reload the page
    const data = new FormData(form);       // gather every named field
    for (const [name, value] of data.entries()) {
        console.log(`${name}: ${value}`);
    }
    // …validate and send with fetch()
});

Window & document events

// The DOM is parsed and ready (scripts with defer already run after this)
document.addEventListener('DOMContentLoaded', () => {
    console.log('DOM ready β€” safe to query elements');
});

// A sticky header on scroll (throttle in real code β€” see the last section)
window.addEventListener('scroll', () => {
    document.querySelector('header')
        .classList.toggle('sticky', window.scrollY > 100);
});

// Warn about unsaved changes before leaving
window.addEventListener('beforeunload', (e) => {
    if (hasUnsavedChanges()) {
        e.preventDefault();
        e.returnValue = '';   // required for the browser to show its prompt
    }
});

βœ… Attach submit to the form, not the button

Listening for submit on the <form> catches every way a user submits β€” clicking the button and pressing Enter in a field. Listening for click on the button misses the keyboard path.

Custom Events & Cleanup

Making your own events

You aren't limited to built-in events. CustomEvent lets one part of your app announce that something happened and carry data along in a detail property β€” a clean way for components to talk without tightly coupling them.

// Create an event that carries a payload
const cartEvent = new CustomEvent('cart:add', {
    detail: { productId: 42, quantity: 2 },
    bubbles: true          // let it travel up the DOM like a native event
});

// Dispatch it from any element
document.querySelector('#add-to-cart').dispatchEvent(cartEvent);

// Anywhere else, listen for it
document.addEventListener('cart:add', (e) => {
    console.log(`Added ${e.detail.quantity} of product ${e.detail.productId}`);
    updateCartBadge();
});

Removing listeners & avoiding memory leaks

Every listener holds a reference to its handler and its element. If you add listeners for a component and never remove them, the browser can't garbage-collect that memory β€” a classic leak in long-lived apps. To remove a listener you must pass the same function reference you added (so anonymous inline functions can't be removed).

function handleResize() {
    console.log('Window is', window.innerWidth, 'px wide');
}

// Add with a named reference…
window.addEventListener('resize', handleResize);

// …then remove the exact same reference later:
window.removeEventListener('resize', handleResize);

// Modern shortcut: AbortController removes MANY listeners at once
const controller = new AbortController();
button.addEventListener('click', doThing, { signal: controller.signal });
input.addEventListener('input', doOther, { signal: controller.signal });
controller.abort();   // detaches both listeners in one call

⚠️ Anonymous handlers can't be removed

el.addEventListener('click', () => {...}) creates a brand-new function every call, so there's no reference to hand to removeEventListener. If a listener needs to be removed, give the handler a name or use an AbortController signal.

Practice & Quiz

πŸ‹οΈ Exercise 1: Live character counter

Goal: Given a <textarea id="bio"> and a <span id="count">, show how many of a 200-character budget remain, and turn the counter red when fewer than 20 remain.

const bio = document.querySelector('#bio');
const count = document.querySelector('#count');
const MAX = 200;

// TODO: on every input, update #count and toggle a warning
πŸ’‘ Hint

Listen for the input event (not keydown β€” it catches paste too). The remaining count is MAX - bio.value.length. Toggle a class with count.classList.toggle('warn', remaining < 20).

βœ… Solution
bio.addEventListener('input', () => {
    const remaining = MAX - bio.value.length;
    count.textContent = `${remaining} characters left`;
    count.classList.toggle('warn', remaining < 20);
});

πŸ‹οΈ Exercise 2: Keyboard shortcut handler

Goal: Log "Saving…" on Ctrl+S (or Cmd+S) without triggering the browser's Save dialog, and log "Closing" on Escape.

βœ… Solution
document.addEventListener('keydown', (e) => {
    if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
        e.preventDefault();          // block the native Save dialog
        console.log('Saving…');
    }
    if (e.key === 'Escape') {
        console.log('Closing');
    }
});

🎯 Quick Quiz

Question 1: Why is addEventListener('click', fn) preferred over element.onclick = fn?

Question 2: Inside a click handler on a button, you click an icon inside that button. What is event.target?

Question 3: Which call stops a form's submit button from reloading the page?

Best Practices & Pitfalls

βœ… Do

  • Use addEventListener and keep JavaScript out of your HTML
  • Read event.key (not the deprecated keyCode) for keyboard events
  • Attach submit to the <form> so Enter and clicks both work
  • Throttle or debounce high-frequency events (scroll, mousemove, resize, input)
  • Remove listeners you no longer need β€” name your handlers or use AbortController

❌ Don't

  • Sprinkle onclick="..." attributes through your markup
  • Assume anonymous handlers can be removed later β€” they can't
  • Do heavy work directly inside a scroll or mousemove handler
  • Forget preventDefault() on submit handlers when you fetch instead of reload

⚠️ Throttle the firehose

// A tiny throttle: run at most once per `limit` ms
function throttle(fn, limit) {
    let waiting = false;
    return function (...args) {
        if (waiting) return;
        fn.apply(this, args);
        waiting = true;
        setTimeout(() => (waiting = false), limit);
    };
}

window.addEventListener('scroll', throttle(updateHeader, 100));

Events like scroll and mousemove can fire dozens of times per second. Doing layout work on every one janks the page β€” throttle it.

Summary

πŸŽ‰ Key Takeaways

  • An event flows from user action β†’ fired event β†’ listener β†’ handler β†’ response
  • Use addEventListener; it supports multiple handlers, options, and removal
  • The event object tells you the type, target, position, and keys β€” and lets you call preventDefault()
  • event.target is where it happened; event.currentTarget is where the listener lives
  • Prefer input for typing, submit on the form, and event.key for the keyboard
  • Clean up listeners β€” name handlers or use AbortController β€” to avoid memory leaks

πŸ“š Additional Resources

πŸš€ What's Next?

You can now attach a handler to any element. But events don't stay put β€” they travel through the DOM in phases. Next up, Event Propagation and Delegation, where you'll learn to handle events for hundreds of elements with a single listener.

πŸŽ‰ Great work!

Your pages can now respond to the people using them. That's the heart of front-end development.