⚡ Events and Event Handling

Responding to User Interactions

What are Events?

Events are actions or occurrences that happen in the browser, such as clicking, typing, or loading a page. JavaScript can "listen" for these events and execute code in response.

📝 Adding Event Listeners

addEventListener Method

// Basic syntax: element.addEventListener(event, callback, options)
let button = document.querySelector('#myButton');

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

// Arrow function (no own 'this')
button.addEventListener('click', () => {
    console.log('Clicked with arrow function');
});

// Named function (reusable, can be removed)
function handleClick() {
    console.log('Handled by named function');
}
button.addEventListener('click', handleClick);

// Multiple listeners on same event
button.addEventListener('click', function() {
    console.log('First listener');
});
button.addEventListener('click', function() {
    console.log('Second listener');
});

// Different events on same element
button.addEventListener('click', () => console.log('Clicked'));
button.addEventListener('mouseenter', () => console.log('Mouse entered'));
button.addEventListener('mouseleave', () => console.log('Mouse left'));

Removing Event Listeners

// Must use named function to remove
function handleClick() {
    console.log('Clicked');
}

let button = document.querySelector('button');
button.addEventListener('click', handleClick);

// Remove later
button.removeEventListener('click', handleClick);

// Can't remove anonymous functions
button.addEventListener('click', function() {
    console.log('Cannot remove this');
});

// One-time listeners (ES2015)
button.addEventListener('click', handleClick, { once: true });
// Automatically removed after first trigger

Old Methods (avoid)

// onclick property (overwrites previous handlers)
button.onclick = function() {
    console.log('Clicked');
};

// This overwrites the above
button.onclick = function() {
    console.log('New handler');
};

// Inline HTML (bad practice)
// 

// Why avoid: Only one handler per event, mixes HTML and JavaScript

🎯 Common Event Types

Mouse Events

let element = document.querySelector('.box');

// Click events
element.addEventListener('click', e => console.log('Clicked'));
element.addEventListener('dblclick', e => console.log('Double clicked'));
element.addEventListener('contextmenu', e => console.log('Right clicked'));

// Mouse movement
element.addEventListener('mouseenter', e => console.log('Mouse entered'));
element.addEventListener('mouseleave', e => console.log('Mouse left'));
element.addEventListener('mouseover', e => console.log('Mouse over (bubbles)'));
element.addEventListener('mouseout', e => console.log('Mouse out (bubbles)'));
element.addEventListener('mousemove', e => {
    console.log(`Position: ${e.clientX}, ${e.clientY}`);
});

// Mouse buttons
element.addEventListener('mousedown', e => console.log('Button pressed'));
element.addEventListener('mouseup', e => console.log('Button released'));

Keyboard Events

let input = document.querySelector('input');

// Keyboard events
input.addEventListener('keydown', e => {
    console.log('Key pressed:', e.key);
    console.log('Key code:', e.code);
});

input.addEventListener('keyup', e => {
    console.log('Key released:', e.key);
});

input.addEventListener('keypress', e => {
    // Deprecated, use keydown instead
    console.log('Key pressed (old)');
});

// Check modifier keys
document.addEventListener('keydown', e => {
    if (e.ctrlKey && e.key === 's') {
        e.preventDefault();  // Prevent browser save
        console.log('Ctrl+S pressed');
    }
    
    if (e.shiftKey) console.log('Shift held');
    if (e.altKey) console.log('Alt held');
    if (e.metaKey) console.log('Cmd/Win held');
});

// Specific keys
document.addEventListener('keydown', e => {
    if (e.key === 'Enter') console.log('Enter pressed');
    if (e.key === 'Escape') console.log('Escape pressed');
    if (e.key === 'ArrowUp') console.log('Up arrow');
    if (e.code === 'Space') console.log('Spacebar');
});

Form Events

let form = document.querySelector('form');
let input = document.querySelector('input');

// Submit event (on form, not button)
form.addEventListener('submit', e => {
    e.preventDefault();  // Prevent page reload
    console.log('Form submitted');
    
    // Get form data
    let formData = new FormData(form);
    for (let [key, value] of formData) {
        console.log(key, value);
    }
});

// Input events
input.addEventListener('input', e => {
    // Fires on every change
    console.log('Current value:', e.target.value);
});

input.addEventListener('change', e => {
    // Fires when element loses focus after change
    console.log('Changed to:', e.target.value);
});

input.addEventListener('focus', e => {
    console.log('Input focused');
    e.target.classList.add('focused');
});

input.addEventListener('blur', e => {
    console.log('Input lost focus');
    e.target.classList.remove('focused');
});

// Select event (for text selection)
input.addEventListener('select', e => {
    console.log('Text selected');
});

Page Events

// Page loaded (DOM ready)
document.addEventListener('DOMContentLoaded', () => {
    console.log('DOM fully loaded');
    // Initialize your app here
});

// Everything loaded (images, styles, etc.)
window.addEventListener('load', () => {
    console.log('Page fully loaded');
});

