Event Propagation and Delegation

Understanding How Events Flow Through the DOM

Understanding Event Propagation

When an event occurs on an element, it doesn't just trigger on that element alone. Events propagate through the DOM in three phases, like ripples in a pond. Imagine dropping a stone in water - the ripples spread outward from the point of impact. Similarly, events flow through the DOM tree in a predictable pattern.

graph TD A[Window] --> B[Document] B --> C[HTML] C --> D[Body] D --> E[Div] E --> F[Button - Event Target] style F fill:#f96,stroke:#333,stroke-width:4px style A fill:#bbf,stroke:#333,stroke-width:2px subgraph "1. Capture Phase ↓" A B C D E end subgraph "2. Target Phase" F end subgraph "3. Bubble Phase ↑" E D C B A end

The Three Event Phases

Events propagate through the DOM in three distinct phases, each offering different opportunities for handling events.

1. Capture Phase (Capturing)

The event travels down from the window to the target element, checking each ancestor along the way.

2. Target Phase

The event reaches the target element where it originated.

3. Bubble Phase (Bubbling)

The event bubbles up from the target element back to the window, visiting each ancestor again.

// Demonstrating all three phases
const button = document.querySelector('button');
const div = document.querySelector('div');
const body = document.body;

// Capture phase listener
window.addEventListener('click', () => {
    console.log('Window capture');
}, true); // true = capture phase

body.addEventListener('click', () => {
    console.log('Body capture');
}, true);

div.addEventListener('click', () => {
    console.log('Div capture');
}, true);

// Target phase (both capture and bubble listeners trigger here)
button.addEventListener('click', () => {
    console.log('Button clicked (target)');
});

// Bubble phase listener  
div.addEventListener('click', () => {
    console.log('Div bubble');
}); // false = bubble phase (default)

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

window.addEventListener('click', () => {
    console.log('Window bubble');
});

// When button is clicked, output will be:
// Window capture
// Body capture  
// Div capture
// Button clicked (target)
// Div bubble
// Body bubble
// Window bubble

Interactive Propagation Visualizer

class PropagationVisualizer {
    constructor(rootElement) {
        this.root = rootElement;
        this.logContainer = document.createElement('div');
        this.logContainer.className = 'event-log';
        this.root.appendChild(this.logContainer);
        
        this.setupElements();
        this.attachListeners();
    }
    
