Event Propagation and Delegation

Understanding How Events Flow Through the DOM

Introduction to Event Propagation

In our previous lectures, we explored DOM events, handlers, and listener registration. Now we'll dive deeper into event propagation—the mechanism by which events travel through the DOM tree—and delegation, a powerful pattern that leverages propagation for efficient event handling.

Event propagation is a fundamental concept in the DOM event model. When an event occurs on an element, it doesn't just affect that specific element but also triggers handlers on containing elements in a specific order.

Key Concepts

Event Propagation
The process by which an event travels through the DOM tree
Event Bubbling
Events bubble up from the target element to the root of the document
Event Capturing
Events are captured down from the root to the target element
Event Target
The specific element where the event initially occurred
Event Delegation
Pattern of handling events at a higher level than the elements generating them

Real-World Analogy: The Stadium Wave

Think of event propagation like a "wave" in a sports stadium:

  • Capturing phase: The wave starts at the top row (document) and travels down toward where it was initiated (target element)
  • Target phase: The wave reaches the section where it started (the event target)
  • Bubbling phase: The wave then travels back up from the starting section to the top row

Just as a person can participate in the wave when it passes their row going down OR up, event handlers can respond during either the capturing or bubbling phase (or both).

Event Flow: Capturing and Bubbling

When an event occurs on an element, the DOM event model processes it in three phases:

graph TD A[Document Root] -->|1. Capturing Phase| B[Parent Element] B -->|1. Capturing Phase| C[Target Element] C -->|2. Target Phase| C C -->|3. Bubbling Phase| B B -->|3. Bubbling Phase| A classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px; classDef phase1 fill:#ffecec,stroke:#333,stroke-width:1px; classDef phase2 fill:#ecffec,stroke:#333,stroke-width:1px; classDef phase3 fill:#ececff,stroke:#333,stroke-width:1px; class A,B phase1; class C phase2; class C,B,A phase3;

The Three Phases

  1. Capturing Phase: The event starts at the top of the DOM (the window and document) and travels down through each ancestor element until it reaches the target element.
  2. Target Phase: The event reaches the element that triggered it (the event target).
  3. Bubbling Phase: The event bubbles back up from the target element through its ancestors to the root of the document.

Visualizing Event Propagation

Consider this HTML structure:

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

Let's attach event listeners to each element in both phases:

// Get elements
const outer = document.getElementById('outer');
const middle = document.getElementById('middle');
const inner = document.getElementById('inner');

// Capturing phase listeners (third parameter true)
outer.addEventListener('click', function(event) {
    console.log('1. CAPTURE phase: Outer div');
}, true);

middle.addEventListener('click', function(event) {
    console.log('2. CAPTURE phase: Middle div');
}, true);

inner.addEventListener('click', function(event) {
    console.log('3. CAPTURE phase: Inner button');
}, true);

// Bubbling phase listeners (third parameter false or omitted)
outer.addEventListener('click', function(event) {
    console.log('6. BUBBLE phase: Outer div');
});

middle.addEventListener('click', function(event) {
    console.log('5. BUBBLE phase: Middle div');
});

inner.addEventListener('click', function(event) {
    console.log('4. BUBBLE phase: Inner button');
});

When clicking the button, you'll see this sequence in the console:

1. CAPTURE phase: Outer div
2. CAPTURE phase: Middle div
3. CAPTURE phase: Inner button
4. BUBBLE phase: Inner button
5. BUBBLE phase: Middle div
6. BUBBLE phase: Outer div

Default Behavior

By default, event listeners are registered for the bubbling phase. To register a listener for the capturing phase, set the third parameter of addEventListener() to true.

// Bubbling phase listener (default)
element.addEventListener('click', handler);

// Capturing phase listener
element.addEventListener('click', handler, true);

// Using options object
element.addEventListener('click', handler, {
    capture: true  // Register for capturing phase
});

Controlling Event Propagation

Sometimes you may want to stop an event from continuing its journey through the DOM. JavaScript provides methods to control propagation:

stopPropagation()

Prevents an event from bubbling up to parent elements (or down during capturing):

middle.addEventListener('click', function(event) {
    console.log('Middle div clicked');
    
    // Stop the event from propagating further
    event.stopPropagation();
    
    // Parent handlers won't be triggered
});

// This won't run if clicking on middle or its children
outer.addEventListener('click', function(event) {
    console.log('Outer div clicked'); // Won't execute if middle was clicked
});

stopImmediatePropagation()

More powerful than stopPropagation()—it prevents the event from triggering any other handlers, even on the same element:

// First handler
element.addEventListener('click', function(event) {
    console.log('First handler');
    event.stopImmediatePropagation();
});

// Second handler - won't execute
element.addEventListener('click', function(event) {
    console.log('Second handler'); // This never runs
});

preventDefault()

Prevents the default browser action associated with the event, but doesn't stop propagation:

// Prevent form submission but allow event to propagate
form.addEventListener('submit', function(event) {
    event.preventDefault(); // Form won't submit
    console.log('Form submission prevented');
    
    // Event still propagates to parent elements
});

