Event Handling

Week 4: Web Fundamentals - Thursday Morning Session

Understanding Events: The Heartbeat of Interactive Web Applications

Welcome to our exploration of JavaScript event handling! Now that we've learned how to select and manipulate DOM elements, we're ready to make our web pages truly interactive by responding to user actions.

This lecture belongs to Week 4: Web Fundamentals and is part of our Python Full Stack Developer course. You'll find this content in the folder structure: /04week/04week_4day_d.html

Events are at the core of interactive web applications. They allow our code to listen for and respond to actions like clicks, keypresses, form submissions, and even system events like page loading. Without events, our web pages would be static documents with no way to interact with users.

What Are Events?

In JavaScript, events are signals that something has happened in the browser. These signals can come from various sources:

Postal Service Metaphor: Think of events as a postal service for your web page. Different parts of the page can "mail" notifications when something happens, and your JavaScript code can "subscribe" to receive these notifications at specific addresses (event listeners). When an event occurs, all subscribers get notified and can respond accordingly.

Anatomy of an Event

Events are represented by event objects that contain information about what happened:

  • Type: What kind of event occurred (click, keypress, etc.)
  • Target: The element that triggered the event
  • Timestamp: When the event occurred
  • Additional Properties: Event-specific details (cursor position, key pressed, etc.)
  • Methods: Functions to control the event's behavior

Common Types of Events

JavaScript can handle a wide variety of events. Here are some of the most commonly used categories:

Mouse Events

  • click: When an element is clicked
  • dblclick: When an element is double-clicked
  • mousedown: When a mouse button is pressed down
  • mouseup: When a mouse button is released
  • mousemove: When the mouse cursor moves
  • mouseover: When the cursor enters an element
  • mouseout: When the cursor leaves an element
  • mouseenter: Like mouseover, but doesn't bubble
  • mouseleave: Like mouseout, but doesn't bubble
// Example of handling a click event
document.getElementById('my-button').addEventListener('click', function(event) {
    console.log('Button was clicked!');
    console.log('Mouse position:', event.clientX, event.clientY);
});

Keyboard Events

  • keydown: When a key is pressed down
  • keyup: When a key is released
  • keypress: When a key is pressed (character keys only)
// Example of handling a keydown event
document.addEventListener('keydown', function(event) {
    console.log('Key pressed:', event.key);
    console.log('Key code:', event.keyCode); // Deprecated but still used
    console.log('Modifier keys:', event.ctrlKey, event.shiftKey, event.altKey);
});

Form Events

  • submit: When a form is submitted
  • change: When an input's value changes (after losing focus)
  • input: When an input's value changes (immediately)
  • focus: When an element receives focus
  • blur: When an element loses focus
  • reset: When a form is reset
// Example of handling a form submission
document.getElementById('login-form').addEventListener('submit', function(event) {
    event.preventDefault(); // Stop the form from actually submitting
    
    const username = document.getElementById('username').value;
    const password = document.getElementById('password').value;
    
    // Form validation logic
    if (username === '' || password === '') {
        alert('Please fill in all fields');
    } else {
        // AJAX form submission or other logic here
        console.log('Form submitted with:', username, password);
    }
});

Document/Window Events

  • load: When the page finishes loading
  • DOMContentLoaded: When the DOM is ready (before resources)
  • resize: When the window is resized
  • scroll: When the document or element is scrolled
  • beforeunload: Before the page is unloaded (closing or navigating)
// Example of DOM ready event
document.addEventListener('DOMContentLoaded', function() {
    console.log('DOM is fully loaded and parsed');
    // Safe to manipulate DOM elements here
});

// Example of window resize event
window.addEventListener('resize', function() {
    console.log('Window dimensions:', window.innerWidth, window.innerHeight);
});

Touch Events

  • touchstart: When a touch point is placed on the screen
  • touchend: When a touch point is removed from the screen
  • touchmove: When a touch point is moved along the screen
  • touchcancel: When a touch point has been disrupted
// Example of handling a touch event
document.getElementById('touch-area').addEventListener('touchstart', function(event) {
    console.log('Touch started!');
    console.log('Number of touch points:', event.touches.length);
    
    // Prevent scrolling/zooming
    event.preventDefault();
});