    setupElements() {
        // Create nested structure
        const html = `
            
Outer
Middle
Inner
`; const container = document.createElement('div'); container.innerHTML = html; this.root.insertBefore(container, this.logContainer); } attachListeners() { const elements = this.root.querySelectorAll('[data-name]'); elements.forEach(element => { const name = element.dataset.name; // Capture phase element.addEventListener('click', (e) => { this.log(`${name} - CAPTURE`, 'capture'); element.classList.add('capture-highlight'); setTimeout(() => element.classList.remove('capture-highlight'), 500); }, true); // Bubble phase element.addEventListener('click', (e) => { this.log(`${name} - BUBBLE`, 'bubble'); element.classList.add('bubble-highlight'); setTimeout(() => element.classList.remove('bubble-highlight'), 500); }, false); }); // Add clear button const clearButton = document.createElement('button'); clearButton.textContent = 'Clear Log'; clearButton.addEventListener('click', (e) => { e.stopPropagation(); this.clearLog(); }); this.root.insertBefore(clearButton, this.logContainer); } log(message, phase) { const entry = document.createElement('div'); entry.className = `log-entry ${phase}`; entry.textContent = message; this.logContainer.appendChild(entry); // Auto-scroll to bottom this.logContainer.scrollTop = this.logContainer.scrollHeight; } clearLog() { this.logContainer.innerHTML = ''; } } // Initialize visualizer new PropagationVisualizer(document.querySelector('#propagation-demo'));

Controlling Event Propagation

JavaScript provides several methods to control how events propagate through the DOM.

stopPropagation()

Prevents the event from propagating further in either capturing or bubbling phase.

// Stop propagation example
const inner = document.querySelector('.inner');
const outer = document.querySelector('.outer');

inner.addEventListener('click', (e) => {
    console.log('Inner clicked');
    e.stopPropagation(); // Event won't reach outer
});

outer.addEventListener('click', (e) => {
    console.log('Outer clicked'); // This won't run if inner is clicked
});

// Practical example: Modal dialog
class Modal {
    constructor() {
        this.modal = document.querySelector('.modal');
        this.overlay = document.querySelector('.modal-overlay');
        this.closeButton = document.querySelector('.modal-close');
        
        this.setupEventListeners();
    }
    
    setupEventListeners() {
        // Close modal when clicking overlay
        this.overlay.addEventListener('click', () => {
            this.close();
        });
        
        // Prevent closing when clicking modal content
        this.modal.addEventListener('click', (e) => {
            e.stopPropagation();
        });
        
        // Close button
        this.closeButton.addEventListener('click', () => {
            this.close();
        });
    }
    
    open() {
        this.overlay.classList.add('active');
        this.modal.classList.add('active');
    }
    
    close() {
        this.overlay.classList.remove('active');
        this.modal.classList.remove('active');
    }
}

stopImmediatePropagation()

Stops the event from propagating and prevents other handlers on the same element from executing.

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

// First handler
button.addEventListener('click', (e) => {
    console.log('Handler 1');
    e.stopImmediatePropagation(); // Stops here
});

// Second handler (won't execute)
button.addEventListener('click', (e) => {
    console.log('Handler 2'); // Never logs
});

// Parent handler (also won't execute)
button.parentElement.addEventListener('click', (e) => {
    console.log('Parent handler'); // Never logs
});

preventDefault()

Prevents the default browser action but doesn't stop propagation.

// Prevent form submission
const form = document.querySelector('form');

form.addEventListener('submit', (e) => {
    e.preventDefault(); // Prevent page reload
    
    // Custom form handling
    const formData = new FormData(form);
    submitFormAjax(formData);
});

// Prevent link navigation
const link = document.querySelector('a');

link.addEventListener('click', (e) => {
    e.preventDefault(); // Prevent navigation
    
    // Custom behavior
    loadContentAjax(e.target.href);
});

// Allow propagation but prevent default
const checkbox = document.querySelector('input[type="checkbox"]');

checkbox.addEventListener('click', (e) => {
    if (!userCanToggle()) {
        e.preventDefault(); // Prevent checking/unchecking
        showMessage('You cannot change this setting');
    }
    // Event still bubbles up to parent handlers
});

Event Delegation

Event delegation is a powerful pattern that takes advantage of event bubbling. Instead of attaching event listeners to multiple child elements, you attach a single listener to a parent element and handle events for all children.

graph TD A[Parent Element
Single Event Listener] --> B[Child 1] A --> C[Child 2] A --> D[Child 3] A --> E[Child 4] A --> F[... Child N] style A fill:#f96,stroke:#333,stroke-width:4px style B fill:#bbf,stroke:#333,stroke-width:2px style C fill:#bbf,stroke:#333,stroke-width:2px style D fill:#bbf,stroke:#333,stroke-width:2px style E fill:#bbf,stroke:#333,stroke-width:2px style F fill:#bbf,stroke:#333,stroke-width:2px

Basic Event Delegation

// Without delegation (inefficient)
const buttons = document.querySelectorAll('.item-button');
buttons.forEach(button => {
    button.addEventListener('click', handleButtonClick);
});

// With delegation (efficient)
const container = document.querySelector('.items-container');
container.addEventListener('click', (e) => {
    if (e.target.matches('.item-button')) {
        handleButtonClick(e);
    }
});

// Multiple element types
container.addEventListener('click', (e) => {
    const target = e.target;
    
    if (target.matches('.delete-button')) {
        deleteItem(target.closest('.item'));
    } else if (target.matches('.edit-button')) {
        editItem(target.closest('.item'));
    } else if (target.matches('.item-title')) {
        toggleItemDetails(target.closest('.item'));
    }
});

// Using closest() for nested elements
container.addEventListener('click', (e) => {
    const item = e.target.closest('.item');
    if (!item) return; // Clicked outside any item
    
    const action = e.target.dataset.action;
    if (action) {
        handleItemAction(item, action);
    }
});

Advanced Delegation Patterns

// Delegated event handler class
class DelegatedEventHandler {
    constructor(root, handlers) {
        this.root = root;
        this.handlers = handlers;
        this.setupDelegation();
    }
    
    setupDelegation() {
        Object.keys(this.handlers).forEach(eventType => {
            this.root.addEventListener(eventType, (e) => {
                const handlers = this.handlers[eventType];
                
                for (const [selector, handler] of Object.entries(handlers)) {
                    const target = e.target.closest(selector);
                    if (target && this.root.contains(target)) {
                        handler.call(target, e);
                    }
                }
            });
        });
    }
}

// Usage
const handler = new DelegatedEventHandler(document.body, {
    click: {
        '.button': function(e) {
            console.log('Button clicked:', this.textContent);
        },
        '.menu-item': function(e) {
            console.log('Menu item clicked:', this.dataset.value);
        },
        'a[data-ajax]': function(e) {
            e.preventDefault();
            loadContent(this.href);
        }
    },
    
    mouseover: {
        '.tooltip-trigger': function(e) {
            showTooltip(this);
        }
    },
    
    mouseout: {
        '.tooltip-trigger': function(e) {
            hideTooltip(this);
        }
    }
});

// Dynamic list with delegation
class DynamicList {
    constructor(container) {
        this.container = container;
        this.items = [];
        this.setupDelegation();
    }
    
    setupDelegation() {
        this.container.addEventListener('click', (e) => {
            const target = e.target;
            const item = target.closest('.list-item');
            
            if (!item) return;
            
            if (target.matches('.delete-btn')) {
                this.deleteItem(item.dataset.id);
            } else if (target.matches('.edit-btn')) {
                this.editItem(item.dataset.id);
            } else if (target.matches('.checkbox')) {
                this.toggleItem(item.dataset.id);
            }
        });
        
        // Keyboard navigation
        this.container.addEventListener('keydown', (e) => {
            const item = e.target.closest('.list-item');
            if (!item) return;
            
            switch(e.key) {
                case 'Delete':
                    this.deleteItem(item.dataset.id);
                    break;
                case 'Enter':
                    if (e.target.matches('.editable')) {
                        this.saveEdit(item.dataset.id, e.target.value);
                    }
                    break;
                case 'Escape':
                    if (e.target.matches('.editable')) {
                        this.cancelEdit(item.dataset.id);
                    }
                    break;
            }
        });
    }
    
    addItem(data) {
        const id = Date.now();
        const itemData = { id, ...data };
        this.items.push(itemData);
        
        const itemElement = this.createItemElement(itemData);
        this.container.appendChild(itemElement);
    }
    
    createItemElement(data) {
        const div = document.createElement('div');
        div.className = 'list-item';
        div.dataset.id = data.id;
        
        div.innerHTML = `
            
            ${data.text}
            
            
        `;
        
        return div;
    }
    
    deleteItem(id) {
        const index = this.items.findIndex(item => item.id == id);
        if (index !== -1) {
            this.items.splice(index, 1);
            const element = this.container.querySelector(`[data-id="${id}"]`);
            if (element) {
                element.remove();
            }
        }
    }
    
    editItem(id) {
        const element = this.container.querySelector(`[data-id="${id}"]`);
        const content = element.querySelector('.content');
        
        const input = document.createElement('input');
        input.type = 'text';
        input.className = 'editable';
        input.value = content.textContent;
        
        content.replaceWith(input);
        input.focus();
    }
    
    saveEdit(id, newValue) {
        const item = this.items.find(item => item.id == id);
        if (item) {
            item.text = newValue;
            
            const element = this.container.querySelector(`[data-id="${id}"]`);
            const input = element.querySelector('.editable');
            
            const span = document.createElement('span');
            span.className = 'content';
            span.textContent = newValue;
            
            input.replaceWith(span);
        }
    }
    
    toggleItem(id) {
        const item = this.items.find(item => item.id == id);
        if (item) {
            item.completed = !item.completed;
        }
    }
}

Real-World Delegation Examples

Infinite Scroll with Delegation

class InfiniteScroll {
    constructor(container, options = {}) {
        this.container = container;
        this.options = {
            threshold: 200,
            itemsPerPage: 20,
            ...options
        };
        
        this.page = 1;
        this.loading = false;
        this.hasMore = true;
        
        this.setupDelegation();
        this.setupScrollListener();
        this.loadInitialContent();
    }
    
    setupDelegation() {
        this.container.addEventListener('click', (e) => {
            const card = e.target.closest('.content-card');
            if (!card) return;
            
            if (e.target.matches('.like-button')) {
                this.toggleLike(card.dataset.id);
            } else if (e.target.matches('.share-button')) {
                this.shareContent(card.dataset.id);
            } else if (e.target.matches('.bookmark-button')) {
                this.toggleBookmark(card.dataset.id);
            }
        });
        
        // Handle image lazy loading
        this.container.addEventListener('scroll', () => {
            this.checkVisibleImages();
        });
    }
    
    setupScrollListener() {
        const scrollHandler = this.throttle(() => {
            if (this.shouldLoadMore()) {
                this.loadMoreContent();
            }
        }, 200);
        
        window.addEventListener('scroll', scrollHandler);
    }
    
    shouldLoadMore() {
        if (this.loading || !this.hasMore) return false;
        
        const scrollPosition = window.innerHeight + window.scrollY;
        const threshold = document.body.offsetHeight - this.options.threshold;
        
        return scrollPosition >= threshold;
    }
    
    async loadMoreContent() {
        if (this.loading) return;
        
        this.loading = true;
        this.showLoader();
        
        try {
            const data = await this.fetchContent(this.page);
            this.renderContent(data.items);
            
            this.page++;
            this.hasMore = data.hasMore;
            
            if (!this.hasMore) {
                this.showEndMessage();
            }
        } catch (error) {
            this.showError(error);
        } finally {
            this.loading = false;
            this.hideLoader();
        }
    }
    
    renderContent(items) {
        const fragment = document.createDocumentFragment();
        
        items.forEach(item => {
            const card = this.createCard(item);
            fragment.appendChild(card);
        });
        
        this.container.appendChild(fragment);
    }
    
    createCard(item) {
        const div = document.createElement('div');
        div.className = 'content-card';
        div.dataset.id = item.id;
        
        div.innerHTML = `
            ${item.title}
            

${item.title}

${item.description}

`; return div; } checkVisibleImages() { const images = this.container.querySelectorAll('img.lazy-image'); images.forEach(img => { if (this.isElementInViewport(img)) { this.loadImage(img); } }); } isElementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth ); } loadImage(img) { const src = img.dataset.src; if (src) { img.src = src; img.classList.remove('lazy-image'); delete img.dataset.src; } } throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } }

Interactive Table with Delegation

class DataTable {
    constructor(container, options = {}) {
        this.container = container;
        this.data = options.data || [];
        this.columns = options.columns || [];
        this.sortColumn = null;
        this.sortDirection = 'asc';
        this.selectedRows = new Set();
        
        this.render();
        this.setupDelegation();
    }
    
