The event object holds the key to advanced event handling, providing detailed information about every event that occurs. Understanding event propagation (bubbling and capturing) and the event delegation pattern is crucial for building efficient, scalable applications—especially when working with dynamic content. These concepts transform how you handle events in complex interfaces. Let's master advanced event handling!
The Event Object
When an event occurs, JavaScript automatically creates an event object containing information about the event and passes it to the handler function.
Accessing the Event Object
let button = document.getElementById('btn');
// Event object is passed as first parameter
button.addEventListener('click', function(event) {
console.log(event); // MouseEvent object
// Common naming conventions:
// event, e, evt
});
// Short version
button.addEventListener('click', function(e) {
console.log(e);
});
// Arrow function
button.addEventListener('click', (e) => {
console.log(e);
});Common Event Object Properties
<!DOCTYPE html>
<html>
<body>
<button id="btn">Click Me</button>
<input type="text" id="input" placeholder="Type here">
<script>
let button = document.getElementById('btn');
let input = document.getElementById('input');
// Mouse event properties
button.addEventListener('click', function(event) {
console.log('Event type:', event.type); // "click"
console.log('Target element:', event.target); // <button>
console.log('Current target:', event.currentTarget); // <button>
console.log('Mouse X:', event.clientX); // X coordinate
console.log('Mouse Y:', event.clientY); // Y coordinate
console.log('Page X:', event.pageX); // X with scroll
console.log('Page Y:', event.pageY); // Y with scroll
console.log('Ctrl key pressed?', event.ctrlKey); // true/false
console.log('Shift key pressed?', event.shiftKey); // true/false
console.log('Alt key pressed?', event.altKey); // true/false
});
// Keyboard event properties
input.addEventListener('keydown', function(event) {
console.log('Key pressed:', event.key); // "a", "Enter", etc.
console.log('Key code:', event.code); // "KeyA", "Enter"
console.log('Ctrl pressed?', event.ctrlKey); // true/false
console.log('Input value:', event.target.value); // Current input value
});
</script>
</body>
</html>event.target vs event.currentTarget
<!DOCTYPE html>
<html>
<body>
<div id="parent" style="padding: 20px; background: lightblue;">
Parent Div
<button id="child">Child Button</button>
</div>
<script>
let parent = document.getElementById('parent');
parent.addEventListener('click', function(event) {
console.log('Target (what was clicked):', event.target);
// Could be: <div id="parent"> or <button id="child">
console.log('Current target (where listener is):', event.currentTarget);
// Always: <div id="parent">
console.log('Same element?', event.target === event.currentTarget);
// false if you clicked button, true if you clicked div directly
// Practical use: check what was actually clicked
if (event.target.tagName === 'BUTTON') {
console.log('Button was clicked!');
} else {
console.log('Div background was clicked!');
}
});
</script>
</body>
</html>Event Object Methods
preventDefault() - Stop Default Behavior
<!DOCTYPE html>
<html>
<body>
<a href="https://example.com" id="link">Don't Navigate</a>
<form id="form">
<input type="text" required>
<button type="submit">Submit</button>
</form>
<script>
let link = document.getElementById('link');
let form = document.getElementById('form');
// Prevent link navigation
link.addEventListener('click', function(event) {
event.preventDefault(); // Link won't navigate!
console.log('Link clicked but navigation prevented');
});
// Prevent form submission (page reload)
form.addEventListener('submit', function(event) {
event.preventDefault(); // Form won't submit!
console.log('Form would have submitted');
// Handle form data with JavaScript instead
let input = this.querySelector('input').value;
console.log('Input value:', input);
});
// Prevent right-click context menu
document.addEventListener('contextmenu', function(event) {
event.preventDefault();
console.log('Right-click prevented');
});
</script>
</body>
</html>stopPropagation() - Stop Event Bubbling
<!DOCTYPE html>
<html>
<body>
<div id="outer" style="padding: 50px; background: lightblue;">
Outer
<div id="middle" style="padding: 30px; background: lightgreen;">
Middle
<button id="inner">Inner Button</button>
</div>
</div>
<script>
let outer = document.getElementById('outer');
let middle = document.getElementById('middle');
let inner = document.getElementById('inner');
// Without stopPropagation
outer.addEventListener('click', function() {
console.log('Outer clicked');
});
middle.addEventListener('click', function() {
console.log('Middle clicked');
});
inner.addEventListener('click', function(event) {
console.log('Inner clicked');
// Stop event from bubbling up
event.stopPropagation();
// Now only "Inner clicked" will log
// Without stopPropagation, all three would log
});
</script>
</body>
</html>stopImmediatePropagation() - Stop Other Listeners
let button = document.getElementById('btn');
button.addEventListener('click', function(event) {
console.log('First listener');
event.stopImmediatePropagation();
// Stops propagation AND prevents other listeners on this element
});
button.addEventListener('click', function() {
console.log('Second listener');
// This won't run!
});
button.addEventListener('click', function() {
console.log('Third listener');
// This won't run either!
});
// Only "First listener" will logEvent Propagation: Bubbling and Capturing
When an event occurs, it travels through the DOM in three phases: capturing, target, and bubbling.
Three Event Phases:
- Capturing Phase: Event travels DOWN from window to target
- Target Phase: Event reaches the target element
- Bubbling Phase: Event travels UP from target to window
Event Flow Example:
Click on button:
Window
↓ (capturing)
Document
↓
<html>
↓
<body>
↓
<div>
↓
<button> ← TARGET (event reaches here)
↑
<div>
↑ (bubbling)
<body>
↑
<html>
↑
Document
↑
Window
Most listeners use bubbling phase (default)Bubbling (Default)
<!DOCTYPE html>
<html>
<body>
<div id="grandparent" style="padding: 40px; background: lightblue;">
Grandparent
<div id="parent" style="padding: 30px; background: lightgreen;">
Parent
<button id="child">Child</button>
</div>
</div>
<script>
let grandparent = document.getElementById('grandparent');
let parent = document.getElementById('parent');
let child = document.getElementById('child');
// All listeners use bubbling (default)
grandparent.addEventListener('click', function() {
console.log('Grandparent (bubbling)');
});
parent.addEventListener('click', function() {
console.log('Parent (bubbling)');
});
child.addEventListener('click', function() {
console.log('Child (bubbling)');
});
// Click button → Output:
// Child (bubbling)
// Parent (bubbling)
// Grandparent (bubbling)
// Event bubbles UP from child to ancestors
</script>
</body>
</html>Capturing Phase
let grandparent = document.getElementById('grandparent');
let parent = document.getElementById('parent');
let child = document.getElementById('child');
// Use capturing phase (third parameter: true or { capture: true })
grandparent.addEventListener('click', function() {
console.log('Grandparent (capturing)');
}, true); // capturing!
parent.addEventListener('click', function() {
console.log('Parent (capturing)');
}, true); // capturing!
child.addEventListener('click', function() {
console.log('Child (target)');
});
// Click button → Output:
// Grandparent (capturing)
// Parent (capturing)
// Child (target)
// Event travels DOWN from ancestors to targetComplete Event Flow
let parent = document.getElementById('parent');
let child = document.getElementById('child');
// Capturing phase listener
parent.addEventListener('click', function() {
console.log('Parent (capturing)');
}, true);
// Target/bubbling phase listener
parent.addEventListener('click', function() {
console.log('Parent (bubbling)');
});
child.addEventListener('click', function() {
console.log('Child (target)');
});
// Click child → Output:
// 1. Parent (capturing) ← Capturing down
// 2. Child (target) ← Target phase
// 3. Parent (bubbling) ← Bubbling upEvent Delegation
Event delegation is a pattern where you attach a single listener to a parent element to handle events on multiple children—even dynamically added ones.
Why Event Delegation?
Delegation is more efficient
Without Delegation
// Without delegation (inefficient)
let buttons = document.querySelectorAll('.btn');
// Attach listener to each button
buttons.forEach(button => {
button.addEventListener('click', function() {
console.log('Clicked:', this.textContent);
});
});
// Problems:
// - Multiple listeners (memory intensive)
// - Doesn't work for dynamically added buttons
// - Must re-attach when adding new buttons
// Add new button
let newBtn = document.createElement('button');
newBtn.className = 'btn';
newBtn.textContent = 'New';
container.appendChild(newBtn);
// New button has no listener!With Delegation (Better)
// With delegation (efficient)
let container = document.getElementById('container');
// Single listener on parent
container.addEventListener('click', function(e) {
// Check if clicked element is a button
if (e.target.classList.contains('btn')) {
console.log('Clicked:', e.target.textContent);
}
});
// Benefits:
// - One listener (memory efficient)
// - Works for ALL buttons (existing + future)
// - Automatically handles dynamic content
// Add new button
let newBtn = document.createElement('button');
newBtn.className = 'btn';
newBtn.textContent = 'New';
container.appendChild(newBtn);
// New button automatically works!Event Delegation Pattern
<!DOCTYPE html>
<html>
<body>
<ul id="todoList">
<li>Task 1 <button class="delete">Delete</button></li>
<li>Task 2 <button class="delete">Delete</button></li>
<li>Task 3 <button class="delete">Delete</button></li>
</ul>
<button id="addTask">Add Task</button>
<script>
let todoList = document.getElementById('todoList');
let addTask = document.getElementById('addTask');
let taskCount = 3;
// Event delegation: listen on parent
todoList.addEventListener('click', function(event) {
// Check if delete button was clicked
if (event.target.classList.contains('delete')) {
// Remove the parent <li>
event.target.parentElement.remove();
console.log('Task deleted');
}
});
// Add new tasks dynamically
addTask.addEventListener('click', function() {
taskCount++;
let li = document.createElement('li');
li.innerHTML = ``;
todoList.appendChild(li);
// Delete button automatically works (event delegation!)
// No need to add listener to new button
});
</script>
</body>
</html>Advanced Delegation Patterns
<!DOCTYPE html>
<html>
<body>
<div id="gallery">
<!-- Images added dynamically -->
</div>
<button id="addImage">Add Image</button>
<script>
let gallery = document.getElementById('gallery');
let addImage = document.getElementById('addImage');
let imageCount = 0;
// Event delegation with multiple actions
gallery.addEventListener('click', function(event) {
let target = event.target;
// Check for different elements using closest()
let card = target.closest('.image-card');
if (!card) return; // Click wasn't on a card
// Handle different buttons within card
if (target.classList.contains('like-btn')) {
let likes = card.querySelector('.likes');
let count = parseInt(likes.textContent) + 1;
likes.textContent = count;
console.log('Liked image:', card.dataset.id);
}
if (target.classList.contains('delete-btn')) {
card.remove();
console.log('Deleted image:', card.dataset.id);
}
if (target.classList.contains('view-btn')) {
console.log('Viewing image:', card.dataset.id);
}
});
// Add images dynamically
addImage.addEventListener('click', function() {
imageCount++;
let card = document.createElement('div');
card.className = 'image-card';
card.dataset.id = imageCount;
card.innerHTML = ``;
gallery.appendChild(card);
});
// Add initial images
for (let i = 0; i < 3; i++) {
addImage.click();
}
</script>
</body>
</html>Delegation with matches() and closest()
let container = document.getElementById('container');
container.addEventListener('click', function(event) {
let target = event.target;
// Method 1: matches() - check if element matches selector
if (target.matches('.btn')) {
console.log('Button clicked');
}
if (target.matches('.item')) {
console.log('Item clicked');
}
// Method 2: closest() - find nearest ancestor matching selector
// Useful when clicking inside nested elements
let card = target.closest('.card');
if (card) {
console.log('Clicked somewhere in card:', card.dataset.id);
// Check what was specifically clicked
if (target.matches('.delete-btn')) {
card.remove();
}
if (target.matches('.edit-btn')) {
console.log('Edit card:', card.dataset.id);
}
}
});
// Example structure:
// <div class="card" data-id="1">
// <div class="card-content">
// <button class="edit-btn">Edit</button>
// <button class="delete-btn">Delete</button>
// </div>
// </div>
// Clicking button → target is button
// target.closest('.card') → finds the card ancestorPractical Examples
Example 1: Dynamic Shopping Cart
<!DOCTYPE html>
<html>
<head>
<style>
.cart-item { padding: 10px; margin: 5px; background: #f0f0f0; }
.cart-item button { margin: 0 5px; }
</style>
</head>
<body>
<div id="cart"></div>
<button id="addItem">Add Item</button>
<script>
let cart = document.getElementById('cart');
let addItem = document.getElementById('addItem');
let itemCount = 0;
// Event delegation for all cart actions
cart.addEventListener('click', function(event) {
let target = event.target;
let item = target.closest('.cart-item');
if (!item) return;
let itemId = item.dataset.id;
let quantity = item.querySelector('.quantity');
// Increase quantity
if (target.classList.contains('increase')) {
let count = parseInt(quantity.textContent);
quantity.textContent = count + 1;
updateTotal();
}
// Decrease quantity
if (target.classList.contains('decrease')) {
let count = parseInt(quantity.textContent);
if (count > 1) {
quantity.textContent = count - 1;
updateTotal();
}
}
// Remove item
if (target.classList.contains('remove')) {
item.remove();
updateTotal();
}
});
function updateTotal() {
let items = cart.querySelectorAll('.cart-item');
let total = 0;
items.forEach(item => {
let price = parseFloat(item.dataset.price);
let quantity = parseInt(item.querySelector('.quantity').textContent);
total += price * quantity;
});
console.log('Cart total: $' + total.toFixed(2));
}
// Add new items
addItem.addEventListener('click', function() {
itemCount++;
let price = (Math.random() * 50 + 10).toFixed(2);
let item = document.createElement('div');
item.className = 'cart-item';
item.dataset.id = itemCount;
item.dataset.price = price;
item.innerHTML = ``;
cart.appendChild(item);
updateTotal();
});
</script>
</body>
</html>Example 2: Sortable Table
<!DOCTYPE html>
<html>
<head>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; }
th { background: #4CAF50; color: white; cursor: pointer; }
th:hover { background: #45a049; }
</style>
</head>
<body>
<table id="dataTable">
<thead>
<tr>
<th data-column="name">Name ↕</th>
<th data-column="age">Age ↕</th>
<th data-column="email">Email ↕</th>
</tr>
</thead>
<tbody>
<tr><td>Alice</td><td>25</td><td>alice@test.com</td></tr>
<tr><td>Bob</td><td>30</td><td>bob@test.com</td></tr>
<tr><td>Charlie</td><td>22</td><td>charlie@test.com</td></tr>
</tbody>
</table>
<script>
let table = document.getElementById('dataTable');
let tbody = table.querySelector('tbody');
let sortStates = {}; // Track sort direction
// Event delegation on table header
table.addEventListener('click', function(event) {
// Only handle clicks on <th> elements
if (event.target.tagName !== 'TH') return;
let column = event.target.dataset.column;
let columnIndex = event.target.cellIndex;
// Toggle sort direction
sortStates[column] = sortStates[column] === 'asc' ? 'desc' : 'asc';
// Get all rows
let rows = Array.from(tbody.querySelectorAll('tr'));
// Sort rows
rows.sort((a, b) => {
let aValue = a.cells[columnIndex].textContent;
let bValue = b.cells[columnIndex].textContent;
// Try numeric comparison first
let aNum = parseFloat(aValue);
let bNum = parseFloat(bValue);
if (!isNaN(aNum) && !isNaN(bNum)) {
return sortStates[column] === 'asc' ? aNum - bNum : bNum - aNum;
}
// String comparison
if (sortStates[column] === 'asc') {
return aValue.localeCompare(bValue);
} else {
return bValue.localeCompare(aValue);
}
});
// Re-append rows in new order
rows.forEach(row => tbody.appendChild(row));
console.log(`Sorted by ${column} (${sortStates[column]})`);
});
</script>
</body>
</html>Event Object and Delegation Practice
Master advanced event handling patterns
console.log() to see your output in the console above.Key Takeaways
- Event object contains detailed information about the event
event.targetis element that triggered eventevent.currentTargetis element with listener attachedpreventDefault()stops default browser behaviorstopPropagation()stops event from bubbling- Events bubble UP from child to parent (default)
- Events can also capture DOWN from parent to child
- Event delegation: one listener on parent handles many children
- Delegation works with dynamic content automatically
- Use
matches()andclosest()for delegation
What's Next?
Congratulations! You've completed the DOM Manipulation section and now have a comprehensive understanding of how JavaScript interacts with HTML. You've learned element selection, modification, creation, event handling, and advanced patterns like event delegation.
You've now completed 42 out of 62 JavaScript tutorial pages—that's 68% complete! You've covered nine major sections: Introduction, Variables, Operators, Control Flow, Loops, Functions, Arrays, Objects, and DOM Manipulation. Excellent progress!
💪 Practice Challenge:
Before moving on, try creating:
- A dynamic todo list using event delegation
- A table that can sort by clicking column headers
- A shopping cart with increase/decrease/remove buttons (delegation)
- A form that prevents submission unless validation passes
- An image gallery with like/delete buttons using delegation