Event Listeners: Responding to Events

To respond to events, we need to "listen" for them using event listeners. JavaScript provides several ways to attach event listeners to elements:

Method 1: addEventListener (Recommended)

The modern and most flexible way to handle events:

// Basic syntax
element.addEventListener(eventType, handlerFunction, options);

// Example
const button = document.getElementById('submit-button');

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

// Using a named function
function handleClick(event) {
    console.log('Button clicked!');
    console.log('Event object:', event);
}

button.addEventListener('click', handleClick);

// With options (third parameter)
button.addEventListener('click', handleClick, {
    once: true,           // Only trigger once
    capture: false,       // Use bubbling phase (default)
    passive: true         // Promise not to call preventDefault()
});

Benefits of addEventListener:

  • Allows multiple listeners for the same event
  • Can remove specific listeners later
  • Provides additional options
  • Doesn't override existing listeners
  • Clear separation between HTML and JavaScript

Method 2: Event Handler Properties

A simpler but more limited approach:

// Syntax
element.oneventname = handlerFunction;

// Example
const button = document.getElementById('submit-button');

button.onclick = function(event) {
    console.log('Button clicked!');
};

// Using a named function
function handleClick(event) {
    console.log('Button clicked!');
}

button.onclick = handleClick;

// This will OVERWRITE the previous handler!
button.onclick = function() {
    console.log('New handler - the previous one is gone');
};

Limitations:

  • Only one handler per event type per element
  • Can't use capture phase
  • Less control over how events are handled

Method 3: Inline Event Handlers (Not Recommended)

The oldest method, written directly in HTML:

<!-- HTML with inline event handler -->
<button onclick="handleClick(event)">Click Me</button>

<!-- Or with direct code -->
<button onclick="alert('Button clicked!')">Click Me</button>

// JavaScript
function handleClick(event) {
    console.log('Button clicked!');
}

Why to avoid this approach:

  • Mixes HTML and JavaScript (poor separation of concerns)
  • Performance issues with complex handlers
  • Limited access to the event object
  • Difficult to manage and maintain
  • Can lead to security risks like XSS

Removing Event Listeners

For events that should only happen under certain conditions, you may need to remove event listeners:

// Adding a listener
function handleClick(event) {
    console.log('Button clicked!');
}

button.addEventListener('click', handleClick);

// Removing the listener - MUST use the same function reference
button.removeEventListener('click', handleClick);

// This WON'T work (anonymous function)
button.addEventListener('click', function() {
    console.log('Click handled');
});

// This attempt to remove will fail because it's a different function reference
button.removeEventListener('click', function() {
    console.log('Click handled');
});

Tip: Always store function references in variables if you plan to remove them later.

// Correct approach for removing listeners
const handleHover = function() {
    console.log('Element hovered');
};

// Add the listener
element.addEventListener('mouseover', handleHover);

// Later, remove the same listener
element.removeEventListener('mouseover', handleHover);

The Event Object: Understanding What Happened

When an event occurs, JavaScript passes an event object to your handler function. This object contains details about the event that can be extremely useful:

Common Event Object Properties

  • event.type: The type of event (e.g., "click", "keydown")
  • event.target: The element that triggered the event
  • event.currentTarget: The element that the listener is attached to
  • event.timeStamp: When the event occurred
  • event.bubbles: Whether the event bubbles up through the DOM
  • event.cancelable: Whether the event can be canceled
document.getElementById('my-button').addEventListener('click', function(event) {
    console.log('Event type:', event.type);
    console.log('Target element:', event.target);
    console.log('Current target:', event.currentTarget);
    console.log('Time of event:', event.timeStamp);
});

Event-Specific Properties

Different event types provide additional information relevant to that event:

Mouse Events:
  • event.clientX/clientY: Coordinates relative to the viewport
  • event.pageX/pageY: Coordinates relative to the document
  • event.screenX/screenY: Coordinates relative to the screen
  • event.button: Which mouse button was pressed
  • event.altKey/ctrlKey/shiftKey/metaKey: Modifier keys
