Learning Objectives

  • Add and remove event listeners
  • Understand event bubbling and capturing
  • Use event delegation for performance
  • Handle common events (click, input, submit)
  • Apply event handling best practices

Adding Event Listeners

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

// Modern way - addEventListener
button.addEventListener('click', (event) => {
    console.log('Button clicked!');
    console.log('Event object:', event);
});

// Can add multiple listeners
function handler1() {
    console.log('Handler 1');
}

function handler2() {
    console.log('Handler 2');
}

button.addEventListener('click', handler1);
button.addEventListener('click', handler2);

// Remove listener
button.removeEventListener('click', handler1);

// Options
button.addEventListener('click', handler, {
    once: true,      // Remove after first call
    passive: true,   // Won't call preventDefault
    capture: false   // Use bubbling phase
});

Event Object

button.addEventListener('click', (event) => {
    // Event properties
    event.target;        // Element that triggered event
    event.currentTarget; // Element with listener attached
    event.type;          // Event type ('click')
    event.timeStamp;     // When event occurred
    
    // Event methods
    event.preventDefault();  // Prevent default action
    event.stopPropagation(); // Stop event bubbling
    event.stopImmediatePropagation(); // Stop other listeners
    
    // Mouse event specific
    event.clientX;  // X coordinate
    event.clientY;  // Y coordinate
    event.button;   // Which mouse button
    
    // Keyboard event specific
    event.key;      // Key pressed
    event.code;     // Physical key code
    event.ctrlKey;  // Ctrl pressed?
    event.shiftKey; // Shift pressed?
});

Event Delegation

// Instead of adding listeners to many items
const list = document.querySelector('#todoList');

// Add one listener to parent
list.addEventListener('click', (event) => {
    // Check if clicked element matches
    if (event.target.matches('.delete-btn')) {
        const item = event.target.closest('.todo-item');
        item.remove();
    }
    
    if (event.target.matches('.todo-checkbox')) {
        const item = event.target.closest('.todo-item');
        item.classList.toggle('completed');
    }
});

// Benefits:
// 1. Better performance (one listener vs many)
// 2. Works for dynamically added items
// 3. Less memory usage

Common Events

// Click events
element.addEventListener('click', handler);
element.addEventListener('dblclick', handler);
element.addEventListener('contextmenu', handler); // Right-click

// Input events
input.addEventListener('input', handler);   // Every keystroke
input.addEventListener('change', handler);  // After blur
input.addEventListener('focus', handler);
input.addEventListener('blur', handler);

// Form events
form.addEventListener('submit', (e) => {
    e.preventDefault(); // Stop form submission
    const formData = new FormData(form);
    // Process form data
});

form.addEventListener('reset', handler);

// Keyboard events
document.addEventListener('keydown', (e) => {
    console.log('Key pressed:', e.key);
    
    if (e.key === 'Enter') {
        console.log('Enter pressed');
    }
    
    if (e.ctrlKey && e.key === 's') {
        e.preventDefault();
        console.log('Ctrl+S pressed');
    }
});

document.addEventListener('keyup', handler);
document.addEventListener('keypress', handler); // Deprecated

// Mouse events
element.addEventListener('mouseenter', handler);
element.addEventListener('mouseleave', handler);
element.addEventListener('mousemove', handler);
element.addEventListener('mousedown', handler);
element.addEventListener('mouseup', handler);

// Touch events (mobile)
element.addEventListener('touchstart', handler);
element.addEventListener('touchmove', handler);
element.addEventListener('touchend', handler);

// Window events
window.addEventListener('load', handler);     // Page fully loaded
window.addEventListener('resize', handler);   // Window resized
window.addEventListener('scroll', handler);   // Page scrolled
window.addEventListener('beforeunload', handler); // Before leaving

Event Bubbling and Capturing

// Event flow: Capturing → Target → Bubbling

// Bubbling (default) - from target up to root
document.querySelector('#child').addEventListener('click', () => {
    console.log('1. Child clicked');
});

document.querySelector('#parent').addEventListener('click', () => {
    console.log('2. Parent clicked');
});

document.body.addEventListener('click', () => {
    console.log('3. Body clicked');
});

// Clicking child logs: 1, 2, 3

// Stop bubbling
child.addEventListener('click', (e) => {
    e.stopPropagation();
    console.log('Only child fires');
});

// Capturing phase - from root down to target
parent.addEventListener('click', () => {
    console.log('Parent (capturing)');
}, true); // true = use capturing

Real-World Examples

Form Validation

const form = document.querySelector('#signupForm');
const email = document.querySelector('#email');
const password = document.querySelector('#password');

// Real-time validation
email.addEventListener('input', (e) => {
    const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.target.value);
    e.target.classList.toggle('invalid', !isValid);
});

password.addEventListener('input', (e) => {
    const isValid = e.target.value.length >= 8;
    e.target.classList.toggle('invalid', !isValid);
});

// Form submission
form.addEventListener('submit', (e) => {
    e.preventDefault();
    
    const formData = new FormData(form);
    const data = Object.fromEntries(formData);
    
    console.log('Submitting:', data);
    // Send to server
});

Debouncing Search

function debounce(fn, delay) {
    let timeout;
    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => fn.apply(this, args), delay);
    };
}

const searchInput = document.querySelector('#search');

const performSearch = debounce((query) => {
    console.log('Searching for:', query);
    // API call here
}, 300);

searchInput.addEventListener('input', (e) => {
    performSearch(e.target.value);
});

Best Practices

1. Use event delegation for lists
// Good - one listener
list.addEventListener('click', (e) => {
    if (e.target.matches('.item')) {
        // Handle
    }
});

// Bad - many listeners
items.forEach(item => {
    item.addEventListener('click', handler);
});
2. Remove listeners to prevent memory leaks
const handler = () => console.log('clicked');

element.addEventListener('click', handler);

// Later, when done
element.removeEventListener('click', handler);
3. Debounce expensive operations
window.addEventListener('resize', debounce(() => {
    // Expensive layout calculations
}, 250));

Key Takeaways