Event delegation is a powerful JavaScript technique that leverages event bubbling to handle events efficiently. Instead of attaching event listeners to multiple child elements, you attach a single listener to a parent element and handle events for all children through that parent.
When an event occurs on an element, it doesn't just trigger on that element alone. The event "bubbles up" through the DOM tree, triggering on each parent element until it reaches the document root. Event delegation takes advantage of this bubbling behavior.
Let me create a comprehensive example that demonstrates event delegation in action:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Delegation Demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background-color: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
margin-bottom: 30px;
}
.demo-section {
margin-bottom: 40px;
padding: 20px;
border: 2px dashed #ddd;
border-radius: 8px;
}
.demo-section h2 {
color: #666;
margin-top: 0;
}
.add-todo-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
input[type="text"] {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background-color: #0056b3;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
margin-bottom: 10px;
background-color: #f8f9fa;
border-radius: 4px;
transition: all 0.3s;
}
.todo-item:hover {
background-color: #e9ecef;
transform: translateX(5px);
}
.todo-item.completed {
opacity: 0.6;
background-color: #d4edda;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
}
.todo-checkbox {
margin-right: 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-text {
flex: 1;
cursor: pointer;
user-select: none;
}
.todo-actions {
display: flex;
gap: 10px;
}
.edit-btn,
.delete-btn {
padding: 5px 10px;
font-size: 14px;
border-radius: 3px;
}
.edit-btn {
background-color: #28a745;
}
.edit-btn:hover {
background-color: #218838;
}
.delete-btn {
background-color: #dc3545;
}
.delete-btn:hover {
background-color: #c82333;
}
.event-log {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 15px;
max-height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 14px;
}
.log-entry {
margin-bottom: 5px;
padding: 5px;
background-color: #fff;
border-radius: 3px;
}
.log-entry.click {
border-left: 3px solid #007bff;
}
.log-entry.change {
border-left: 3px solid #28a745;
}
.stats {
display: flex;
gap: 20px;
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #007bff;
}
.stat-label {
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>Event Delegation in JavaScript</h1>
<!-- Demo 1: Dynamic Todo List with Event Delegation -->
<div class="demo-section">
<h2>Demo 1: Todo List with Event Delegation</h2>
<p>All events are handled by a single listener on the parent <ul> element.</p>
<form class="add-todo-form" id="todoForm">
<input type="text" id="todoInput" placeholder="Enter a new todo..." required>
<button type="submit">Add Todo</button>
</form>
<ul class="todo-list" id="todoList">
<!-- Todo items will be dynamically added here -->
</ul>
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="totalTodos">0</div>
<div class="stat-label">Total Todos</div>
</div>
<div class="stat-item">
<div class="stat-value" id="completedTodos">0</div>
<div class="stat-label">Completed</div>
</div>
<div class="stat-item">
<div class="stat-value" id="pendingTodos">0</div>
<div class="stat-label">Pending</div>
</div>
</div>
</div>
<!-- Demo 2: Event Log -->
<div class="demo-section">
<h2>Demo 2: Event Delegation Log</h2>
<p>Watch how events bubble up and are captured by the parent element:</p>
<div class="event-log" id="eventLog"></div>
<button id="clearLog">Clear Log</button>
</div>
</div>
<script>
// Initialize todo counter
let todoCounter = 0;
// Get DOM elements
const todoForm = document.getElementById('todoForm');
const todoInput = document.getElementById('todoInput');
const todoList = document.getElementById('todoList');
const eventLog = document.getElementById('eventLog');
const clearLogBtn = document.getElementById('clearLog');
// Stats elements
const totalTodosEl = document.getElementById('totalTodos');
const completedTodosEl = document.getElementById('completedTodos');
const pendingTodosEl = document.getElementById('pendingTodos');
// Add some initial todos
const initialTodos = [
'Learn Event Delegation',
'Understand Event Bubbling',
'Practice JavaScript'
];
initialTodos.forEach(text => addTodo(text));
// Form submission handler
todoForm.addEventListener('submit', function (e) {
e.preventDefault();
const todoText = todoInput.value.trim();
if (todoText) {
addTodo(todoText);
todoInput.value = '';
logEvent('submit', 'Form submitted: ' + todoText);
}
});
// ========================================
// MAIN EVENT DELEGATION HANDLER
// This single listener handles ALL todo interactions
// ========================================
todoList.addEventListener('click', function (e) {
// Log the click event details
logEvent('click', `Clicked on: ${e.target.tagName}.${e.target.className}`);
// Get the closest todo item to handle clicks on child elements
const todoItem = e.target.closest('.todo-item');
if (!todoItem) return; // Click was outside a todo item
// Handle different types of clicks based on what was clicked
if (e.target.classList.contains('todo-checkbox')) {
// Toggle todo completion
toggleTodo(todoItem);
logEvent('change', 'Toggled todo: ' + todoItem.querySelector('.todo-text').textContent);
} else if (e.target.classList.contains('edit-btn')) {
// Edit todo
editTodo(todoItem);
logEvent('click', 'Edit button clicked');
} else if (e.target.classList.contains('delete-btn')) {
// Delete todo
deleteTodo(todoItem);
logEvent('click', 'Delete button clicked');
} else if (e.target.classList.contains('todo-text')) {
// Clicking on text also toggles the checkbox
const checkbox = todoItem.querySelector('.todo-checkbox');
checkbox.checked = !checkbox.checked;
toggleTodo(todoItem);
logEvent('click', 'Todo text clicked - toggled completion');
}
// Update stats after any change
updateStats();
});
// Also demonstrate event delegation with change events
todoList.addEventListener('change', function (e) {
if (e.target.classList.contains('todo-checkbox')) {
logEvent('change', 'Checkbox changed via direct interaction');
}
});
// Function to add a new todo
function addTodo(text) {
todoCounter++;
const todoItem = document.createElement('li');
todoItem.className = 'todo-item';
todoItem.dataset.todoId = todoCounter;
todoItem.innerHTML = `
<input type="checkbox" class="todo-checkbox">
<span class="todo-text">${escapeHtml(text)}</span>
<div class="todo-actions">
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
</div>
`;
todoList.appendChild(todoItem);
updateStats();
logEvent('add', 'New todo added: ' + text);
}
// Function to toggle todo completion
function toggleTodo(todoItem) {
todoItem.classList.toggle('completed');
}
// Function to edit a todo
function editTodo(todoItem) {
const textElement = todoItem.querySelector('.todo-text');
const currentText = textElement.textContent;
const newText = prompt('Edit todo:', currentText);
if (newText !== null && newText.trim() !== '') {
textElement.textContent = newText.trim();
logEvent('edit', 'Todo edited: ' + newText.trim());
}
}
// Function to delete a todo
function deleteTodo(todoItem) {
const todoText = todoItem.querySelector('.todo-text').textContent;
if (confirm('Delete this todo: "' + todoText + '"?')) {
todoItem.remove();
updateStats();
logEvent('delete', 'Todo deleted: ' + todoText);
}
}
// Function to update statistics
function updateStats() {
const allTodos = todoList.querySelectorAll('.todo-item');
const completedCount = todoList.querySelectorAll('.todo-item.completed').length;
totalTodosEl.textContent = allTodos.length;
completedTodosEl.textContent = completedCount;
pendingTodosEl.textContent = allTodos.length - completedCount;
}
// Function to log events
function logEvent(type, message) {
const logEntry = document.createElement('div');
logEntry.className = 'log-entry ' + type;
const timestamp = new Date().toLocaleTimeString();
logEntry.textContent = `[${timestamp}] ${message}`;
eventLog.insertBefore(logEntry, eventLog.firstChild);
// Keep only last 10 entries
while (eventLog.children.length > 10) {
eventLog.removeChild(eventLog.lastChild);
}
}
// Clear log button
clearLogBtn.addEventListener('click', function () {
eventLog.innerHTML = '';
logEvent('clear', 'Log cleared');
});
// Utility function to escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Initial stats update
updateStats();
// ========================================
// DEMONSTRATION: Without Event Delegation
// This shows what you would need to do WITHOUT event delegation
// (Commented out - for demonstration purposes only)
// ========================================
/*
// WITHOUT event delegation, you would need to:
// 1. Add listeners to each todo item individually
// 2. Re-attach listeners whenever new items are added
// 3. Remove listeners when items are deleted (to prevent memory leaks)
function attachTodoListeners(todoItem) {
const checkbox = todoItem.querySelector('.todo-checkbox');
const editBtn = todoItem.querySelector('.edit-btn');
const deleteBtn = todoItem.querySelector('.delete-btn');
const todoText = todoItem.querySelector('.todo-text');
checkbox.addEventListener('change', function() {
toggleTodo(todoItem);
});
editBtn.addEventListener('click', function() {
editTodo(todoItem);
});
deleteBtn.addEventListener('click', function() {
deleteTodo(todoItem);
});
todoText.addEventListener('click', function() {
checkbox.checked = !checkbox.checked;
toggleTodo(todoItem);
});
}
// You'd have to call this for EVERY new todo item!
// This creates many event listeners and uses more memory
*/
</script>
</body>
</html>
When you click on an element, the event doesn't just fire on that element. It bubbles up through all parent elements in the DOM tree. For example, clicking a button inside a div also triggers click events on the div, its parent elements, and eventually the document itself.
The example I've created demonstrates a real-world scenario where event delegation shines - a dynamic todo list where items can be added, edited, deleted, and marked as complete, all handled by a single event listener on the parent <ul>
element.
No comments:
Post a Comment