Keyboard Events:
  • event.key: The key value (e.g., "a", "Enter")
  • event.code: The physical key (e.g., "KeyA", "Enter")
  • event.keyCode: Numeric key code (deprecated)
  • event.altKey/ctrlKey/shiftKey/metaKey: Modifier keys
  • event.repeat: Whether the key is being held down
Form Events:
  • event.value: Current value of the form element
  • event.checked: For checkboxes and radio buttons
  • event.selected: For select options
// Mouse event example
document.addEventListener('mousemove', function(event) {
    // Update a div showing the current mouse position
    document.getElementById('mouse-position').textContent = 
        `Mouse at: ${event.clientX}, ${event.clientY}`;
});

// Keyboard event example
document.addEventListener('keydown', function(event) {
    // Check for specific keys
    if (event.key === 'Escape') {
        closeModal();
    }
    
    // Check for keyboard shortcuts
    if (event.ctrlKey && event.key === 's') {
        event.preventDefault(); // Prevent browser's save dialog
        saveDocument();
    }
});

Important Event Methods

The event object also provides methods to control event behavior:

  • event.preventDefault(): Stops the default browser behavior
  • event.stopPropagation(): Stops the event from bubbling/capturing
  • event.stopImmediatePropagation(): Stops other listeners on the same element
// Prevent default behavior of a link
document.querySelector('a.stay-here').addEventListener('click', function(event) {
    event.preventDefault(); // Prevent navigating to the link
    console.log('Link was clicked, but page did not navigate');
});

// Prevent form submission
document.querySelector('form').addEventListener('submit', function(event) {
    if (!validateForm()) {
        event.preventDefault(); // Don't submit if validation fails
    }
});

Event Propagation: Bubbling and Capturing

When an event occurs on an element, it doesn't just fire there. Events in JavaScript propagate through the DOM in two phases:

  1. Capturing Phase: The event travels down from the document root to the target element
  2. Bubbling Phase: The event bubbles up from the target back to the document root

Ripple Metaphor: Think of event propagation like dropping a stone into a pond. The ripples first travel from the edge of the pond inward (capturing phase), then from the point of impact outward (bubbling phase). Each element the ripple passes can detect and respond to it.

Visual Representation of Event Propagation

                    CAPTURING PHASE (1)       BUBBLING PHASE (2)
                          ↓                          ↑
                    +--------------+           +--------------+
                    |   document   |           |   document   |
                    +--------------+           +--------------+
                          ↓                          ↑
                    +--------------+           +--------------+
                    |     html     |           |     html     |
                    +--------------+           +--------------+
                          ↓                          ↑
                    +--------------+           +--------------+
                    |     body     |           |     body     |
                    +--------------+           +--------------+
                          ↓                          ↑
                    +--------------+           +--------------+
                    |     div      |           |     div      |
                    +--------------+           +--------------+
                          ↓                          ↑
                    +--------------+           +--------------+
                    |    button    |           |    button    |
                    +--------------+           +--------------+
                          ↓                          ↑
                    +======EVENT======+      +======EVENT======+
                

Example of Event Bubbling

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

// Add click listeners to all elements
document.getElementById('outer').addEventListener('click', function() {
    console.log('Outer div clicked');
});

document.getElementById('middle').addEventListener('click', function() {
    console.log('Middle div clicked');
});

document.getElementById('inner').addEventListener('click', function() {
    console.log('Button clicked');
});

// When the button is clicked, the console will show:
// "Button clicked"
// "Middle div clicked"
// "Outer div clicked"
// This is because the event bubbles up from the target element.

Using the Capturing Phase

By default, event listeners are triggered during the bubbling phase. To use the capturing phase instead, set the third parameter of addEventListener to true or {capture: true}:

// Same HTML structure as above

// Register a listener for the capturing phase
document.getElementById('outer').addEventListener('click', function() {
    console.log('Outer div clicked - CAPTURE');
}, true); // true enables capturing phase

document.getElementById('middle').addEventListener('click', function() {
    console.log('Middle div clicked - CAPTURE');
}, true);

document.getElementById('inner').addEventListener('click', function() {
    console.log('Button clicked - CAPTURE');
}, true);