    setupDelegation() {
        this.container.addEventListener('click', (e) => {
            const target = e.target;
            
            // Header clicks for sorting
            if (target.matches('th[data-sortable]')) {
                this.handleSort(target.dataset.column);
            }
            
            // Row selection
            if (target.matches('input[type="checkbox"].row-select')) {
                this.handleRowSelect(target);
            }
            
            // Select all checkbox
            if (target.matches('input[type="checkbox"].select-all')) {
                this.handleSelectAll(target.checked);
            }
            
            // Action buttons
            if (target.matches('[data-action]')) {
                const row = target.closest('tr');
                if (row) {
                    this.handleAction(target.dataset.action, row.dataset.id);
                }
            }
            
            // Cell editing
            if (target.matches('td[data-editable="true"]')) {
                this.handleCellEdit(target);
            }
        });
        
        // Handle cell editing completion
        this.container.addEventListener('blur', (e) => {
            if (e.target.matches('.cell-editor')) {
                this.saveCellEdit(e.target);
            }
        }, true);
        
        this.container.addEventListener('keydown', (e) => {
            if (e.target.matches('.cell-editor')) {
                if (e.key === 'Enter') {
                    this.saveCellEdit(e.target);
                } else if (e.key === 'Escape') {
                    this.cancelCellEdit(e.target);
                }
            }
        });
    }
    