// This will still run
document.body.addEventListener('submit', function(event) {
    console.log('Body received submit event');
});

Common Misconception

It's important to understand that preventDefault() and stopPropagation() are completely independent:

  • preventDefault(): Stops the browser's default action but allows the event to continue propagating
  • stopPropagation(): Stops the event from traveling through the DOM but doesn't prevent default browser behavior

You may need to use both methods together for some scenarios:

link.addEventListener('click', function(event) {
    // Prevent navigation
    event.preventDefault();
    
    // Also stop the event from bubbling up
    event.stopPropagation();
    
    // Custom behavior
    showContent(this.getAttribute('href'));
});

Practical Example: Modal Dialog

Here's how event propagation is used in a modal dialog:

// HTML structure
// <div id="modal-overlay" class="modal-overlay">
//   <div id="modal-content" class="modal-content">
//     <button id="close-button">×</button>
//     <div id="modal-body">...content...</div>
//   </div>
// </div>

const overlay = document.getElementById('modal-overlay');
const content = document.getElementById('modal-content');
const closeButton = document.getElementById('close-button');

// Close the modal when clicking the overlay (outside content)
overlay.addEventListener('click', function(event) {
    if (event.target === overlay) {
        closeModal();
    }
});

// Close button handler
closeButton.addEventListener('click', function(event) {
    // This doesn't need stopPropagation since the overlay handler 
    // only cares if the target is the overlay itself
    closeModal();
});

// Prevent clicks inside the content area from closing the modal
content.addEventListener('click', function(event) {
    // Don't need stopPropagation here if the overlay handler 
    // checks event.target as shown above
});

Event Delegation Pattern

Event delegation is a powerful pattern that leverages event bubbling to handle events efficiently. Instead of attaching event listeners to specific elements, you attach a single listener to a parent element and use the event.target property to determine which child element triggered the event.

Benefits of Event Delegation

  • Memory Efficiency: Fewer event listeners means less memory usage
  • Dynamic Elements: Works with elements added to the DOM after initial setup
  • Less Code: No need to attach/detach listeners when elements are added/removed
  • Simplified Maintenance: Centralized event handling logic
graph TD A[Single Parent Event Listener] --- B[Click Event Handler] C[Child 1] -.-> A D[Child 2] -.-> A E[Child 3] -.-> A F[Future Child] -.- |"Works with dynamically added elements"| A B --- G{Check event.target} G --- |"Is Child 1"| H[Handler for Child 1] G --- |"Is Child 2"| I[Handler for Child 2] G --- |"Is Child 3"| J[Handler for Child 3] classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px;

Without Event Delegation

// Without delegation - inefficient for many elements
document.querySelectorAll('.menu-item').forEach(item => {
    item.addEventListener('click', function(event) {
        console.log('Item clicked:', this.textContent);
        navigateToSection(this.dataset.section);
    });
});

With Event Delegation

// With delegation - one listener handles all items
document.getElementById('menu').addEventListener('click', function(event) {
    // Find the closest .menu-item that was clicked (or a child of it)
    const menuItem = event.target.closest('.menu-item');
    
    // If a menu item was clicked
    if (menuItem) {
        console.log('Item clicked:', menuItem.textContent);
        navigateToSection(menuItem.dataset.section);
    }
});

Advanced Delegation Example

// HTML structure
// <div id="task-list">
//   <div class="task-item" data-id="1">
//     <span class="task-title">Task 1</span>
//     <div class="task-actions">
//       <button class="edit-btn">Edit</button>
//       <button class="delete-btn">Delete</button>
//       <button class="complete-btn">Complete</button>
//     </div>
//   </div>
//   <!-- More task items... -->
// </div>

const taskList = document.getElementById('task-list');

// One listener handles all task-related actions
taskList.addEventListener('click', function(event) {
    // Find the task item containing the clicked element
    const taskItem = event.target.closest('.task-item');
    
    // If not clicking on a task item or its children, exit
    if (!taskItem) return;
    
    // Get the task ID
    const taskId = taskItem.dataset.id;
    
    // Handle different button clicks
    if (event.target.classList.contains('edit-btn')) {
        editTask(taskId);
    } else if (event.target.classList.contains('delete-btn')) {
        deleteTask(taskId);
    } else if (event.target.classList.contains('complete-btn')) {
        completeTask(taskId);
    } else if (event.target.classList.contains('task-title')) {
        showTaskDetails(taskId);
    }
});

// Functions to handle various actions
function editTask(id) {
    console.log('Editing task:', id);
    // Implementation...
}

function deleteTask(id) {
    console.log('Deleting task:', id);
    // Implementation...
}

function completeTask(id) {
    console.log('Marking task complete:', id);
    // Implementation...
}

function showTaskDetails(id) {
    console.log('Showing details for task:', id);
    // Implementation...
}

The closest() Method

The closest() method is invaluable for event delegation. It traverses up the DOM tree from the target element, looking for the first ancestor that matches a specified selector:

// event.target might be a deeply nested element
// closest() helps find the relevant container element
const listItem = event.target.closest('li');
const card = event.target.closest('.card');
const tableRow = event.target.closest('tr');