// Before unload (user leaving)
window.addEventListener('beforeunload', e => {
    // Show confirmation dialog
    e.preventDefault();
    e.returnValue = '';  // Chrome requires this
});

// Page unloaded
window.addEventListener('unload', () => {
    console.log('Page unloading');
});

// Window resized
window.addEventListener('resize', () => {
    console.log('Window size:', window.innerWidth, window.innerHeight);
});

// Page scrolled
window.addEventListener('scroll', () => {
    console.log('Scroll position:', window.scrollY);
});

📦 The Event Object

element.addEventListener('click', event => {
    // Event properties
    console.log(event.type);           // 'click'
    console.log(event.target);         // Element that triggered event
    console.log(event.currentTarget);  // Element listener is attached to
    console.log(event.timeStamp);      // When event occurred
    
    // Mouse events
    console.log(event.clientX);        // X relative to viewport
    console.log(event.clientY);        // Y relative to viewport
    console.log(event.pageX);          // X relative to document
    console.log(event.pageY);          // Y relative to document
    console.log(event.screenX);        // X relative to screen
    console.log(event.screenY);        // Y relative to screen
    console.log(event.button);         // Mouse button (0=left, 1=middle, 2=right)
    
    // Keyboard events
    console.log(event.key);            // Key value ('a', 'Enter', 'Shift')
    console.log(event.code);           // Physical key ('KeyA', 'Enter')
    console.log(event.ctrlKey);        // Ctrl held?
    console.log(event.shiftKey);       // Shift held?
    console.log(event.altKey);         // Alt held?
    console.log(event.metaKey);        // Cmd/Win held?
    
    // Form events
    console.log(event.target.value);   // Input value
    console.log(event.target.checked); // Checkbox state
});

// target vs currentTarget
document.querySelector('.container').addEventListener('click', e => {
    console.log(e.target);         // Element clicked (might be child)
    console.log(e.currentTarget);  // .container (listener attached here)
});

🎈 Event Bubbling and Capturing

Event Propagation

// HTML: 
// Bubbling (default): event travels from target up to root document.querySelector('button').addEventListener('click', () => { console.log('Button clicked'); }); document.querySelector('.inner').addEventListener('click', () => { console.log('Inner clicked'); }); document.querySelector('.outer').addEventListener('click', () => { console.log('Outer clicked'); }); // Click button logs: // "Button clicked" // "Inner clicked" // "Outer clicked" // Capturing: event travels from root down to target document.querySelector('.outer').addEventListener('click', () => { console.log('Outer (capture)'); }, true); // true enables capture phase document.querySelector('button').addEventListener('click', () => { console.log('Button'); }); // Click button logs: // "Outer (capture)" // "Button" // "Inner clicked" // "Outer clicked"

Stopping Propagation

// stopPropagation - prevent bubbling/capturing
button.addEventListener('click', e => {
    e.stopPropagation();
    console.log('Button clicked');
    // Parent listeners won't fire
});

// stopImmediatePropagation - also stops other listeners on same element
button.addEventListener('click', e => {
    e.stopImmediatePropagation();
    console.log('First listener');
});

button.addEventListener('click', () => {
    console.log('Second listener');  // Won't fire
});

🛡️ preventDefault

// Prevent default behavior
let link = document.querySelector('a');
link.addEventListener('click', e => {
    e.preventDefault();  // Don't navigate
    console.log('Link clicked but not followed');
});

// Prevent form submission
form.addEventListener('submit', e => {
    e.preventDefault();
    console.log('Form not submitted');
    
    // Validate and submit manually
    if (isValid(form)) {
        submitForm(form);
    }
});

// Prevent context menu
document.addEventListener('contextmenu', e => {
    e.preventDefault();
    console.log('Right-click disabled');
});

// Prevent keyboard shortcuts
document.addEventListener('keydown', e => {
    if (e.ctrlKey && e.key === 's') {
        e.preventDefault();
        console.log('Save prevented');
    }
});

// Check if preventDefault is possible
if (!event.defaultPrevented) {
    // Default behavior hasn't been prevented yet
}

🎭 Event Delegation

// Instead of adding listener to each item
let items = document.querySelectorAll('.item');
items.forEach(item => {
    item.addEventListener('click', handleClick);
});

// Use delegation: attach to parent
let container = document.querySelector('.container');
container.addEventListener('click', e => {
    // Check if clicked element matches
    if (e.target.matches('.item')) {
        handleClick(e);
    }
    
    // Or use closest for nested elements
    let item = e.target.closest('.item');
    if (item) {
        handleClick(e, item);
    }
});

// Benefits:
// 1. Works with dynamically added elements
// 2. Better performance (one listener instead of many)
// 3. Less memory usage

// Practical example: dynamic list
let list = document.querySelector('.todo-list');

// Add listener once
list.addEventListener('click', e => {
    if (e.target.matches('.delete-btn')) {
        let item = e.target.closest('.todo-item');
        item.remove();
    }
    
    if (e.target.matches('.checkbox')) {
        let item = e.target.closest('.todo-item');
        item.classList.toggle('completed');
    }
});

