Advanced Event Handling
Beyond basic event listeners, understanding event propagation, delegation, and advanced patterns will make your code more efficient and maintainable.
Event Propagation
Events propagate through the DOM in three phases:
1. Capture phase: From document → target
2. Target phase: At the target element
3. Bubble phase: From target → document
Event Bubbling
Events bubble up from child to parent:
<div id="outer">
<div id="middle">
<button id="inner">Click me</button>
</div>
</div>
const outer = document.querySelector('#outer');
const middle = document.querySelector('#middle');
const inner = document.querySelector('#inner');
outer.addEventListener('click', () => {
console.log('Outer clicked');
});
middle.addEventListener('click', () => {
console.log('Middle clicked');
});
inner.addEventListener('click', () => {
console.log('Inner clicked');
});
// Clicking button logs:
// "Inner clicked"
// "Middle clicked"
// "Outer clicked"
Stopping Propagation
inner.addEventListener('click', (e) => {
console.log('Inner clicked');
e.stopPropagation(); // Stop bubbling
});
// Now only logs: "Inner clicked"
Event Capture Phase
Listen during capture instead of bubble:
// Third parameter = true for capture phase
outer.addEventListener('click', () => {
console.log('Outer clicked (capture)');
}, true);
inner.addEventListener('click', () => {
console.log('Inner clicked');
});
// Logs:
// "Outer clicked (capture)" (capture phase, top-down)
// "Inner clicked" (bubble phase, bottom-up)
Event Object
The event object contains useful information:
element.addEventListener('click', (e) => {
// Target: element that triggered the event
console.log(e.target); // <button>
// CurrentTarget: element with the listener
console.log(e.currentTarget); // element
// Event type
console.log(e.type); // "click"
// Mouse position
console.log(e.clientX, e.clientY); // Viewport coordinates
console.log(e.pageX, e.pageY); // Document coordinates
// Modifier keys
console.log(e.ctrlKey); // Ctrl pressed?
console.log(e.shiftKey); // Shift pressed?
console.log(e.altKey); // Alt pressed?
// Button clicked (mouse events)
console.log(e.button); // 0: left, 1: middle, 2: right
// Timestamp
console.log(e.timeStamp);
});
target vs currentTarget
<div id="parent">
<button id="child">Click me</button>
</div>
const parent = document.querySelector('#parent');
parent.addEventListener('click', (e) => {
console.log('Target:', e.target); // <button> (what was clicked)
console.log('CurrentTarget:', e.currentTarget); // <div> (what has the listener)
});
// Clicking button logs:
// Target: <button id="child">
// CurrentTarget: <div id="parent">
Event Delegation
Attach one listener to a parent instead of many listeners to children:
Without Delegation (Inefficient)
// ❌ Add listener to every item
const items = document.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', () => {
console.log('Item clicked:', item.textContent);
});
});
// Problems:
// - 100 items = 100 listeners
// - Doesn't work for dynamically added items
With Delegation (Efficient)
// ✅ One listener on parent
const list = document.querySelector('#list');
list.addEventListener('click', (e) => {
// Check if clicked element is an item
if (e.target.classList.contains('item')) {
console.log('Item clicked:', e.target.textContent);
}
});
// Benefits:
// - 1000 items = 1 listener
// - Works for dynamically added items
Practical Delegation Examples
Delete buttons:
<ul id="todo-list">
<li>
Task 1
<button class="delete-btn" data-id="1">Delete</button>
</li>
<li>
Task 2
<button class="delete-btn" data-id="2">Delete</button>
</li>
</ul>
const list = document.querySelector('#todo-list');
list.addEventListener('click', (e) => {
// Find if click was on delete button
const deleteBtn = e.target.closest('.delete-btn');
if (deleteBtn) {
const id = deleteBtn.dataset.id;
const listItem = deleteBtn.closest('li');
console.log('Delete task:', id);
listItem.remove();
}
});
Table row actions:
const table = document.querySelector('#data-table');
table.addEventListener('click', (e) => {
const row = e.target.closest('tr');
if (!row) return;
// Edit button
if (e.target.classList.contains('edit-btn')) {
console.log('Edit row:', row.dataset.id);
}
// Delete button
if (e.target.classList.contains('delete-btn')) {
if (confirm('Delete this row?')) {
row.remove();
}
}
});
Preventing Default Behavior
preventDefault()
Stop browser’s default action:
// Prevent form submission
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
e.preventDefault(); // Don't submit form
// Handle with JavaScript instead
const formData = new FormData(e.target);
console.log('Form data:', Object.fromEntries(formData));
});
// Prevent link navigation
const link = document.querySelector('a');
link.addEventListener('click', (e) => {
e.preventDefault();
console.log('Link clicked, but not navigating');
});
// Prevent context menu
document.addEventListener('contextmenu', (e) => {
e.preventDefault();
console.log('Right-click disabled');
});
Common Use Cases
// Custom file upload
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (e) => {
e.preventDefault();
const files = e.target.files;
// Handle files with custom logic
});
// Drag and drop
const dropZone = document.querySelector('#drop-zone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // Allow drop
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
const files = e.dataTransfer.files;
console.log('Files dropped:', files);
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl+S to save
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('Saving...');
}
});
Keyboard Events
Key Events
const input = document.querySelector('input');
// Key pressed down
input.addEventListener('keydown', (e) => {
console.log('Key down:', e.key);
});
// Key released
input.addEventListener('keyup', (e) => {
console.log('Key up:', e.key);
});
// Character typed (deprecated, use keydown)
input.addEventListener('keypress', (e) => {
console.log('Key press:', e.key);
});
Keyboard Properties
document.addEventListener('keydown', (e) => {
// Key name
console.log('Key:', e.key); // "a", "Enter", "Escape", "ArrowUp"
console.log('Code:', e.code); // "KeyA", "Enter", "Escape", "ArrowUp"
// Modifiers
console.log('Ctrl:', e.ctrlKey);
console.log('Shift:', e.shiftKey);
console.log('Alt:', e.altKey);
console.log('Meta:', e.metaKey); // Command/Windows key
});
Keyboard Shortcuts
// Global shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl+K
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
openSearchModal();
}
// Escape to close
if (e.key === 'Escape') {
closeModal();
}
// Arrow navigation
if (e.key === 'ArrowUp') {
navigateUp();
}
if (e.key === 'ArrowDown') {
navigateDown();
}
});
Input Filtering
// Numbers only
const numberInput = document.querySelector('#number');
numberInput.addEventListener('keydown', (e) => {
// Allow: backspace, delete, arrows, tab
const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'];
if (allowedKeys.includes(e.key)) {
return;
}
// Allow: Ctrl+A, Ctrl+C, Ctrl+V
if (e.ctrlKey) {
return;
}
// Block non-numeric keys
if (!/^[0-9]$/.test(e.key)) {
e.preventDefault();
}
});
Mouse Events
Mouse Event Types
const element = document.querySelector('#box');
// Click events
element.addEventListener('click', () => {
console.log('Single click');
});
element.addEventListener('dblclick', () => {
console.log('Double click');
});
// Mouse button events
element.addEventListener('mousedown', () => {
console.log('Button pressed');
});
element.addEventListener('mouseup', () => {
console.log('Button released');
});
// Mouse movement
element.addEventListener('mouseenter', () => {
console.log('Mouse entered (no bubbling)');
});
element.addEventListener('mouseleave', () => {
console.log('Mouse left (no bubbling)');
});
element.addEventListener('mouseover', () => {
console.log('Mouse over (bubbles)');
});
element.addEventListener('mouseout', () => {
console.log('Mouse out (bubbles)');
});
element.addEventListener('mousemove', (e) => {
console.log('Mouse position:', e.clientX, e.clientY);
});
Mouse Coordinates
element.addEventListener('click', (e) => {
// Relative to viewport
console.log('Client:', e.clientX, e.clientY);
// Relative to page (with scroll)
console.log('Page:', e.pageX, e.pageY);
// Relative to element
const rect = element.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
console.log('Element:', x, y);
});
Drag Example
let isDragging = false;
let offsetX, offsetY;
const draggable = document.querySelector('#draggable');
draggable.addEventListener('mousedown', (e) => {
isDragging = true;
const rect = draggable.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
draggable.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
draggable.style.left = (e.clientX - offsetX) + 'px';
draggable.style.top = (e.clientY - offsetY) + 'px';
});
document.addEventListener('mouseup', () => {
isDragging = false;
draggable.style.cursor = 'grab';
});
Custom Events
Create and dispatch custom events:
// Create custom event
const customEvent = new CustomEvent('userLogin', {
detail: {
username: 'alice',
timestamp: Date.now()
}
});
// Listen for custom event
document.addEventListener('userLogin', (e) => {
console.log('User logged in:', e.detail.username);
});
// Dispatch event
document.dispatchEvent(customEvent);
Practical Custom Events
// Component communication
class TodoList {
addTodo(todo) {
// ... add todo logic
// Notify other components
const event = new CustomEvent('todoAdded', {
detail: { todo }
});
this.element.dispatchEvent(event);
}
}
const todoList = new TodoList();
todoList.element.addEventListener('todoAdded', (e) => {
console.log('New todo:', e.detail.todo);
updateStats();
});
Event Performance
Debouncing
Limit how often a function executes:
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Usage: Search as user types
const searchInput = document.querySelector('#search');
const handleSearch = debounce((e) => {
console.log('Searching for:', e.target.value);
// Expensive search operation
}, 300);
searchInput.addEventListener('input', handleSearch);
Throttling
Execute at most once per time period:
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage: Scroll event
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', handleScroll);
Best Practices
✅ DO:
- Use event delegation for lists
- Remove event listeners when elements are removed
- Use
preventDefault()appropriately - Debounce/throttle expensive operations
- Check
e.targetin delegated events
❌ DON’T:
- Attach many listeners to many elements
- Forget to remove listeners (memory leaks)
- Stop propagation unnecessarily
- Put heavy logic in mousemove/scroll handlers
- Use inline event handlers (onclick="…")
Summary
| Concept | Method | Use Case |
|---|---|---|
| Stop bubbling | e.stopPropagation() |
Prevent parent handlers |
| Prevent default | e.preventDefault() |
Stop form submit, link click |
| Event delegation | Listener on parent | Efficient list handling |
| Target element | e.target |
What was clicked |
| Current element | e.currentTarget |
What has the listener |
| Debounce | Custom function | Search input, resize |
| Throttle | Custom function | Scroll, mousemove |
| Custom events | CustomEvent |
Component communication |
Next Article: Form Validation