// When the button is clicked, the console will show:
// "Outer div clicked - CAPTURE"
// "Middle div clicked - CAPTURE"
// "Button clicked - CAPTURE"
// This is the reverse order from bubbling

Stopping Event Propagation

Sometimes you want to prevent an event from continuing its journey through the DOM:

// Prevent event bubbling
document.getElementById('middle').addEventListener('click', function(event) {
    console.log('Middle div clicked');
    event.stopPropagation(); // Event stops here, won't reach outer div
});

// Stop all handlers on this element
document.getElementById('inner').addEventListener('click', function(event) {
    console.log('First button handler');
    event.stopImmediatePropagation(); // Prevents other handlers on this element
});

// This handler won't run if the button is clicked
document.getElementById('inner').addEventListener('click', function() {
    console.log('Second button handler - this will not run');
});

Important Note: Be careful with stopPropagation(). It can break functionality that relies on event bubbling, like analytics tracking or delegate event handlers. Use it only when necessary.

Event Delegation: Efficient Event Handling

Event delegation is a technique that leverages event bubbling to handle events efficiently. Instead of attaching event listeners to many similar elements, you attach a single listener to a common ancestor.

Management Metaphor: Think of event delegation like a corporate management structure. Instead of the CEO (your JavaScript) trying to directly manage every individual employee (element), they delegate responsibility to department managers (parent elements). When employees have concerns (events), they report to their manager, who can handle common issues and only escalate unique situations to the CEO.

Benefits of Event Delegation

  • Memory Efficiency: Fewer event listeners = less memory use
  • Dynamic Elements: Works with elements added to the DOM after page load
  • Less Code: Simpler, more maintainable code
  • Improved Performance: Especially with many similar elements

Traditional Approach vs. Event Delegation

// HTML:
// <ul id="task-list">
//   <li><span>Task 1</span> <button class="delete">Delete</button></li>
//   <li><span>Task 2</span> <button class="delete">Delete</button></li>
//   <li><span>Task 3</span> <button class="delete">Delete</button></li>
// </ul>

// TRADITIONAL APPROACH: Add a listener to each button
const deleteButtons = document.querySelectorAll('#task-list .delete');
deleteButtons.forEach(button => {
    button.addEventListener('click', function() {
        this.parentElement.remove();
    });
});

// Problems with this approach:
// 1. New items added dynamically won't have listeners
// 2. Many event listeners in memory
// 3. Need to update code if structure changes

// EVENT DELEGATION APPROACH: One listener on the parent
document.getElementById('task-list').addEventListener('click', function(event) {
    // Check if the clicked element is a delete button
    if (event.target.classList.contains('delete')) {
        // Find the li parent and remove it
        event.target.closest('li').remove();
    }
});

Implementing Event Delegation

To implement event delegation effectively:

  1. Identify a common parent element for all target elements
  2. Add a single event listener to that parent
  3. Use event.target to determine which specific element was interacted with
  4. Check if the target (or one of its ancestors) matches your selector
  5. Perform the appropriate action
// HTML:
// <div id="products">
//   <div class="product" data-id="1">
//     <h3>Product 1</h3>
//     <button class="view">View</button>
//     <button class="add-to-cart">Add to Cart</button>
//     <button class="favorite">Favorite</button>
//   </div>
//   <div class="product" data-id="2">...</div>
//   <div class="product" data-id="3">...</div>
// </div>

document.getElementById('products').addEventListener('click', function(event) {
    // Find the product container
    const product = event.target.closest('.product');
    
    // If click wasn't within a product, exit early
    if (!product) return;
    
    // Get the product ID
    const productId = product.dataset.id;
    
    // Determine which button was clicked
    if (event.target.classList.contains('view')) {
        viewProductDetails(productId);
    } 
    else if (event.target.classList.contains('add-to-cart')) {
        addProductToCart(productId);
    } 
    else if (event.target.classList.contains('favorite')) {
        toggleFavoriteStatus(productId);
    }
});

function viewProductDetails(id) {
    console.log(`Viewing details for product ${id}`);
    // Navigate to product page or show modal
}

function addProductToCart(id) {
    console.log(`Adding product ${id} to cart`);
    // Add to cart logic
}