// Checking if an element matches a selector
if (event.target.closest('.interactive-element')) {
    // Handle interaction
}

This is more reliable than checking class names directly, as it handles cases where a child element of your target was actually clicked.

Advanced Delegation Techniques

Event delegation can be extended to create highly efficient and flexible event handling systems:

Multi-Level Delegation

Handling different types of elements at different levels of the DOM:

// Handle all clicks at the document level
document.addEventListener('click', function(event) {
    // Navigation menu handling
    if (event.target.closest('nav')) {
        const menuItem = event.target.closest('.menu-item');
        if (menuItem) {
            navigateToSection(menuItem.dataset.section);
            return;
        }
    }
    
    // Card actions handling
    if (event.target.closest('.card')) {
        const card = event.target.closest('.card');
        
        // Handle different buttons within the card
        if (event.target.classList.contains('card-expand-btn')) {
            expandCard(card);
        } else if (event.target.classList.contains('card-delete-btn')) {
            deleteCard(card);
        } else {
            // Clicking on the card itself
            selectCard(card);
        }
        return;
    }
    
    // Form field validation
    if (event.target.closest('form')) {
        const input = event.target.closest('input, select, textarea');
        if (input && event.type === 'blur') {
            validateField(input);
        }
    }
});

Data Attributes for Actions

Using data attributes to specify the action to take:

// HTML structure
// <div id="dashboard">
//   <button data-action="export" data-format="csv">Export CSV</button>
//   <button data-action="export" data-format="pdf">Export PDF</button>
//   <button data-action="refresh">Refresh Data</button>
//   <button data-action="filter" data-filter="today">Today Only</button>
//   <button data-action="filter" data-filter="week">This Week</button>
// </div>

document.getElementById('dashboard').addEventListener('click', function(event) {
    // Find closest element with a data-action attribute
    const actionElement = event.target.closest('[data-action]');
    
    if (!actionElement) return;
    
    // Get the specified action
    const action = actionElement.dataset.action;
    
    // Handle different actions
    switch (action) {
        case 'export':
            const format = actionElement.dataset.format;
            exportData(format);
            break;
            
        case 'refresh':
            refreshData();
            break;
            
        case 'filter':
            const filterType = actionElement.dataset.filter;
            applyFilter(filterType);
            break;
    }
});

Multiple Event Types with Delegation

Handling various event types on the same container:

const productList = document.getElementById('product-list');

// Handle clicks
productList.addEventListener('click', function(event) {
    const product = event.target.closest('.product');
    if (!product) return;
    
    if (event.target.classList.contains('add-to-cart')) {
        addToCart(product.dataset.id);
    } else if (event.target.classList.contains('view-details')) {
        viewProductDetails(product.dataset.id);
    } else {
        // Clicking on the product itself
        selectProduct(product);
    }
});

// Handle hover events
productList.addEventListener('mouseover', function(event) {
    const product = event.target.closest('.product');
    if (product) {
        showProductPreview(product);
    }
});

productList.addEventListener('mouseout', function(event) {
    const product = event.target.closest('.product');
    if (product) {
        hideProductPreview(product);
    }
});

Binding Data to DOM Elements

Combining delegation with data binding for powerful UIs:

// When rendering data to the DOM, store references to the data objects
function renderUsers(users) {
    const userList = document.getElementById('user-list');
    userList.innerHTML = '';
    
    users.forEach(user => {
        const userElement = document.createElement('div');
        userElement.className = 'user-card';
        userElement.dataset.userId = user.id;
        
        // Store reference to the full user object
        userElement._userData = user;
        
        userElement.innerHTML = `
            <img src="${user.avatar}" alt="${user.name}" class="user-avatar">
            <div class="user-info">
                <h3>${user.name}</h3>
                <p>${user.email}</p>
            </div>
            <div class="user-actions">
                <button class="edit-btn">Edit</button>
                <button class="delete-btn">Delete</button>
            </div>
        `;
        
        userList.appendChild(userElement);
    });
}

// Event delegation with data retrieval
document.getElementById('user-list').addEventListener('click', function(event) {
    const userCard = event.target.closest('.user-card');
    if (!userCard) return;
    
    // Access the stored user data
    const userData = userCard._userData;
    
    if (event.target.classList.contains('edit-btn')) {
        openUserEditor(userData);
    } else if (event.target.classList.contains('delete-btn')) {
        confirmUserDeletion(userData);
    } else {
        // Clicking on the card itself
        showUserDetails(userData);
    }
});

Events That Don't Bubble

While event delegation relies on bubbling, not all events bubble up the DOM tree. For these events, you'll need to attach listeners directly to the elements:

Common Non-Bubbling Events

Event Description Alternative Approach
focus Element receives focus Use focusin (bubbles) instead
blur Element loses focus Use focusout (bubbles) instead
mouseenter Mouse enters an element Use mouseover (bubbles) instead
mouseleave Mouse leaves an element Use mouseout (bubbles) instead
load Resource loading completes Attach listeners directly to elements
unload Page is being unloaded Attach to window object directly
resize Element size changes Use ResizeObserver instead for elements
scroll (sometimes) Element scrolling Depends on browser implementation

