Problem Statement
Explain event bubbling, capturing, and event delegation. Provide practical examples and use cases.
Explanation
Events in JavaScript propagate through the DOM in three phases: capturing, target, and bubbling. Understanding this is crucial for effective event handling.
Event capturing phase:
Event travels from root down to target element. Window to document to body to parent to target. Use third parameter true in addEventListener. Rarely used in practice. Useful for intercepting events before they reach target.
Target phase:
Event reaches the actual target element. Target's event handlers execute. This is where the event originated.
Bubbling phase:
Event bubbles up from target to root. Most event handlers work in bubble phase. This is the default behavior. Target to parent to body to document to window.
Event delegation pattern:
Attach single listener to parent element. Handle events from multiple children. Check event.target to identify which child triggered event. More efficient than many individual listeners. Automatically works for dynamically added elements.
Stopping propagation:
Event.stopPropagation stops bubbling to parents. Event.stopImmediatePropagation stops other handlers on same element. Use sparingly as it can break delegation patterns.
Event.target vs event.currentTarget:
Target is the element that triggered the event, the innermost clicked element. CurrentTarget is the element with the listener attached, the element currently handling the event. During bubbling, target stays same but currentTarget changes.
Practical use cases:
Event delegation for lists and tables. Click outside to close dropdowns. Modal background click to close. Navigation menus. Dynamic form fields. Infinite scroll lists.
Benefits of delegation:
Better performance with fewer listeners. Less memory usage. Works with dynamic content. Cleaner code with centralized handling. Easier maintenance.
When not to use delegation:
Events that do not bubble like focus and blur. Need specific this binding for each element. Very specific single-element behavior. Performance-critical handlers on many elements.
Understanding event propagation is essential for building efficient, maintainable event-driven applications.
Code Solution
SolutionRead Only
// EVENT PROPAGATION PHASES
const outer = document.getElementById('outer');
const middle = document.getElementById('middle');
const inner = document.getElementById('inner');
// Capture phase (true parameter)
outer.addEventListener('click', () => {
console.log('Outer CAPTURE');
}, true);
middle.addEventListener('click', () => {
console.log('Middle CAPTURE');
}, true);
inner.addEventListener('click', () => {
console.log('Inner CAPTURE');
}, true);
// Bubble phase (default)
outer.addEventListener('click', () => {
console.log('Outer BUBBLE');
});
middle.addEventListener('click', () => {
console.log('Middle BUBBLE');
});
inner.addEventListener('click', () => {
console.log('Inner BUBBLE');
});
// When inner clicked, output:
// Outer CAPTURE
// Middle CAPTURE
// Inner CAPTURE
// Inner BUBBLE
// Middle BUBBLE
// Outer BUBBLE
// EVENT DELEGATION - Todo List
const todoList = document.getElementById('todoList');
// One listener for all todos
todoList.addEventListener('click', (event) => {
const target = event.target;
// Delete button clicked
if (target.classList.contains('delete')) {
const li = target.closest('li');
li.remove();
}
// Checkbox clicked
if (target.type === 'checkbox') {
const li = target.closest('li');
li.classList.toggle('completed');
}
// Edit button clicked
if (target.classList.contains('edit')) {
const li = target.closest('li');
const span = li.querySelector('.text');
span.contentEditable = true;
span.focus();
}
});
// Add todos dynamically (delegation handles them automatically)
function addTodo(text) {
const li = document.createElement('li');
li.innerHTML = `
<input type="checkbox">
<span class="text">${text}</span>
<button class="edit">Edit</button>
<button class="delete">Delete</button>
`;
todoList.appendChild(li);
}
// EVENT DELEGATION - Table
const table = document.getElementById('dataTable');
table.addEventListener('click', (event) => {
const target = event.target;
// Find the row (even if clicked on cell)
const row = target.closest('tr');
if (row && row.dataset.id) {
console.log('Row clicked:', row.dataset.id);
// Check which column
const cell = target.closest('td');
if (cell) {
const columnIndex = cell.cellIndex;
console.log('Column:', columnIndex);
// Handle specific columns
if (columnIndex === 3) { // Actions column
if (target.classList.contains('edit')) {
editRow(row.dataset.id);
}
if (target.classList.contains('delete')) {
deleteRow(row.dataset.id);
}
}
}
}
});
// CLICK OUTSIDE TO CLOSE
const dropdown = document.getElementById('dropdown');
const button = document.getElementById('dropdownBtn');
// Open dropdown
button.addEventListener('click', (event) => {
event.stopPropagation(); // Don't trigger document listener
dropdown.classList.add('open');
});
// Close when clicking outside
document.addEventListener('click', (event) => {
if (!dropdown.contains(event.target)) {
dropdown.classList.remove('open');
}
});
// Don't close when clicking inside dropdown
dropdown.addEventListener('click', (event) => {
event.stopPropagation();
});
// MODAL WITH EVENT DELEGATION
const modal = document.getElementById('modal');
const modalContent = modal.querySelector('.modal-content');
// Close on background click
modal.addEventListener('click', (event) => {
// Only close if clicked directly on modal, not children
if (event.target === modal) {
closeModal();
}
});
// Alternative: check if clicked outside content
modal.addEventListener('click', (event) => {
if (!modalContent.contains(event.target)) {
closeModal();
}
});
// NAVIGATION MENU
const nav = document.getElementById('nav');
nav.addEventListener('click', (event) => {
const link = event.target.closest('a');
if (link) {
// Check if has submenu
const parent = link.parentElement;
if (parent.querySelector('.submenu')) {
event.preventDefault();
parent.classList.toggle('open');
}
}
});
// FORM WITH DELEGATION
const form = document.getElementById('dynamicForm');
// Handle all inputs with delegation
form.addEventListener('input', (event) => {
const input = event.target;
if (input.type === 'email') {
validateEmail(input);
}
if (input.type === 'password') {
validatePassword(input);
}
if (input.hasAttribute('data-validate')) {
validateField(input);
}
});
// Handle all buttons
form.addEventListener('click', (event) => {
if (event.target.matches('.add-field')) {
addField();
}
if (event.target.matches('.remove-field')) {
event.target.closest('.field').remove();
}
});
// PERFORMANCE COMPARISON
// Bad: Many listeners (memory intensive)
const items = document.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', handleClick);
});
// If 1000 items = 1000 listeners!
// Good: One listener with delegation
const container = document.getElementById('container');
container.addEventListener('click', (event) => {
if (event.target.classList.contains('item')) {
handleClick(event);
}
});
// Only 1 listener for 1000 items!
// STOPPING PROPAGATION
inner.addEventListener('click', (event) => {
console.log('Inner');
event.stopPropagation(); // Stops bubbling
// Outer handlers won't fire
});
// Stop other handlers on same element
button.addEventListener('click', (event) => {
console.log('First');
event.stopImmediatePropagation();
});
button.addEventListener('click', () => {
console.log('Second'); // Won't execute
});