function toggleFavoriteStatus(id) {
    console.log(`Toggling favorite status for product ${id}`);
    // Update favorite status
}

The closest() method is particularly useful in event delegation. It traverses up from the target element to find the first ancestor that matches the selector, or returns null if none is found.

Custom Events: Creating Your Own Events

JavaScript allows you to create and dispatch your own custom events, which can be useful for building component-based applications:

Creating and Dispatching Custom Events

// Basic custom event
const simpleEvent = new Event('myCustomEvent');

// Dispatch the event
document.getElementById('myElement').dispatchEvent(simpleEvent);

// Custom event with additional data
const detailedEvent = new CustomEvent('productAdded', {
    bubbles: true, // Allow event to bubble up the DOM
    cancelable: true, // Allow event to be canceled
    detail: { // Custom data to pass with the event
        productId: 123,
        productName: 'Wireless Headphones',
        price: 79.99
    }
});

// Dispatch the detailed event
document.dispatchEvent(detailedEvent);

// Listen for the custom event
document.addEventListener('productAdded', function(event) {
    console.log('Product added!', event.detail);
    updateCartCount(event.detail.price);
});

Practical Example: Component Communication

Custom events can be used to create a publish-subscribe pattern between components:

// Shopping cart component
const ShoppingCart = {
    items: [],
    
    addItem: function(product) {
        this.items.push(product);
        
        // Announce that the cart has changed
        const event = new CustomEvent('cartUpdated', {
            bubbles: true,
            detail: {
                itemCount: this.items.length,
                totalPrice: this.calculateTotal(),
                lastItemAdded: product
            }
        });
        
        document.dispatchEvent(event);
    },
    
    calculateTotal: function() {
        return this.items.reduce((total, item) => total + item.price, 0);
    }
};

// Cart summary display component
document.addEventListener('DOMContentLoaded', function() {
    // Listen for cart updates
    document.addEventListener('cartUpdated', function(event) {
        // Update the cart display
        document.getElementById('cart-count').textContent = event.detail.itemCount;
        document.getElementById('cart-total').textContent = `$${event.detail.totalPrice.toFixed(2)}`;
        
        // Show notification about the added item
        showNotification(`Added: ${event.detail.lastItemAdded.name}`);
    });
});

// Product listing component
document.getElementById('product-list').addEventListener('click', function(event) {
    if (event.target.classList.contains('add-to-cart')) {
        const productElement = event.target.closest('.product');
        const product = {
            id: productElement.dataset.id,
            name: productElement.dataset.name,
            price: parseFloat(productElement.dataset.price)
        };
        
        ShoppingCart.addItem(product);
    }
});

Component Communication Metaphor: Custom events are like a bulletin board system where different parts of your application can post notices (dispatch events) and interested parties can subscribe to those notices (add event listeners). Components don't need to know about each other directly; they just need to agree on the notice format.

Optimizing Events: Debouncing and Throttling

Some events like scroll, resize, and mousemove can fire many times per second. Processing each event can be expensive and lead to performance issues. Debouncing and throttling are techniques to limit how often your event handlers run.

Debouncing

Debouncing ensures that a function is only executed after a certain amount of time has passed since it was last invoked. Think of it like waiting until someone stops typing before processing their search query.

// Basic debounce function
function debounce(func, delay) {
    let timeoutId;
    
    return function(...args) {
        // Clear previous timeout
        clearTimeout(timeoutId);
        
        // Set new timeout
        timeoutId = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// Example: Search input that updates results only after typing stops
const searchInput = document.getElementById('search-input');

const debouncedSearch = debounce(function(e) {
    console.log('Searching for:', e.target.value);
    fetchSearchResults(e.target.value);
}, 300); // Wait 300ms after typing stops

searchInput.addEventListener('input', debouncedSearch);

Elevator Metaphor: Debouncing is like an elevator that waits until no new people have entered for a few seconds before starting its journey. If someone new enters during the waiting period, the timer resets.

Throttling

Throttling limits how often a function can be called over time. It ensures the function executes at a regular interval, regardless of how many times the event is triggered.

// Basic throttle function
function throttle(func, limit) {
    let inThrottle = false;
    
    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            
            setTimeout(() => {
                inThrottle = false;
            }, limit);
        }
    };
}