Using Bubbling Alternatives

// Instead of individual focus/blur handlers:
document.querySelectorAll('input').forEach(input => {
    input.addEventListener('focus', handleFocus);
    input.addEventListener('blur', handleBlur);
});

// Use bubbling events with delegation:
document.querySelector('form').addEventListener('focusin', function(event) {
    if (event.target.tagName === 'INPUT') {
        handleFocus.call(event.target, event);
    }
});

document.querySelector('form').addEventListener('focusout', function(event) {
    if (event.target.tagName === 'INPUT') {
        handleBlur.call(event.target, event);
    }
});

Event Behavior Differences

While focusin/focusout and mouseover/mouseout are bubbling alternatives to their non-bubbling counterparts, they don't behave exactly the same way:

  • mouseenter/mouseleave only fire when the mouse enters/leaves the element itself, not when entering/leaving child elements
  • mouseover/mouseout fire when entering/leaving the element OR any of its children

Consider these differences when choosing which events to use.

Custom Event Delegation

You can create advanced event handling systems by combining event delegation with custom events:

Delegated Custom Events

// Set up a custom event system with delegation
const EventSystem = {
    // Create and dispatch a custom event
    trigger: function(element, eventName, detail = {}) {
        const event = new CustomEvent(eventName, {
            bubbles: true,      // Enable bubbling for delegation
            cancelable: true,   // Allow preventing default behavior
            detail: detail      // Custom data to pass with the event
        });
        
        element.dispatchEvent(event);
    },
    
    // Set up delegated handling of custom events
    delegate: function(container, eventName, selector, handler) {
        container.addEventListener(eventName, function(event) {
            // If this is a custom event, use event.target
            // If it bubbled from a regular event, use event.detail.originalEvent.target
            const target = event.detail.originalEvent ? 
                           event.detail.originalEvent.target : 
                           event.target;
            
            // Find matching element
            const matchedElement = target.closest(selector);
            
            if (matchedElement) {
                // Call handler with matched element as 'this'
                handler.call(matchedElement, event, event.detail);
            }
        });
    }
};

// Usage example
// Set up delegation for a custom event
EventSystem.delegate(document.body, 'item:select', '.list-item', function(event, detail) {
    console.log('Item selected:', this);
    console.log('Extra data:', detail);
});

// Trigger the custom event
const item = document.querySelector('.list-item');
EventSystem.trigger(item, 'item:select', {
    index: 5,
    value: 'Sample Item'
});

// You can also use it to create higher-level events from regular DOM events
document.querySelector('.product-list').addEventListener('click', function(event) {
    if (event.target.closest('.buy-button')) {
        const product = event.target.closest('.product');
        
        // Create a custom semantic event that includes the original event
        EventSystem.trigger(product, 'product:buy', {
            originalEvent: event,
            productId: product.dataset.id,
            productName: product.querySelector('.product-name').textContent,
            price: parseFloat(product.dataset.price)
        });
        
        // Prevent default behavior of the original event
        event.preventDefault();
    }
});

Benefits of Custom Event Delegation

  • Semantic Events: Events describe what happened in your application domain, not just DOM interactions
  • Decoupling: Components can listen for high-level events without knowing how they're triggered
  • Testability: Easy to simulate events without mimicking exact DOM interactions
  • Code Organization: Keeps related event handling together regardless of DOM structure

Real-World Applications

Application 1: Dynamic Data Table with Sorting, Editing, and Selection