    render() {
        const table = document.createElement('table');
        table.className = 'data-table';
        
        // Create header
        const thead = document.createElement('thead');
        const headerRow = document.createElement('tr');
        
        // Add select all checkbox
        const selectAllTh = document.createElement('th');
        selectAllTh.innerHTML = '';
        headerRow.appendChild(selectAllTh);
        
        // Add column headers
        this.columns.forEach(column => {
            const th = document.createElement('th');
            th.textContent = column.label;
            
            if (column.sortable) {
                th.dataset.sortable = 'true';
                th.dataset.column = column.field;
                th.classList.add('sortable');
                
                if (this.sortColumn === column.field) {
                    th.classList.add('sorted', this.sortDirection);
                }
            }
            
            headerRow.appendChild(th);
        });
        
        // Add actions column
        const actionsTh = document.createElement('th');
        actionsTh.textContent = 'Actions';
        headerRow.appendChild(actionsTh);
        
        thead.appendChild(headerRow);
        table.appendChild(thead);
        
        // Create body
        const tbody = document.createElement('tbody');
        this.renderRows(tbody);
        table.appendChild(tbody);
        
        this.container.innerHTML = '';
        this.container.appendChild(table);
    }
    
    renderRows(tbody) {
        this.data.forEach(row => {
            const tr = document.createElement('tr');
            tr.dataset.id = row.id;
            
            // Checkbox cell
            const checkboxTd = document.createElement('td');
            checkboxTd.innerHTML = `
                
            `;
            tr.appendChild(checkboxTd);
            
            // Data cells
            this.columns.forEach(column => {
                const td = document.createElement('td');
                td.textContent = row[column.field];
                
                if (column.editable) {
                    td.dataset.editable = 'true';
                    td.dataset.field = column.field;
                }
                
                tr.appendChild(td);
            });
            
            // Actions cell
            const actionsTd = document.createElement('td');
            actionsTd.innerHTML = `
                
                
            `;
            tr.appendChild(actionsTd);
            
            tbody.appendChild(tr);
        });
    }
    