// Example: Limiting how often we respond to scroll events
const handleScroll = throttle(function() {
    console.log('Scroll position:', window.scrollY);
    checkElementsInViewport();
}, 100); // Execute at most every 100ms

window.addEventListener('scroll', handleScroll);

Highway Toll Metaphor: Throttling is like a highway toll booth that only allows one car through every few seconds, no matter how many cars are waiting. Unlike debouncing, which waits for a quiet period, throttling ensures regular processing at a sustainable rate.

When to Use Each Technique

  • Use Debouncing When:
    • The final state is what matters (search inputs, form validation)
    • You want to wait for a "quiet period" before acting
    • Processing during intermediate states would be wasteful
  • Use Throttling When:
    • Regular updates are needed (infinite scroll, progress updates)
    • You need to ensure some feedback during continuous events
    • You want to limit the rate of potentially expensive operations

Event Handling Best Practices

Performance Considerations

  • Use Event Delegation for groups of similar elements
  • Remove Unnecessary Listeners when they're no longer needed
  • Debounce/Throttle handlers for frequently firing events
  • Keep Handlers Small and focused on their specific task
  • Avoid Anonymous Functions if you need to remove listeners later
  • Be Careful with this Context when using arrow functions

Accessibility Considerations

  • Use Semantic Elements with built-in event handling (buttons, links)
  • Add Keyboard Support alongside mouse/touch events
  • Don't Rely Solely on Hover events (mobile has no hover)
  • Respect User Preferences like reduced motion
  • Ensure Focus Management for custom UI components
// Bad: Div acting as a button with only click handling
<div onclick="submitForm()" class="button-like">Submit</div>

// Good: Using a button element with proper keyboard accessibility
<button type="button" onclick="submitForm()">Submit</button>

// Even better: Separate JavaScript from HTML
<button type="button" id="submit-button">Submit</button>

// JavaScript
document.getElementById('submit-button').addEventListener('click', submitForm);

Code Organization and Maintainability

  • Separate Logic from Handlers - event handlers should call functions, not contain complex logic
  • Namespace Your Events especially when creating custom events
  • Document Your Events including what data they contain
  • Use Consistent Patterns across your application
// Poor organization - logic inside event handler
button.addEventListener('click', function(event) {
    // Complex logic here...
    const userData = fetchUserData();
    
    if (userData.isLoggedIn) {
        if (userData.hasPurchased) {
            showPremiumContent();
        } else {
            if (userData.freeTrialsRemaining > 0) {
                offerFreeTrial();
            } else {
                showPurchaseDialog();
            }
        }
    } else {
        redirectToLogin();
    }
});

// Better organization - separate logic from handler
button.addEventListener('click', handleAccessRequest);

function handleAccessRequest(event) {
    const userData = fetchUserData();
    
    if (!userData.isLoggedIn) {
        return redirectToLogin();
    }
    
    if (userData.hasPurchased) {
        return showPremiumContent();
    }
    
    userData.freeTrialsRemaining > 0 ? offerFreeTrial() : showPurchaseDialog();
}

Browser Compatibility Considerations

While modern browsers have largely standardized event handling, there are still some things to be aware of:

For modern web development, you generally don't need to worry about most of these issues, but if you need to support older browsers, consider using a polyfill or feature detection library.

Using Feature Detection

// Check if addEventListener is supported
const addEvent = function(element, event, handler) {
    if (element.addEventListener) {
        element.addEventListener(event, handler, false);
    } else if (element.attachEvent) {
        element.attachEvent('on' + event, handler);
    } else {
        element['on' + event] = handler;
    }
};

// Example usage
addEvent(document.getElementById('my-button'), 'click', function() {
    console.log('Button clicked!');
});

For most modern applications, you can just use addEventListener directly, as it's supported in all current browsers.

Complete Practical Examples

Example 1: Tabbed Interface