document.addEventListener('DOMContentLoaded', function() {
    const DataTable = {
        elements: {
            table: document.getElementById('data-table'),
            tableBody: document.getElementById('data-body'),
            searchInput: document.getElementById('search-input'),
            sortButtons: document.querySelectorAll('.sort-header')
        },
        
        data: [],
        sortField: 'id',
        sortDirection: 'asc',
        
        init: function(data) {
            this.data = data;
            this.render();
            this.setupEventListeners();
        },
        
        setupEventListeners: function() {
            // Table click delegation - handles multiple actions
            this.elements.table.addEventListener('click', this.handleTableClick.bind(this));
            
            // Sort header click delegation
            document.addEventListener('click', this.handleSortClick.bind(this));
            
            // Handle search input
            this.elements.searchInput.addEventListener('input', 
                this.debounce(this.handleSearch.bind(this), 300));
            
            // Listen for custom row events
            this.elements.table.addEventListener('row:edited', this.handleRowEdited.bind(this));
        },
        
        handleTableClick: function(event) {
            // Handle different button clicks within the table
            
            // Edit button
            if (event.target.closest('.edit-btn')) {
                const row = event.target.closest('tr');
                const itemId = row.dataset.id;
                this.editRow(itemId, row);
                return;
            }
            
            // Delete button
            if (event.target.closest('.delete-btn')) {
                const row = event.target.closest('tr');
                const itemId = row.dataset.id;
                this.deleteRow(itemId, row);
                return;
            }
            
            // Row selection
            const row = event.target.closest('tr');
            if (row && !event.target.closest('button')) {
                this.toggleRowSelection(row);
            }
        },
        
        handleSortClick: function(event) {
            const sortHeader = event.target.closest('.sort-header');
            if (!sortHeader) return;
            
            const field = sortHeader.dataset.field;
            
            // Toggle direction if same field, otherwise default to ascending
            if (field === this.sortField) {
                this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
            } else {
                this.sortField = field;
                this.sortDirection = 'asc';
            }
            
            // Update UI to show sort direction
            this.elements.sortButtons.forEach(btn => {
                btn.classList.remove('sort-asc', 'sort-desc');
            });
            
            sortHeader.classList.add(this.sortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
            
            // Re-render with new sort
            this.render();
        },
        
        handleSearch: function(event) {
            const searchTerm = event.target.value.toLowerCase();
            
            // Filter visible rows based on search term
            const rows = this.elements.tableBody.querySelectorAll('tr');
            rows.forEach(row => {
                const text = row.textContent.toLowerCase();
                row.classList.toggle('hidden', searchTerm && !text.includes(searchTerm));
            });
        },
        
        handleRowEdited: function(event) {
            // Handle custom event when a row is edited
            const { itemId, newData } = event.detail;
            
            // Update data
            const itemIndex = this.data.findIndex(item => item.id == itemId);
            if (itemIndex !== -1) {
                this.data[itemIndex] = { ...this.data[itemIndex], ...newData };
                
                // Re-render the specific row
                const row = this.elements.tableBody.querySelector(`tr[data-id="${itemId}"]`);
                if (row) {
                    this.renderRow(row, this.data[itemIndex]);
                }
            }
        },
        
        editRow: function(itemId, row) {
            const item = this.data.find(item => item.id == itemId);
            if (!item) return;
            
            // Replace row content with editable fields
            const cells = row.querySelectorAll('td:not(.actions)');
            
            // Store original values for cancel
            row._originalContent = row.innerHTML;
            
            // Replace each cell with input
            cells.forEach(cell => {
                const field = cell.dataset.field;
                if (!field) return;
                
                // Current value
                const value = item[field];
                
                // Replace with input
                cell.innerHTML = ``;
            });
            
            // Replace action buttons
            const actionsCell = row.querySelector('.actions');
            if (actionsCell) {
                actionsCell.innerHTML = `
                    
                    
                `;
                
                // Add direct event listeners to new buttons
                actionsCell.querySelector('.save-btn').addEventListener('click', () => {
                    this.saveRow(itemId, row);
                });
                
                actionsCell.querySelector('.cancel-btn').addEventListener('click', () => {
                    this.cancelEdit(row);
                });
            }
        },
        
        saveRow: function(itemId, row) {
            // Collect values from inputs
            const inputs = row.querySelectorAll('input');
            const newData = {};
            
            inputs.forEach(input => {
                newData[input.name] = input.value;
            });
            
            // Dispatch custom event for row edited
            const event = new CustomEvent('row:edited', {
                bubbles: true,
                detail: {
                    itemId: itemId,
                    newData: newData
                }
            });
            
            row.dispatchEvent(event);
            
            // Restore row to view mode
            this.cancelEdit(row);
        },
        
        cancelEdit: function(row) {
            // Restore original content
            if (row._originalContent) {
                row.innerHTML = row._originalContent;
                delete row._originalContent;
            }
        },
        
        deleteRow: function(itemId, row) {
            if (confirm('Are you sure you want to delete this item?')) {
                // Remove from data
                this.data = this.data.filter(item => item.id != itemId);
                
                // Remove row with animation
                row.classList.add('fade-out');
                
                // Remove after animation completes
                setTimeout(() => {
                    row.remove();
                }, 300);
            }
        },
        
        toggleRowSelection: function(row) {
            // Toggle selected class
            row.classList.toggle('selected');
            
            // Update "selected count" display if needed
            const selectedCount = this.elements.tableBody.querySelectorAll('tr.selected').length;
            const countDisplay = document.getElementById('selected-count');
            if (countDisplay) {
                countDisplay.textContent = selectedCount;
                countDisplay.parentElement.classList.toggle('hidden', selectedCount === 0);
            }
        },
        
        render: function() {
            // Sort data first
            const sortedData = [...this.data].sort((a, b) => {
                const fieldA = a[this.sortField];
                const fieldB = b[this.sortField];
                
                // Handle different data types
                if (typeof fieldA === 'number' && typeof fieldB === 'number') {
                    return this.sortDirection === 'asc' ? fieldA - fieldB : fieldB - fieldA;
                } else {
                    const strA = String(fieldA).toLowerCase();
                    const strB = String(fieldB).toLowerCase();
                    
                    return this.sortDirection === 'asc' ? 
                        strA.localeCompare(strB) : 
                        strB.localeCompare(strA);
                }
            });
            
            // Clear table
            this.elements.tableBody.innerHTML = '';
            
            // Add rows
            sortedData.forEach(item => {
                const row = document.createElement('tr');
                row.dataset.id = item.id;
                this.renderRow(row, item);
                this.elements.tableBody.appendChild(row);
            });
        },
        
        renderRow: function(row, item) {
            row.innerHTML = `
                ${item.id}
                ${item.name}
                ${item.category}
                $${parseFloat(item.price).toFixed(2)}
                ${item.stock}
                
                    
                    
                
            `;
        },
        
        // Utility method for debouncing
        debounce: function(func, wait) {
            let timeout;
            return function(...args) {
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(this, args), wait);
            };
        }
    };
    
    // Sample data
    const sampleData = [
        { id: 1, name: 'Laptop', category: 'Electronics', price: 999.99, stock: 15 },
        { id: 2, name: 'Desk Chair', category: 'Furniture', price: 189.50, stock: 23 },
        { id: 3, name: 'Coffee Maker', category: 'Appliances', price: 79.99, stock: 42 },
        { id: 4, name: 'Headphones', category: 'Electronics', price: 149.99, stock: 30 },
        { id: 5, name: 'Desk Lamp', category: 'Lighting', price: 49.99, stock: 18 }
    ];
    
    // Initialize
    DataTable.init(sampleData);
});