// Add items dynamically (listeners work automatically)
function addTodo(text) {
    let item = document.createElement('div');
    item.className = 'todo-item';
    item.innerHTML = `
        
        ${text}
        
    `;
    list.appendChild(item);
}

⚙️ Event Options

// addEventListener options (third parameter)
element.addEventListener('click', handler, {
    capture: false,   // Use capture phase (default: false)
    once: true,       // Remove after first trigger
    passive: true     // Never calls preventDefault (improves scroll performance)
});

// Passive listeners (for scroll/touch events)
element.addEventListener('touchstart', e => {
    // Can't use preventDefault with passive: true
    console.log('Touch started');
}, { passive: true });

// Once option (ES2015)
button.addEventListener('click', () => {
    console.log('Clicked once');
    // Automatically removed
}, { once: true });

// Signal for aborting listeners
let controller = new AbortController();

element.addEventListener('click', handler, {
    signal: controller.signal
});

// Remove listener later
controller.abort();  // Removes all listeners with this signal

💡 Practical Examples

Toggle Dark Mode

let toggleBtn = document.querySelector('#darkModeToggle');

toggleBtn.addEventListener('click', () => {
    document.body.classList.toggle('dark-mode');
    
    let isDark = document.body.classList.contains('dark-mode');
    toggleBtn.textContent = isDark ? '☀️ Light Mode' : '🌙 Dark Mode';
    
    // Save preference
    localStorage.setItem('darkMode', isDark);
});

// Load preference on page load
document.addEventListener('DOMContentLoaded', () => {
    let isDark = localStorage.getItem('darkMode') === 'true';
    if (isDark) {
        document.body.classList.add('dark-mode');
    }
});

Form Validation

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

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

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

// Submit validation
form.addEventListener('submit', e => {
    e.preventDefault();
    
    let errors = [];
    
    if (!email.value) {
        errors.push('Email required');
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
        errors.push('Invalid email');
    }
    
    if (password.value.length < 8) {
        errors.push('Password must be 8+ characters');
    }
    
    if (errors.length > 0) {
        alert(errors.join('\n'));
    } else {
        console.log('Form valid, submitting...');
        form.submit();
    }
});

Debouncing Search Input

let searchInput = document.querySelector('#search');
let timeout;

searchInput.addEventListener('input', e => {
    // Clear previous timeout
    clearTimeout(timeout);
    
    // Set new timeout
    timeout = setTimeout(() => {
        performSearch(e.target.value);
    }, 500);  // Wait 500ms after typing stops
});

function performSearch(query) {
    console.log('Searching for:', query);
    // Make API call
}

Keyboard Shortcuts

let shortcuts = {
    's': () => save(),
    'n': () => createNew(),
    'o': () => open(),
    'Escape': () => closeModal()
};

document.addEventListener('keydown', e => {
    // Only if Ctrl/Cmd is held (except Escape)
    if ((e.ctrlKey || e.metaKey) || e.key === 'Escape') {
        if (shortcuts[e.key]) {
            e.preventDefault();
            shortcuts[e.key]();
        }
    }
});

function save() {
    console.log('Saving...');
}

function createNew() {
    console.log('Creating new...');
}

Drag and Drop

let draggable = document.querySelector('.draggable');
let dropZone = document.querySelector('.drop-zone');

// Make element draggable
draggable.draggable = true;

draggable.addEventListener('dragstart', e => {
    e.dataTransfer.setData('text/plain', e.target.id);
    e.target.classList.add('dragging');
});

draggable.addEventListener('dragend', e => {
    e.target.classList.remove('dragging');
});

// Drop zone
dropZone.addEventListener('dragover', e => {
    e.preventDefault();  // Allow drop
    dropZone.classList.add('drag-over');
});

dropZone.addEventListener('dragleave', () => {
    dropZone.classList.remove('drag-over');
});

dropZone.addEventListener('drop', e => {
    e.preventDefault();
    dropZone.classList.remove('drag-over');
    
    let id = e.dataTransfer.getData('text/plain');
    let element = document.getElementById(id);
    dropZone.appendChild(element);
});

Infinite Scroll

let loading = false;
let page = 1;

window.addEventListener('scroll', () => {
    // Check if near bottom
    let scrollPosition = window.scrollY + window.innerHeight;
    let pageHeight = document.documentElement.scrollHeight;
    
    if (scrollPosition > pageHeight - 100 && !loading) {
        loadMoreItems();
    }
});

async function loadMoreItems() {
    loading = true;
    console.log('Loading page', page);
    
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    // Add items
    let container = document.querySelector('.items');
    for (let i = 0; i < 10; i++) {
        let item = document.createElement('div');
        item.className = 'item';
        item.textContent = `Item ${(page - 1) * 10 + i + 1}`;
        container.appendChild(item);
    }
    
    page++;
    loading = false;
}

🎯 Key Takeaways