    handleSort(column) {
        if (this.sortColumn === column) {
            this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            this.sortColumn = column;
            this.sortDirection = 'asc';
        }
        
        this.data.sort((a, b) => {
            const valueA = a[column];
            const valueB = b[column];
            
            if (typeof valueA === 'string') {
                return this.sortDirection === 'asc' 
                    ? valueA.localeCompare(valueB)
                    : valueB.localeCompare(valueA);
            } else {
                return this.sortDirection === 'asc'
                    ? valueA - valueB
                    : valueB - valueA;
            }
        });
        
        this.render();
    }
    
    handleRowSelect(checkbox) {
        const row = checkbox.closest('tr');
        const id = row.dataset.id;
        
        if (checkbox.checked) {
            this.selectedRows.add(id);
        } else {
            this.selectedRows.delete(id);
        }
        
        this.updateSelectAllCheckbox();
    }
    
    handleSelectAll(checked) {
        const checkboxes = this.container.querySelectorAll('.row-select');
        
        checkboxes.forEach(checkbox => {
            checkbox.checked = checked;
            const row = checkbox.closest('tr');
            const id = row.dataset.id;
            
            if (checked) {
                this.selectedRows.add(id);
            } else {
                this.selectedRows.delete(id);
            }
        });
    }
    
    updateSelectAllCheckbox() {
        const selectAll = this.container.querySelector('.select-all');
        const checkboxes = this.container.querySelectorAll('.row-select');
        const checkedCount = this.selectedRows.size;
        
        selectAll.checked = checkedCount === checkboxes.length;
        selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
    }
    