// HTML:
// <div class="tabs">
//   <div class="tab-buttons">
//     <button class="tab-button active" data-tab="tab1">Tab 1</button>
//     <button class="tab-button" data-tab="tab2">Tab 2</button>
//     <button class="tab-button" data-tab="tab3">Tab 3</button>
//   </div>
//   <div class="tab-content">
//     <div id="tab1" class="tab-panel active">Content for Tab 1</div>
//     <div id="tab2" class="tab-panel">Content for Tab 2</div>
//     <div id="tab3" class="tab-panel">Content for Tab 3</div>
//   </div>
// </div>

// CSS (simplified):
// .tab-panel { display: none; }
// .tab-panel.active { display: block; }
// .tab-button { background: #f0f0f0; }
// .tab-button.active { background: #fff; }

// JavaScript with event delegation
document.querySelector('.tab-buttons').addEventListener('click', function(event) {
    // Check if a tab button was clicked
    if (event.target.classList.contains('tab-button')) {
        // Get the tab ID from data attribute
        const tabId = event.target.dataset.tab;
        
        // Remove active class from all buttons and panels
        document.querySelectorAll('.tab-button').forEach(button => {
            button.classList.remove('active');
        });
        
        document.querySelectorAll('.tab-panel').forEach(panel => {
            panel.classList.remove('active');
        });
        
        // Add active class to clicked button and corresponding panel
        event.target.classList.add('active');
        document.getElementById(tabId).classList.add('active');
    }
});

Example 2: Image Gallery with Keyboard Navigation

// HTML:
// <div class="gallery">
//   <div class="main-image">
//     <img id="current-image" src="image1.jpg" alt="Gallery Image 1">
//   </div>
//   <div class="thumbnails">
//     <img class="thumb active" data-src="image1.jpg" src="thumb1.jpg" alt="Thumbnail 1">
//     <img class="thumb" data-src="image2.jpg" src="thumb2.jpg" alt="Thumbnail 2">
//     <img class="thumb" data-src="image3.jpg" src="thumb3.jpg" alt="Thumbnail 3">
//     <img class="thumb" data-src="image4.jpg" src="thumb4.jpg" alt="Thumbnail 4">
//   </div>
//   <div class="controls">
//     <button id="prev">Previous</button>
//     <button id="next">Next</button>
//   </div>
// </div>

// JavaScript
document.addEventListener('DOMContentLoaded', function() {
    const currentImage = document.getElementById('current-image');
    const thumbnails = document.querySelectorAll('.thumb');
    const prevButton = document.getElementById('prev');
    const nextButton = document.getElementById('next');
    
    let currentIndex = 0;
    
    // Function to change the active image
    function setActiveImage(index) {
        // Wrap around if index is out of bounds
        if (index < 0) index = thumbnails.length - 1;
        if (index >= thumbnails.length) index = 0;
        
        // Update currentIndex
        currentIndex = index;
        
        // Update main image
        const newSrc = thumbnails[index].dataset.src;
        currentImage.src = newSrc;
        currentImage.alt = thumbnails[index].alt;
        
        // Update active thumbnail
        thumbnails.forEach(thumb => thumb.classList.remove('active'));
        thumbnails[index].classList.add('active');
    }
    
    // Thumbnail click event using event delegation
    document.querySelector('.thumbnails').addEventListener('click', function(event) {
        if (event.target.classList.contains('thumb')) {
            const clickedIndex = Array.from(thumbnails).indexOf(event.target);
            setActiveImage(clickedIndex);
        }
    });
    
    // Previous button click
    prevButton.addEventListener('click', function() {
        setActiveImage(currentIndex - 1);
    });
    
    // Next button click
    nextButton.addEventListener('click', function() {
        setActiveImage(currentIndex + 1);
    });
    
    // Keyboard navigation
    document.addEventListener('keydown', function(event) {
        switch (event.key) {
            case 'ArrowLeft':
                setActiveImage(currentIndex - 1);
                break;
            case 'ArrowRight':
                setActiveImage(currentIndex + 1);
                break;
        }
    });
});

Example 3: Drag and Drop Interface

// HTML:
// <div class="kanban-board">
//   <div id="todo" class="column">
//     <h3>To Do</h3>
//     <div class="task" draggable="true" data-id="task1">Task 1</div>
//     <div class="task" draggable="true" data-id="task2">Task 2</div>
//   </div>
//   <div id="doing" class="column">
//     <h3>Doing</h3>
//     <div class="task" draggable="true" data-id="task3">Task 3</div>
//   </div>
//   <div id="done" class="column">
//     <h3>Done</h3>
//   </div>
// </div>