Application 2: Interactive Multi-Step Form with Event Delegation

document.addEventListener('DOMContentLoaded', function() {
    const MultiStepForm = {
        elements: {
            form: document.getElementById('multi-step-form'),
            steps: document.querySelectorAll('.form-step'),
            progressBar: document.querySelector('.progress-bar'),
            stepIndicators: document.querySelectorAll('.step-indicator')
        },
        
        state: {
            currentStep: 0,
            formData: {},
            isValid: false
        },
        
        init: function() {
            // Set up event delegation for the entire form
            this.elements.form.addEventListener('click', this.handleClick.bind(this));
            
            // Form input handling with delegation
            this.elements.form.addEventListener('input', this.handleInput.bind(this));
            this.elements.form.addEventListener('change', this.handleChange.bind(this));
            
            // Blur for validation
            this.elements.form.addEventListener('focusout', this.handleBlur.bind(this));
            
            // Setup submission handling
            this.elements.form.addEventListener('submit', this.handleSubmit.bind(this));
            
            // Initialize first step
            this.showStep(0);
            
            // Load any saved form data from localStorage
            this.loadFormData();
        },
        
        handleClick: function(event) {
            // Next button
            if (event.target.classList.contains('next-btn')) {
                this.nextStep();
            }
            
            // Previous button
            if (event.target.classList.contains('prev-btn')) {
                this.prevStep();
            }
            
            // Step indicator clicks
            if (event.target.closest('.step-indicator')) {
                const index = parseInt(event.target.closest('.step-indicator').dataset.step);
                // Only allow clicking on completed steps or the next available step
                if (index <= this.state.currentStep + 1 && index !== this.state.currentStep) {
                    this.goToStep(index);
                }
            }
            
            // Help button clicks
            if (event.target.classList.contains('help-btn')) {
                const fieldName = event.target.dataset.field;
                this.showFieldHelp(fieldName);
            }
        },
        
        handleInput: function(event) {
            // Update form data as user types
            if (event.target.name) {
                this.updateFormData(event.target);
            }
            
            // Real-time validation for certain fields
            if (event.target.dataset.validateLive === 'true') {
                this.validateField(event.target);
            }
            
            // Handle password strength meter
            if (event.target.id === 'password') {
                this.updatePasswordStrength(event.target.value);
            }
        },
        
        handleChange: function(event) {
            // Update form data for select boxes, checkboxes, radios
            if (event.target.name) {
                this.updateFormData(event.target);
            }
            
            // Validate field on change
            this.validateField(event.target);
            
            // Special handling for certain fields
            if (event.target.id === 'country') {
                this.updateStateOptions(event.target.value);
            }
        },
        
        handleBlur: function(event) {
            // Validate when field loses focus
            if (event.target.tagName === 'INPUT' || 
                event.target.tagName === 'SELECT' || 
                event.target.tagName === 'TEXTAREA') {
                
                // Mark field as visited
                event.target.dataset.visited = 'true';
                
                // Validate
                this.validateField(event.target);
            }
        },
        
        handleSubmit: function(event) {
            event.preventDefault();
            
            // Validate the current step before submission
            if (!this.validateStep(this.state.currentStep)) {
                return;
            }
            
            // If on the last step, submit the form
            if (this.state.currentStep === this.elements.steps.length - 1) {
                this.submitForm();
            } else {
                // Otherwise go to the next step
                this.nextStep();
            }
        },
        
        updateFormData: function(field) {
            const name = field.name;
            let value;
            
            // Handle different input types
            if (field.type === 'checkbox') {
                value = field.checked;
            } else if (field.type === 'radio') {
                // Only update if checked
                if (field.checked) {
                    value = field.value;
                } else {
                    return; // Don't update if the radio isn't checked
                }
            } else {
                value = field.value;
            }
            
            // Update the formData object
            this.state.formData[name] = value;
            
            // Save form data to localStorage
            this.saveFormData();
        },
        
        validateField: function(field) {
            // Skip validation if field hasn't been visited yet
            if (!field.dataset.visited && !field.required) return true;
            
            const name = field.name;
            const value = field.value;
            const errorElement = document.getElementById(`${name}-error`);
            
            // Reset validation state
            field.classList.remove('valid', 'invalid');
            if (errorElement) errorElement.textContent = '';
            
            // Required field validation
            if (field.required && !value) {
                this.setFieldInvalid(field, errorElement, 'This field is required');
                return false;
            }
            
            // Email validation
            if (field.type === 'email' && value) {
                const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                if (!emailPattern.test(value)) {
                    this.setFieldInvalid(field, errorElement, 'Please enter a valid email address');
                    return false;
                }
            }
            
            // Password validation
            if (field.id === 'password' && value) {
                if (value.length < 8) {
                    this.setFieldInvalid(field, errorElement, 'Password must be at least 8 characters');
                    return false;
                }
            }
            
            // Confirm password validation
            if (field.id === 'confirm-password' && value) {
                const password = document.getElementById('password').value;
                if (value !== password) {
                    this.setFieldInvalid(field, errorElement, 'Passwords do not match');
                    return false;
                }
            }
            
            // Custom validation rules from data attributes
            if (field.dataset.minLength && value.length < parseInt(field.dataset.minLength)) {
                this.setFieldInvalid(field, errorElement, 
                    `Minimum length is ${field.dataset.minLength} characters`);
                return false;
            }
            
            if (field.dataset.pattern) {
                const pattern = new RegExp(field.dataset.pattern);
                if (!pattern.test(value)) {
                    this.setFieldInvalid(field, errorElement, field.dataset.errorMsg || 'Invalid format');
                    return false;
                }
            }
            
            // If we get here, the field is valid
            field.classList.add('valid');
            return true;
        },
        
        setFieldInvalid: function(field, errorElement, message) {
            field.classList.add('invalid');
            if (errorElement) {
                errorElement.textContent = message;
            }
        },
        
        validateStep: function(stepIndex) {
            const step = this.elements.steps[stepIndex];
            const fields = step.querySelectorAll('input, select, textarea');
            
            let isValid = true;
            
            // Validate each field in the step
            fields.forEach(field => {
                // Skip fields without a name attribute and buttons
                if (!field.name || field.type === 'button' || field.type === 'submit') return;
                
                // Mark as visited for validation
                field.dataset.visited = 'true';
                
                // Validate and update overall validity
                if (!this.validateField(field)) {
                    isValid = false;
                }
            });
            
            // Update step indicator
            this.elements.stepIndicators[stepIndex].classList.toggle('step-error', !isValid);
            this.elements.stepIndicators[stepIndex].classList.toggle('step-complete', isValid);
            
            return isValid;
        },
        
        nextStep: function() {
            // Validate current step before proceeding
            if (!this.validateStep(this.state.currentStep)) {
                // Show error message
                const errorSummary = this.elements.steps[this.state.currentStep]
                    .querySelector('.error-summary');
                
                if (errorSummary) {
                    errorSummary.textContent = 'Please correct the errors in this step before proceeding.';
                    errorSummary.classList.add('visible');
                }
                return;
            }
            
            // Move to next step if not on the last step
            if (this.state.currentStep < this.elements.steps.length - 1) {
                this.showStep(this.state.currentStep + 1);
            }
        },
        
        prevStep: function() {
            // Move to previous step if not on the first step
            if (this.state.currentStep > 0) {
                this.showStep(this.state.currentStep - 1);
            }
        },
        
        goToStep: function(stepIndex) {
            // Validate all steps before the target step
            let canProceed = true;
            for (let i = 0; i < stepIndex; i++) {
                if (!this.validateStep(i)) {
                    canProceed = false;
                    // Jump to the first invalid step
                    this.showStep(i);
                    break;
                }
            }
            
            if (canProceed) {
                this.showStep(stepIndex);
            }
        },
        
        showStep: function(stepIndex) {
            // Hide all steps
            this.elements.steps.forEach(step => {
                step.classList.remove('active');
            });
            
            // Show the current step
            this.elements.steps[stepIndex].classList.add('active');
            
            // Update current step
            this.state.currentStep = stepIndex;
            
            // Update progress bar
            const progress = ((stepIndex) / (this.elements.steps.length - 1)) * 100;
            this.elements.progressBar.style.width = `${progress}%`;
            
            // Update step indicators
            this.elements.stepIndicators.forEach((indicator, index) => {
                indicator.classList.toggle('active', index === stepIndex);
                indicator.classList.toggle('completed', index < stepIndex);
            });
            
            // Scroll to top of form
            this.elements.form.scrollIntoView({ behavior: 'smooth', block: 'start' });
            
            // Hide any error summaries
            const errorSummary = this.elements.steps[stepIndex].querySelector('.error-summary');
            if (errorSummary) {
                errorSummary.classList.remove('visible');
            }
            
            // Focus the first field in the step
            setTimeout(() => {
                const firstInput = this.elements.steps[stepIndex]
                    .querySelector('input:not([type="hidden"]), select, textarea');
                if (firstInput) {
                    firstInput.focus();
                }
            }, 300);
        },
        
        updatePasswordStrength: function(password) {
            const strengthBar = document.getElementById('password-strength-bar');
            const strengthText = document.getElementById('password-strength-text');
            
            if (!strengthBar || !strengthText) return;
            
            // Calculate password strength
            let strength = 0;
            
            // Length
            if (password.length >= 8) strength += 1;
            if (password.length >= 12) strength += 1;
            
            // Complexity
            if (/[A-Z]/.test(password)) strength += 1;
            if (/[a-z]/.test(password)) strength += 1;
            if (/[0-9]/.test(password)) strength += 1;
            if (/[^A-Za-z0-9]/.test(password)) strength += 1;
            
            // Calculate percentage (max strength is 6)
            const percentage = Math.min(100, (strength / 6) * 100);
            
            // Update UI
            strengthBar.style.width = `${percentage}%`;
            
            // Remove existing classes
            strengthBar.classList.remove('weak', 'medium', 'strong');
            
            // Add appropriate class
            let strengthLabel = '';
            if (percentage < 40) {
                strengthBar.classList.add('weak');
                strengthLabel = 'Weak';
            } else if (percentage < 70) {
                strengthBar.classList.add('medium');
                strengthLabel = 'Medium';
            } else {
                strengthBar.classList.add('strong');
                strengthLabel = 'Strong';
            }
            
            strengthText.textContent = strengthLabel;
        },
        
        updateStateOptions: function(country) {
            const stateSelect = document.getElementById('state');
            if (!stateSelect) return;
            
            // Clear existing options
            stateSelect.innerHTML = '';
            
            // Add options based on selected country
            let options = [];
            
            switch (country) {
                case 'US':
                    options = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', '...'];
                    break;
                case 'Canada':
                    options = ['Alberta', 'British Columbia', 'Manitoba', 'New Brunswick', '...'];
                    break;
                case 'UK':
                    options = ['England', 'Scotland', 'Wales', 'Northern Ireland'];
                    break;
                // Add more countries as needed
            }
            
            // Add options to select
            options.forEach(option => {
                const optionElement = document.createElement('option');
                optionElement.value = option;
                optionElement.textContent = option;
                stateSelect.appendChild(optionElement);
            });
            
            // Show/hide state field based on country selection
            const stateField = document.getElementById('state-field');
            if (stateField) {
                stateField.style.display = options.length ? 'block' : 'none';
            }
        },
        
        showFieldHelp: function(fieldName) {
            const helpText = {
                'name': 'Enter your full legal name as it appears on official documents.',
                'email': 'We\'ll use this email for account verification and communication.',
                'password': 'Choose a strong password with at least 8 characters, including uppercase, lowercase, numbers, and special characters.',
                // Add more help text as needed
            };
            
            const message = helpText[fieldName] || 'No help available for this field.';
            
            // Show help in a tooltip or modal
            alert(message); // Replace with your preferred UI component
        },
        
        saveFormData: function() {
            // Save form data to localStorage
            localStorage.setItem('formData', JSON.stringify(this.state.formData));
        },
        
        loadFormData: function() {
            // Load saved form data
            const savedData = localStorage.getItem('formData');
            if (savedData) {
                this.state.formData = JSON.parse(savedData);
                
                // Fill in form fields
                for (const [name, value] of Object.entries(this.state.formData)) {
                    const field = this.elements.form.querySelector(`[name="${name}"]`);
                    if (!field) continue;
                    
                    if (field.type === 'checkbox') {
                        field.checked = value;
                    } else if (field.type === 'radio') {
                        const radio = this.elements.form.querySelector(`[name="${name}"][value="${value}"]`);
                        if (radio) radio.checked = true;
                    } else {
                        field.value = value;
                    }
                }
            }
        },
        
        submitForm: function() {
            // Show loading state
            const submitButton = this.elements.form.querySelector('button[type="submit"]');
            if (submitButton) {
                submitButton.disabled = true;
                submitButton.innerHTML = ' Submitting...';
            }
            
            // In a real application, you would send data to a server
            console.log('Form Data:', this.state.formData);
            
            // Simulate AJAX request
            setTimeout(() => {
                // Show success message
                this.elements.form.innerHTML = `
                    

Thank You!

Your form has been submitted successfully.

We'll be in touch soon.

`; // Clear saved form data localStorage.removeItem('formData'); }, 1500); } }; // Initialize the form MultiStepForm.init(); });

Practical Exercise

Build a complete UI component library with event delegation by implementing the following:

  1. Create a tabbed interface where clicking tab headers shows different content panels
  2. Implement an accordion component with expandable/collapsible sections
  3. Build a dropdown menu system with nested submenus
  4. Create a sortable, filterable data table with pagination
  5. Add a modal dialog system with different types of modals (alert, confirm, prompt)
  6. Implement a notification system for displaying alerts and messages
  7. Create a carousel/slider with touch support and automatic rotation
  8. Build a form validation system with field-specific error messages

For all components:

This exercise will give you practical experience with all aspects of event delegation and propagation in a realistic application context.

Summary

Understanding event propagation and mastering event delegation are crucial skills for building efficient, scalable JavaScript applications. These techniques help create cleaner code that performs better and works with dynamic content.

Additional Resources