    handleCellEdit(cell) {
        if (cell.querySelector('.cell-editor')) return;
        
        const currentValue = cell.textContent;
        const input = document.createElement('input');
        input.type = 'text';
        input.className = 'cell-editor';
        input.value = currentValue;
        
        cell.textContent = '';
        cell.appendChild(input);
        input.focus();
        input.select();
    }
    
    saveCellEdit(input) {
        const cell = input.parentElement;
        const newValue = input.value;
        const row = cell.closest('tr');
        const field = cell.dataset.field;
        const id = row.dataset.id;
        
        // Update data
        const dataRow = this.data.find(r => r.id == id);
        if (dataRow) {
            dataRow[field] = newValue;
        }
        
        cell.textContent = newValue;
    }
    
    cancelCellEdit(input) {
        const cell = input.parentElement;
        const row = cell.closest('tr');
        const field = cell.dataset.field;
        const id = row.dataset.id;
        
        const dataRow = this.data.find(r => r.id == id);
        cell.textContent = dataRow ? dataRow[field] : '';
    }
    
    handleAction(action, id) {
        switch(action) {
            case 'edit':
                console.log('Edit row:', id);
                break;
            case 'delete':
                if (confirm('Delete this row?')) {
                    this.data = this.data.filter(row => row.id != id);
                    this.render();
                }
                break;
        }
    }
}

Performance Benefits of Delegation

Event delegation provides significant performance advantages, especially for dynamic content.

Without Delegation

  • Multiple event listeners (one per element)
  • Memory usage increases with element count
  • Must re-attach listeners for new elements
  • Poor performance with many elements

With Delegation

  • Single event listener
  • Constant memory usage
  • Works automatically with new elements
  • Excellent performance regardless of element count
// Performance comparison
class PerformanceTest {
    constructor() {
        this.container = document.querySelector('#test-container');
    }
    
    // Without delegation - poor performance
    testWithoutDelegation(itemCount) {
        console.time('Without delegation');
        
        for (let i = 0; i < itemCount; i++) {
            const button = document.createElement('button');
            button.textContent = `Button ${i}`;
            button.addEventListener('click', () => {
                console.log(`Button ${i} clicked`);
            });
            this.container.appendChild(button);
        }
        
        console.timeEnd('Without delegation');
        
        // Memory usage: itemCount event listeners
        console.log(`Created ${itemCount} event listeners`);
    }
    
    // With delegation - excellent performance
    testWithDelegation(itemCount) {
        console.time('With delegation');
        
        // Single event listener
        this.container.addEventListener('click', (e) => {
            if (e.target.matches('button')) {
                console.log(`${e.target.textContent} clicked`);
            }
        });
        
        // Create buttons without individual listeners
        for (let i = 0; i < itemCount; i++) {
            const button = document.createElement('button');
            button.textContent = `Button ${i}`;
            this.container.appendChild(button);
        }
        
        console.timeEnd('With delegation');
        
        // Memory usage: 1 event listener
        console.log('Created 1 event listener');
    }
    
    // Dynamic content handling
    testDynamicContent() {
        // With delegation - automatically works
        this.container.addEventListener('click', (e) => {
            if (e.target.matches('.dynamic-button')) {
                console.log('Dynamic button clicked');
            }
        });
        
        // Add buttons dynamically later
        setTimeout(() => {
            const button = document.createElement('button');
            button.className = 'dynamic-button';
            button.textContent = 'Added Later';
            this.container.appendChild(button);
            // No need to attach listener - delegation handles it!
        }, 1000);
    }
}

// Run tests
const test = new PerformanceTest();
test.testWithoutDelegation(1000); // Slower, more memory
test.testWithDelegation(1000);    // Faster, less memory

Best Practices

// Best practices example
class BestPracticesDemo {
    constructor(container) {
        this.container = container;
        this.setupDelegation();
    }
    