document.addEventListener('DOMContentLoaded', function() {
    // Get all draggable tasks
    const tasks = document.querySelectorAll('.task');
    const columns = document.querySelectorAll('.column');
    
    let draggedTask = null;
    
    // Add event listeners to tasks
    tasks.forEach(task => {
        // When drag starts
        task.addEventListener('dragstart', function(event) {
            draggedTask = task;
            event.dataTransfer.setData('text/plain', task.dataset.id);
            
            // Add a class for styling
            setTimeout(() => {
                task.classList.add('dragging');
            }, 0);
        });
        
        // When drag ends
        task.addEventListener('dragend', function() {
            draggedTask = null;
            task.classList.remove('dragging');
        });
    });
    
    // Add event listeners to columns
    columns.forEach(column => {
        // When dragged task enters a column
        column.addEventListener('dragenter', function(event) {
            event.preventDefault(); // Needed for drop to work
            column.classList.add('drag-over');
        });
        
        // When dragged task is over a column
        column.addEventListener('dragover', function(event) {
            event.preventDefault(); // Needed for drop to work
        });
        
        // When dragged task leaves a column
        column.addEventListener('dragleave', function() {
            column.classList.remove('drag-over');
        });
        
        // When task is dropped in a column
        column.addEventListener('drop', function(event) {
            event.preventDefault();
            column.classList.remove('drag-over');
            
            // Get the dragged task ID
            const taskId = event.dataTransfer.getData('text/plain');
            const task = document.querySelector(`.task[data-id="${taskId}"]`);
            
            // Append the task to the column
            column.appendChild(task);
            
            // Create a custom event to notify about task movement
            const taskMovedEvent = new CustomEvent('taskMoved', {
                bubbles: true,
                detail: {
                    taskId: taskId,
                    fromColumn: task.parentElement.id,
                    toColumn: column.id
                }
            });
            
            task.dispatchEvent(taskMovedEvent);
        });
    });
    
    // Listen for task movement
    document.addEventListener('taskMoved', function(event) {
        console.log(`Task ${event.detail.taskId} moved from ${event.detail.fromColumn} to ${event.detail.toColumn}`);
        
        // Here you would typically update your data model or send to server
        updateTaskStatus(event.detail.taskId, event.detail.toColumn);
    });
    
    function updateTaskStatus(taskId, newStatus) {
        // Example: send to server
        console.log(`Updating task ${taskId} status to ${newStatus}`);
        
        // In a real app, you might use fetch:
        // fetch('/api/tasks/' + taskId, {
        //   method: 'PATCH',
        //   body: JSON.stringify({ status: newStatus }),
        //   headers: { 'Content-Type': 'application/json' }
        // });
    }
});

Practice Exercises

Exercise 1: Event Delegation Todo List

Create a simple todo list application that allows users to:

  • Add new tasks through a form
  • Mark tasks as complete by clicking on them
  • Delete tasks with a delete button
  • Filter tasks (all, active, completed)

Use event delegation for all interactions and include proper keyboard accessibility.

Exercise 2: Interactive Form Validation

Create a registration form with real-time validation:

  • Validate each field as users type (email format, password strength, etc.)
  • Show validation feedback (success/error icons, messages)
  • Disable the submit button until all fields are valid
  • Use custom events to announce form state changes

Implement both debounced validation (for typing) and immediate validation (on blur).

Exercise 3: Custom Event Framework

Build a simple pub/sub (publish-subscribe) system using custom events:

  • Create functions to publish events with data
  • Allow multiple subscribers to listen for events
  • Support namespaced events (e.g., "user.login", "user.logout")
  • Build a small demo application showing components communicating

Use this framework to demonstrate decoupled communication between parts of an application.

Lecture Summary

Today we've explored the world of JavaScript event handling, the key to creating dynamic and interactive web applications. We've covered:

With these skills, you can now create rich, interactive web experiences that respond naturally to user actions and system events. In our next session, we'll explore more advanced ways to enhance your web applications with dynamic features.