    setupDelegation() {
        // Good: Delegate at appropriate container level
        this.container.addEventListener('click', (e) => {
            // Good: Use matches() for precise targeting
            if (e.target.matches('.action-button')) {
                this.handleAction(e.target);
            }
            
            // Good: Use closest() for nested elements
            const item = e.target.closest('.list-item');
            if (item) {
                this.handleItemClick(item, e.target);
            }
        });
        
        // Good: Separate high-frequency events
        this.container.addEventListener('mousemove', 
            this.throttle(this.handleMouseMove.bind(this), 50)
        );
        
        // Good: Use passive listeners for scroll/touch
        this.container.addEventListener('scroll', 
            this.handleScroll.bind(this), 
            { passive: true }
        );
    }
    
    handleItemClick(item, target) {
        // Good: Check specific target within item
        if (target.matches('.delete-button')) {
            this.deleteItem(item);
        } else if (target.matches('.edit-button')) {
            this.editItem(item);
        } else if (target.matches('.toggle-details')) {
            this.toggleDetails(item);
        }
    }
    
    // Good: Clean separation of concerns
    deleteItem(item) {
        if (confirm('Delete this item?')) {
            item.remove();
            this.updateItemCount();
        }
    }
    
    editItem(item) {
        const content = item.querySelector('.content');
        this.makeEditable(content);
    }
    
    // Good: Helper method for common functionality
    throttle(func, limit) {
        let inThrottle;
        return function(...args) {
            if (!inThrottle) {
                func.apply(this, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        };
    }
}

Practice Exercises

  1. Create a todo list that uses event delegation for all interactions
  2. Build a navigation menu that handles clicks through delegation
  3. Implement a data grid with sorting, filtering, and editing using delegation
  4. Create a comment system with nested replies using event delegation

Exercise Solutions (Try First!)

Click to see solutions
// 1. Todo List with Event Delegation
class TodoList {
    constructor(container) {
        this.container = container;
        this.todos = [];
        this.filter = 'all'; // all, active, completed
        
        this.render();
        this.setupDelegation();
    }
    
    setupDelegation() {
        this.container.addEventListener('click', (e) => {
            const target = e.target;
            const todoItem = target.closest('.todo-item');
            
            if (target.matches('.add-todo-btn')) {
                this.handleAddTodo();
            } else if (target.matches('.todo-checkbox')) {
                this.toggleTodo(todoItem.dataset.id);
            } else if (target.matches('.delete-todo')) {
                this.deleteTodo(todoItem.dataset.id);
            } else if (target.matches('.edit-todo')) {
                this.startEdit(todoItem);
            } else if (target.matches('.filter-btn')) {
                this.setFilter(target.dataset.filter);
            } else if (target.matches('.clear-completed')) {
                this.clearCompleted();
            }
        });
        
        this.container.addEventListener('keypress', (e) => {
            if (e.target.matches('.new-todo-input') && e.key === 'Enter') {
                this.handleAddTodo();
            } else if (e.target.matches('.edit-input') && e.key === 'Enter') {
                this.finishEdit(e.target);
            }
        });
        
        this.container.addEventListener('blur', (e) => {
            if (e.target.matches('.edit-input')) {
                this.finishEdit(e.target);
            }
        }, true);
    }
    
    render() {
        this.container.innerHTML = `
            
`; this.renderTodos(); this.updateFooter(); } renderTodos() { const list = this.container.querySelector('.todo-list'); const filteredTodos = this.getFilteredTodos(); list.innerHTML = filteredTodos.map(todo => `
${todo.text}
`).join(''); } handleAddTodo() { const input = this.container.querySelector('.new-todo-input'); const text = input.value.trim(); if (text) { this.todos.push({ id: Date.now(), text: text, completed: false }); input.value = ''; this.renderTodos(); this.updateFooter(); } } toggleTodo(id) { const todo = this.todos.find(t => t.id == id); if (todo) { todo.completed = !todo.completed; this.renderTodos(); this.updateFooter(); } } deleteTodo(id) { this.todos = this.todos.filter(t => t.id != id); this.renderTodos(); this.updateFooter(); } startEdit(todoItem) { const textSpan = todoItem.querySelector('.todo-text'); const currentText = textSpan.textContent; const input = document.createElement('input'); input.type = 'text'; input.className = 'edit-input'; input.value = currentText; textSpan.replaceWith(input); input.focus(); input.select(); } finishEdit(input) { const todoItem = input.closest('.todo-item'); const id = todoItem.dataset.id; const newText = input.value.trim(); if (newText) { const todo = this.todos.find(t => t.id == id); if (todo) { todo.text = newText; } } this.renderTodos(); } setFilter(filter) { this.filter = filter; this.renderTodos(); // Update active filter button this.container.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === filter); }); } getFilteredTodos() { switch (this.filter) { case 'active': return this.todos.filter(t => !t.completed); case 'completed': return this.todos.filter(t => t.completed); default: return this.todos; } } clearCompleted() { this.todos = this.todos.filter(t => !t.completed); this.renderTodos(); this.updateFooter(); } updateFooter() { const activeCount = this.todos.filter(t => !t.completed).length; const countElement = this.container.querySelector('.todo-count'); countElement.textContent = `${activeCount} item${activeCount !== 1 ? 's' : ''} left`; const clearButton = this.container.querySelector('.clear-completed'); clearButton.style.display = this.todos.some(t => t.completed) ? 'block' : 'none'; } } // 2. Navigation Menu with Delegation class NavigationMenu { constructor(container) { this.container = container; this.setupDelegation(); } setupDelegation() { this.container.addEventListener('click', (e) => { const target = e.target; // Handle main menu items if (target.matches('.nav-link')) { e.preventDefault(); this.handleNavClick(target); } // Handle dropdown toggles if (target.matches('.dropdown-toggle')) { e.preventDefault(); this.toggleDropdown(target); } // Handle submenu items if (target.matches('.submenu-link')) { e.preventDefault(); this.handleSubmenuClick(target); } // Close dropdowns when clicking outside if (!target.closest('.dropdown')) { this.closeAllDropdowns(); } }); // Keyboard navigation this.container.addEventListener('keydown', (e) => { const target = e.target; if (target.matches('.nav-link, .dropdown-toggle')) { this.handleKeyboardNav(e, target); } }); // Close dropdowns on escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.closeAllDropdowns(); } }); } handleNavClick(link) { // Remove active class from all links this.container.querySelectorAll('.nav-link').forEach(l => { l.classList.remove('active'); }); // Add active class to clicked link link.classList.add('active'); // Handle navigation const href = link.getAttribute('href'); console.log('Navigating to:', href); } toggleDropdown(toggle) { const dropdown = toggle.closest('.dropdown'); const isOpen = dropdown.classList.contains('open'); // Close all other dropdowns this.closeAllDropdowns(); // Toggle current dropdown if (!isOpen) { dropdown.classList.add('open'); toggle.setAttribute('aria-expanded', 'true'); // Focus first submenu item const firstItem = dropdown.querySelector('.submenu-link'); if (firstItem) firstItem.focus(); } } closeAllDropdowns() { this.container.querySelectorAll('.dropdown.open').forEach(dropdown => { dropdown.classList.remove('open'); dropdown.querySelector('.dropdown-toggle').setAttribute('aria-expanded', 'false'); }); } handleSubmenuClick(link) { console.log('Submenu clicked:', link.textContent); this.closeAllDropdowns(); } handleKeyboardNav(e, target) { const items = Array.from(this.container.querySelectorAll('.nav-link, .dropdown-toggle')); const currentIndex = items.indexOf(target); switch (e.key) { case 'ArrowRight': e.preventDefault(); const nextIndex = (currentIndex + 1) % items.length; items[nextIndex].focus(); break; case 'ArrowLeft': e.preventDefault(); const prevIndex = (currentIndex - 1 + items.length) % items.length; items[prevIndex].focus(); break; case 'ArrowDown': if (target.matches('.dropdown-toggle')) { e.preventDefault(); this.toggleDropdown(target); } break; case 'Enter': case ' ': e.preventDefault(); target.click(); break; } } }

Summary

Event propagation and delegation are powerful concepts that enable efficient event handling:

Next up: We'll explore form events and validation in detail!