Introduction to Event Handlers
In our previous lecture, we explored the DOM event model and the various types of events available in the browser. Now we'll dive deeper into event handlers and listeners—the code that executes when events occur.
Event handlers are the bridge between user interactions and your application logic. Understanding how to properly implement and manage them is crucial for creating responsive, interactive web applications.
- Event Handler
- A function that executes in response to a specific event
- Event Listener
- The connection between an event type, a target element, and a handler function
Real-World Analogy: The Restaurant
Think of event handling like a restaurant's service system:
- The customer is like the user interacting with your page
- The table is like an element in your DOM
- Pressing the service button is like triggering an event (e.g., a click)
- The waiter is like the event listener, assigned to watch for the button press
- The specific service provided is like the handler function that executes
Just as a restaurant might assign different waiters to different sections or have servers respond differently based on the type of service requested, you can set up multiple listeners for different events and handler functions to respond appropriately.
Registering Event Handlers
JavaScript provides multiple methods for registering event handlers. Let's explore them in detail:
1. addEventListener() Method
The modern, recommended way to register event handlers:
// Basic syntax
element.addEventListener(eventType, handlerFunction, options);
// Simple example
const button = document.getElementById('submit-button');
button.addEventListener('click', function(event) {
console.log('Button clicked!');
submitForm();
});
// Using a named function
function handleClick(event) {
console.log('Button clicked!');
submitForm();
}
button.addEventListener('click', handleClick);
The options Parameter
The third parameter to addEventListener() can be a boolean (for legacy support) or an options object with the following properties:
// Using options object
button.addEventListener('click', handleClick, {
capture: false, // Whether the event should be captured in capturing phase (default: false)
once: true, // Whether the listener should be invoked at most once (default: false)
passive: true // Whether the listener will never call preventDefault() (default: false)
});
Benefits of addEventListener()
- Allows attaching multiple listeners to the same event
- Provides options for fine-tuning event behavior
- Supports both event bubbling and capturing phases
- Cleanly separates HTML from JavaScript
- Handlers can be easily removed with
removeEventListener()
2. Event Handler Properties
A simpler but more limited approach using on-event properties:
// Basic syntax
element.oneventname = handlerFunction;
// Examples
const button = document.getElementById('submit-button');
button.onclick = function(event) {
console.log('Button clicked!');
submitForm();
};
const input = document.getElementById('username');
input.onchange = handleInputChange;
const form = document.getElementById('signup-form');
form.onsubmit = validateForm;
Limitations of Event Handler Properties
- Can only assign one handler per event type per element
- New assignments overwrite previous handlers
- No support for capture phase or options
- No built-in method for removing handlers (must set to null)
// Overwriting previous handler
button.onclick = function() { console.log('First handler'); };
button.onclick = function() { console.log('Second handler'); }; // First handler is lost
// Removing handler
button.onclick = null;
3. Inline Event Handlers (Not Recommended)
Defining handlers directly in HTML attributes:
<button onclick="handleClick(event)">Click Me</button>
<input onchange="console.log('Value changed to: ' + this.value)">
<form onsubmit="return validateForm(this)">
Avoid Inline Event Handlers
Inline event handlers have significant drawbacks:
- Mix HTML structure with behavior (poor separation of concerns)
- Limited access to closure variables
- Poor maintainability and readability for complex applications
- Can create security vulnerabilities if combined with user input
- Limited to one handler per event
- Cannot be easily removed programmatically
Handler Registration Comparison
| Feature | addEventListener() | Event Properties | Inline Attributes |
|---|---|---|---|
| Multiple handlers | ✅ Supported | ❌ Overwritten | ❌ Limited to one |
| Capture phase | ✅ Supported | ❌ Not supported | ❌ Not supported |
| Event options | ✅ Supported | ❌ Not supported | ❌ Not supported |
| Separation of concerns | ✅ Good | ✅ Good | ❌ Poor |
| Browser compatibility | ✅ IE9+, all modern | ✅ All browsers | ✅ All browsers |
| Easy to remove | ✅ removeEventListener() | ✅ Set to null | ❌ Requires DOM manipulation |
Creating Effective Event Handlers
Writing good event handlers involves more than just registering functions. Here are important patterns and practices:
Handler Scope and 'this' Context
Understanding how this works in event handlers is crucial:
// In a regular function, 'this' refers to the element the listener is attached to
document.getElementById('button').addEventListener('click', function(event) {
console.log(this); // Refers to the button element
this.classList.toggle('active');
});
// Arrow functions don't have their own 'this' and inherit from the parent scope
const button = document.getElementById('button');
button.addEventListener('click', (event) => {
console.log(this); // Does NOT refer to the button (likely window or undefined in strict mode)
// button.classList.toggle('active'); // Need to use the variable instead
});
// In object methods, 'this' may need binding to maintain context
const controller = {
count: 0,
increment: function() {
this.count++;
this.updateUI();
},
updateUI: function() {
document.getElementById('counter').textContent = this.count;
},
setupListeners: function() {
// Without binding, 'this' would refer to the button, not the controller
document.getElementById('increment-button').addEventListener('click', this.increment.bind(this));
// Alternative using arrow function
document.getElementById('increment-button').addEventListener('click', () => {
this.increment();
});
}
};
Accessing Event Information
The event object provides crucial information about what happened:
document.getElementById('product-list').addEventListener('click', function(event) {
// Get information about the event
console.log('Event type:', event.type);
console.log('Target element:', event.target);
console.log('Current target:', event.currentTarget);
// Mouse event information
console.log('Click position:', event.clientX, event.clientY);
// Get data from the clicked element
if (event.target.classList.contains('buy-button')) {
const productId = event.target.closest('.product-card').dataset.productId;
const productName = event.target.closest('.product-card').querySelector('.product-name').textContent;
const price = parseFloat(event.target.dataset.price);
addToCart(productId, productName, price);
}
});
Preventing Default Behavior
Many events have default browser actions that you might want to prevent:
// Prevent form submission to handle it with JavaScript
document.getElementById('signup-form').addEventListener('submit', function(event) {
// Prevent the default form submission
event.preventDefault();
// Perform client-side validation
if (validateForm()) {
// Submit via AJAX instead
submitFormAsync(this);
}
});
// Prevent link navigation
document.querySelector('.tabs a').addEventListener('click', function(event) {
event.preventDefault();
// Show tab content instead of navigating
showTab(this.getAttribute('href').substring(1));
});
// Prevent context menu on right-click
document.getElementById('custom-menu-area').addEventListener('contextmenu', function(event) {
event.preventDefault();
// Show custom context menu instead
showCustomMenu(event.clientX, event.clientY);
});
Common Default Behaviors to Prevent
- submit: Form submission and page reload
- click on links: Page navigation
- contextmenu: Browser's context menu
- keydown: Browser shortcuts or text input
- dragstart: Default drag behavior for images and links
- touchstart/touchmove: Scrolling, zooming, or selection on mobile
Stopping Event Propagation
Preventing events from bubbling up to parent elements:
// Stop a click from bubbling up to parent elements
document.getElementById('delete-button').addEventListener('click', function(event) {
// Stop the event from reaching parent handlers
event.stopPropagation();
// Show delete confirmation dialog
if (confirm('Are you sure you want to delete this item?')) {
deleteItem(this.dataset.itemId);
}
});
// This parent handler won't receive clicks from the delete button
document.getElementById('item-container').addEventListener('click', function(event) {
// This will be triggered for clicks on the container or any child
// except for the delete button (due to stopPropagation above)
selectItem(event.target.closest('.item'));
});
Use stopPropagation() Carefully
Stopping propagation can interfere with other event listeners in your application:
- Event delegation patterns rely on bubbling
- Analytics tracking may depend on events bubbling to document
- Third-party libraries might expect events to propagate
Only use stopPropagation() when you have a specific reason to prevent an event from bubbling further.
Immediate Propagation
Stopping all handlers on the current element:
// Register multiple click handlers
const button = document.getElementById('multi-action-button');
// First handler
button.addEventListener('click', function(event) {
console.log('First handler');
// Stop other handlers from executing
event.stopImmediatePropagation();
});
// Second handler (won't execute if first handler is triggered)
button.addEventListener('click', function(event) {
console.log('Second handler'); // This won't run due to stopImmediatePropagation
});
The stopImmediatePropagation() method is stronger than stopPropagation(). It prevents the event from both bubbling up and from triggering any other handlers on the current element.
Advanced Handler Patterns
Beyond basic event registration, there are several advanced patterns that help create more maintainable and efficient code:
Event Delegation
Instead of attaching handlers to individual elements, attach a single handler to a parent element and use event.target to determine what was clicked:
// Instead of this (attaching to each button)
document.querySelectorAll('.delete-btn').forEach(button => {
button.addEventListener('click', handleDelete);
});
// Use event delegation
document.getElementById('items-container').addEventListener('click', function(event) {
// Check if a delete button was clicked
if (event.target.classList.contains('delete-btn')) {
handleDelete.call(event.target, event);
}
// Check if an edit button was clicked
else if (event.target.classList.contains('edit-btn')) {
handleEdit.call(event.target, event);
}
// Check if an item was clicked
else if (event.target.closest('.item')) {
selectItem(event.target.closest('.item'));
}
});
Benefits of Event Delegation
- Memory Efficiency: One handler instead of many
- Dynamic Elements: Works with elements added after initial page load
- Less Code: Simpler setup and maintenance
- Cleaner Markup: No need to attach handlers to every element
Handling One-Time Events
Sometimes you want an event handler to run only once:
// Method 1: Using the 'once' option (modern browsers)
button.addEventListener('click', handleFirstClick, { once: true });
// Method 2: Removing the listener in the handler
button.addEventListener('click', function handleFirstClick(event) {
console.log('First click!');
// Do something
// Remove this handler
button.removeEventListener('click', handleFirstClick);
});
Example: Tutorial Overlay
const tutorialOverlay = document.getElementById('tutorial-overlay');
const startButton = document.getElementById('start-tutorial');
startButton.addEventListener('click', function() {
// Show the tutorial overlay
tutorialOverlay.classList.add('visible');
// Listen for the first click anywhere to dismiss the overlay
document.addEventListener('click', function dismissTutorial(event) {
// Ignore clicks on the overlay itself
if (event.target !== tutorialOverlay && !tutorialOverlay.contains(event.target)) {
tutorialOverlay.classList.remove('visible');
// Remove this one-time handler
document.removeEventListener('click', dismissTutorial);
// Set a flag in localStorage so we don't show the tutorial again
localStorage.setItem('tutorialShown', 'true');
}
});
});
Debouncing and Throttling
Controlling the rate at which event handlers fire:
Debounce Function
// Execute a function after a delay, resetting the timer when the event fires again
function debounce(func, delay) {
let timeoutId;
return function(...args) {
// Clear any existing timeout
clearTimeout(timeoutId);
// Set a new timeout
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Example: Search input that only triggers API calls after typing stops
const searchInput = document.getElementById('search-input');
// Create a debounced search function
const debouncedSearch = debounce(function(event) {
const query = event.target.value;
console.log('Searching for:', query);
fetchSearchResults(query);
}, 500); // Wait 500ms after typing stops
// Attach the debounced handler
searchInput.addEventListener('input', debouncedSearch);
Throttle Function
// Execute a function at most once per specified time period
function throttle(func, limit) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
func.apply(this, args);
}
};
}
// Example: Scroll handler that updates UI at most every 100ms
const scrollHandler = throttle(function() {
updateScrollBasedUI();
}, 100);
window.addEventListener('scroll', scrollHandler);
Debounce vs. Throttle: When To Use Each
| Function | Behavior | Best For |
|---|---|---|
debounce() |
Waits until activity stops before executing |
|
throttle() |
Executes periodically during continuous activity |
|
Namespaced Event Handling
Creating organized, modular event handling systems:
// Define a controller object to organize related event handlers
const TodoListController = {
// Element references
elements: {
list: document.getElementById('todo-list'),
form: document.getElementById('todo-form'),
input: document.getElementById('todo-input'),
clearButton: document.getElementById('clear-completed')
},
// Initialize the controller
init() {
// Bind methods to preserve context
this.handleNewTodo = this.handleNewTodo.bind(this);
this.handleTodoClick = this.handleTodoClick.bind(this);
this.handleClearCompleted = this.handleClearCompleted.bind(this);
// Set up event listeners
this.elements.form.addEventListener('submit', this.handleNewTodo);
this.elements.list.addEventListener('click', this.handleTodoClick);
this.elements.clearButton.addEventListener('click', this.handleClearCompleted);
// Load initial data
this.loadTodos();
},
// Event handlers
handleNewTodo(event) {
event.preventDefault();
const todoText = this.elements.input.value.trim();
if (todoText) {
this.addTodo(todoText);
this.elements.input.value = '';
}
},
handleTodoClick(event) {
const todoItem = event.target.closest('.todo-item');
if (!todoItem) return;
// Handle checkbox clicks
if (event.target.type === 'checkbox') {
this.toggleTodoComplete(todoItem.dataset.id, event.target.checked);
}
// Handle delete button clicks
if (event.target.classList.contains('delete-btn')) {
this.deleteTodo(todoItem.dataset.id);
}
},
handleClearCompleted() {
this.clearCompletedTodos();
},
// Business logic methods
loadTodos() {
// Load todos from localStorage
const todos = JSON.parse(localStorage.getItem('todos') || '[]');
todos.forEach(todo => this.renderTodo(todo));
},
addTodo(text) {
const todo = {
id: Date.now().toString(),
text,
completed: false
};
// Add to localStorage
const todos = JSON.parse(localStorage.getItem('todos') || '[]');
todos.push(todo);
localStorage.setItem('todos', JSON.stringify(todos));
// Add to UI
this.renderTodo(todo);
},
// Other methods for rendering, toggling, deleting todos...
// Clean up (important for single-page applications)
destroy() {
// Remove event listeners
this.elements.form.removeEventListener('submit', this.handleNewTodo);
this.elements.list.removeEventListener('click', this.handleTodoClick);
this.elements.clearButton.removeEventListener('click', this.handleClearCompleted);
}
};
Benefits of Namespaced/Controller Pattern
- Organization: Related event handlers and logic kept together
- Maintainability: Clear structure makes code easier to update
- Reusability: Controllers can be instantiated multiple times
- Clean Teardown: Centralized place to remove listeners when needed
- Context Management: Consistent `this` binding across handlers
Managing Event Listeners
Properly managing the lifecycle of event listeners is crucial for performance and preventing memory leaks.
Removing Event Listeners
To properly remove event listeners, you need to reference the same function that was added:
// This WON'T work because they're different function instances
element.addEventListener('click', function() { console.log('Clicked'); });
element.removeEventListener('click', function() { console.log('Clicked'); }); // Won't remove!
// This WILL work - store a reference to the function
function handleClick() { console.log('Clicked'); }
element.addEventListener('click', handleClick);
element.removeEventListener('click', handleClick); // Successfully removed
// For anonymous functions, store the reference
const button = document.getElementById('my-button');
const clickHandler = function() { console.log('Button clicked'); };
button.addEventListener('click', clickHandler);
// Later...
button.removeEventListener('click', clickHandler);
When to Remove Event Listeners
- When an element is removed from the DOM
- When a component is unmounted (in component-based architectures)
- When switching between views in single-page applications
- After one-time events complete
- When detaching a behavior temporarily
Memory Leaks and Event Listeners
A common cause of memory leaks in web applications is forgotten event listeners:
Problematic Code (Memory Leak):
function createModal(content) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<button class="close-btn">×</button>
${content}
</div>
`;
// Add event listener
const closeButton = modal.querySelector('.close-btn');
closeButton.addEventListener('click', function() {
// Remove from DOM, but listener is still in memory!
modal.remove();
});
document.body.appendChild(modal);
return modal;
}
Improved Code (Properly Cleaned Up):
function createModal(content) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<button class="close-btn">×</button>
${content}
</div>
`;
// Store reference to handler function
const closeHandler = function() {
// Remove the event listener first
closeButton.removeEventListener('click', closeHandler);
// Then remove from DOM
modal.remove();
};
// Add event listener
const closeButton = modal.querySelector('.close-btn');
closeButton.addEventListener('click', closeHandler);
document.body.appendChild(modal);
// Return an object with methods for controlling the modal
return {
close: closeHandler,
destroy: function() {
closeButton.removeEventListener('click', closeHandler);
modal.remove();
}
};
}
Monitoring and Debugging Event Listeners
Modern browsers provide tools to help debug and monitor event listeners:
Chrome DevTools Event Listener Debugging
To view attached event listeners in Chrome DevTools:
- Right-click an element and select "Inspect"
- In the Elements panel, click the "Event Listeners" tab
- You'll see all event listeners attached to the selected element and its ancestors
Note: You can filter by event type and toggle "Ancestors" to see only events directly on the element.
Custom Event Listener Tracker
// A simple utility to track event listeners for debugging
const EventTracker = {
// Store all listeners
listeners: [],
// Wrap addEventListener
addListener: function(element, type, handler, options) {
element.addEventListener(type, handler, options);
// Track this listener
this.listeners.push({
element,
type,
handler,
options
});
console.log(`Added ${type} listener to`, element);
},
// Wrap removeEventListener
removeListener: function(element, type, handler) {
element.removeEventListener(type, handler);
// Remove from tracking
this.listeners = this.listeners.filter(listener =>
!(listener.element === element &&
listener.type === type &&
listener.handler === handler)
);
console.log(`Removed ${type} listener from`, element);
},
// List all current listeners
listListeners: function() {
console.table(this.listeners.map(l => ({
element: l.element.tagName + (l.element.id ? `#${l.element.id}` : ''),
type: l.type,
options: JSON.stringify(l.options)
})));
return this.listeners;
},
// Find listeners for a specific element
findListenersForElement: function(element) {
const elementListeners = this.listeners.filter(l => l.element === element);
console.table(elementListeners.map(l => ({
type: l.type,
options: JSON.stringify(l.options)
})));
return elementListeners;
}
};
// Usage
const button = document.getElementById('my-button');
EventTracker.addListener(button, 'click', () => console.log('Clicked!'), { once: true });
EventTracker.listListeners();
Practical Applications
Application 1: Advanced Form Validation with Error Handling
A comprehensive form validation system using multiple event types:
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('signup-form');
const formFields = form.querySelectorAll('input, select, textarea');
// Set up validation on blur for each field
formFields.forEach(field => {
// Skip submit buttons
if (field.type === 'submit') return;
// Create error message container if it doesn't exist
if (!field.nextElementSibling || !field.nextElementSibling.classList.contains('error-message')) {
const errorMessage = document.createElement('div');
errorMessage.className = 'error-message';
field.parentNode.insertBefore(errorMessage, field.nextElementSibling);
}
// Add validation when field loses focus
field.addEventListener('blur', function() {
validateField(this);
});
// For text inputs, validate as typing (after initial blur)
if (field.type === 'text' || field.type === 'email' || field.type === 'password') {
field.addEventListener('input', function() {
// Only validate during typing if the field has already been blurred once
if (this.dataset.blurred) {
validateField(this);
}
});
}
// Mark fields as blurred after first blur
field.addEventListener('blur', function() {
this.dataset.blurred = 'true';
});
});
// Handle form submission
form.addEventListener('submit', function(event) {
// Validate all fields
let isValid = true;
formFields.forEach(field => {
if (field.type !== 'submit') {
if (!validateField(field)) {
isValid = false;
}
}
});
if (!isValid) {
event.preventDefault();
// Show form-level error
const formError = document.getElementById('form-error');
formError.textContent = 'Please correct the errors in the form before submitting.';
formError.classList.add('visible');
// Focus the first invalid field
form.querySelector('.invalid').focus();
}
});
// Field validation function
function validateField(field) {
const errorElement = field.nextElementSibling;
// Reset validation state
field.classList.remove('valid', 'invalid');
errorElement.textContent = '';
// Get the value
const value = field.value.trim();
// Check required fields
if (field.required && !value) {
setInvalid(field, errorElement, 'This field is required');
return false;
}
// Field-specific validation
if (value) {
switch (field.type) {
case 'email':
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(value)) {
setInvalid(field, errorElement, 'Please enter a valid email address');
return false;
}
break;
case 'password':
// Check password complexity
if (field.id === 'password' && value.length < 8) {
setInvalid(field, errorElement, 'Password must be at least 8 characters long');
return false;
}
// Check password confirmation
if (field.id === 'confirm-password') {
const password = document.getElementById('password').value;
if (value !== password) {
setInvalid(field, errorElement, 'Passwords do not match');
return false;
}
}
break;
case 'tel':
const phonePattern = /^\d{10,15}$/;
if (!phonePattern.test(value.replace(/\D/g, ''))) {
setInvalid(field, errorElement, 'Please enter a valid phone number');
return false;
}
break;
case 'select-one':
if (field.value === '') {
setInvalid(field, errorElement, 'Please select an option');
return false;
}
break;
}
// Field-specific validation based on name or data attributes
if (field.name === 'username' || field.id === 'username') {
if (value.length < 4) {
setInvalid(field, errorElement, 'Username must be at least 4 characters long');
return false;
}
// Check for spaces
if (/\s/.test(value)) {
setInvalid(field, errorElement, 'Username cannot contain spaces');
return false;
}
}
// Custom validation via data attributes
if (field.dataset.minLength && value.length < parseInt(field.dataset.minLength)) {
setInvalid(field, errorElement, `Must be at least ${field.dataset.minLength} characters long`);
return false;
}
if (field.dataset.pattern) {
const pattern = new RegExp(field.dataset.pattern);
if (!pattern.test(value)) {
setInvalid(field, errorElement, field.dataset.errorMessage || 'Invalid format');
return false;
}
}
}
// If we made it here, field is valid
setValid(field, errorElement);
return true;
}
function setValid(field, errorElement) {
field.classList.add('valid');
field.classList.remove('invalid');
errorElement.textContent = '';
field.setCustomValidity('');
}
function setInvalid(field, errorElement, message) {
field.classList.add('invalid');
field.classList.remove('valid');
errorElement.textContent = message;
field.setCustomValidity(message);
}
});
Application 2: Interactive Photo Gallery with Filtering
A photo gallery with event-driven filtering, zooming, and navigation:
document.addEventListener('DOMContentLoaded', function() {
const gallery = {
elements: {
container: document.getElementById('gallery-container'),
items: document.querySelectorAll('.gallery-item'),
filters: document.querySelectorAll('.filter-button'),
lightbox: document.getElementById('lightbox'),
lightboxImg: document.getElementById('lightbox-image'),
lightboxClose: document.getElementById('lightbox-close'),
lightboxNext: document.getElementById('lightbox-next'),
lightboxPrev: document.getElementById('lightbox-prev'),
lightboxCaption: document.getElementById('lightbox-caption')
},
state: {
currentFilter: 'all',
currentIndex: 0,
filteredItems: [],
isTransitioning: false
},
init: function() {
// Bind methods to preserve context
this.handleFilterClick = this.handleFilterClick.bind(this);
this.handleItemClick = this.handleItemClick.bind(this);
this.closeLightbox = this.closeLightbox.bind(this);
this.nextImage = this.nextImage.bind(this);
this.prevImage = this.prevImage.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
// Initialize filtered items with all items
this.state.filteredItems = Array.from(this.elements.items);
// Set up event listeners
this.elements.filters.forEach(filter => {
filter.addEventListener('click', this.handleFilterClick);
});
this.elements.container.addEventListener('click', this.handleItemClick);
this.elements.lightboxClose.addEventListener('click', this.closeLightbox);
this.elements.lightboxNext.addEventListener('click', this.nextImage);
this.elements.lightboxPrev.addEventListener('click', this.prevImage);
// Prevent lightbox image click from closing the lightbox
this.elements.lightboxImg.addEventListener('click', function(event) {
event.stopPropagation();
});
// Close lightbox when clicking the background
this.elements.lightbox.addEventListener('click', this.closeLightbox);
// Keyboard navigation
document.addEventListener('keydown', this.handleKeydown);
},
// Event Handlers
handleFilterClick: function(event) {
const filterValue = event.currentTarget.dataset.filter;
// Update the active filter button
this.elements.filters.forEach(btn => {
btn.classList.toggle('active', btn === event.currentTarget);
});
// Apply the filter
this.applyFilter(filterValue);
},
handleItemClick: function(event) {
const item = event.target.closest('.gallery-item');
if (!item) return;
// Get all visible items (filtered)
this.state.filteredItems = Array.from(this.elements.items).filter(item =>
!item.classList.contains('hidden')
);
// Find the index of the clicked item
this.state.currentIndex = this.state.filteredItems.indexOf(item);
// Open lightbox with this image
this.openLightbox(item);
},
handleKeydown: function(event) {
if (!this.elements.lightbox.classList.contains('active')) return;
switch (event.key) {
case 'Escape':
this.closeLightbox();
break;
case 'ArrowRight':
this.nextImage();
break;
case 'ArrowLeft':
this.prevImage();
break;
}
},
// Gallery Methods
applyFilter: function(filterValue) {
this.state.currentFilter = filterValue;
this.elements.items.forEach(item => {
if (filterValue === 'all' || item.dataset.category === filterValue) {
// Show the item with animation
item.classList.remove('hidden');
// Trigger reflow for animation
void item.offsetWidth;
item.classList.add('fade-in');
setTimeout(() => {
item.classList.remove('fade-in');
}, 500);
} else {
// Hide the item
item.classList.add('hidden');
}
});
// Update filtered items list
this.state.filteredItems = Array.from(this.elements.items).filter(item =>
!item.classList.contains('hidden')
);
// Update gallery layout
this.updateLayout();
},
updateLayout: function() {
// Update container height or layout if needed
// This could involve something like masonry layout initialization
console.log('Layout updated with', this.state.filteredItems.length, 'items');
},
// Lightbox Methods
openLightbox: function(item) {
const imgSrc = item.querySelector('img').dataset.fullsize;
const caption = item.querySelector('.caption').textContent;
// Set the image and caption
this.elements.lightboxImg.src = imgSrc;
this.elements.lightboxCaption.textContent = caption;
// Show the lightbox
this.elements.lightbox.classList.add('active');
// Disable scrolling on the body
document.body.style.overflow = 'hidden';
},
closeLightbox: function() {
this.elements.lightbox.classList.remove('active');
// Re-enable scrolling
document.body.style.overflow = '';
// Clear image source after transition (to save memory)
setTimeout(() => {
this.elements.lightboxImg.src = '';
}, 300);
},
nextImage: function() {
if (this.state.isTransitioning) return;
this.state.isTransitioning = true;
// Move to next image
this.state.currentIndex = (this.state.currentIndex + 1) % this.state.filteredItems.length;
// Fade out current image
this.elements.lightboxImg.classList.add('fade-out');
setTimeout(() => {
// Update to next image
const nextItem = this.state.filteredItems[this.state.currentIndex];
this.elements.lightboxImg.src = nextItem.querySelector('img').dataset.fullsize;
this.elements.lightboxCaption.textContent = nextItem.querySelector('.caption').textContent;
// Remove fade out class
this.elements.lightboxImg.classList.remove('fade-out');
this.state.isTransitioning = false;
}, 300);
},
prevImage: function() {
if (this.state.isTransitioning) return;
this.state.isTransitioning = true;
// Move to previous image
this.state.currentIndex = (this.state.currentIndex - 1 + this.state.filteredItems.length) % this.state.filteredItems.length;
// Fade out current image
this.elements.lightboxImg.classList.add('fade-out');
setTimeout(() => {
// Update to previous image
const prevItem = this.state.filteredItems[this.state.currentIndex];
this.elements.lightboxImg.src = prevItem.querySelector('img').dataset.fullsize;
this.elements.lightboxCaption.textContent = prevItem.querySelector('.caption').textContent;
// Remove fade out class
this.elements.lightboxImg.classList.remove('fade-out');
this.state.isTransitioning = false;
}, 300);
},
// Clean up method
destroy: function() {
// Remove event listeners
this.elements.filters.forEach(filter => {
filter.removeEventListener('click', this.handleFilterClick);
});
this.elements.container.removeEventListener('click', this.handleItemClick);
this.elements.lightboxClose.removeEventListener('click', this.closeLightbox);
this.elements.lightboxNext.removeEventListener('click', this.nextImage);
this.elements.lightboxPrev.removeEventListener('click', this.prevImage);
this.elements.lightbox.removeEventListener('click', this.closeLightbox);
document.removeEventListener('keydown', this.handleKeydown);
}
};
// Initialize the gallery
gallery.init();
// For single-page applications, provide a way to clean up
// when navigating away from this page
window.addEventListener('beforeunload', function() {
gallery.destroy();
});
});
Practical Exercise
Create a drag-and-drop task management board with the following features:
- Create three columns: "To Do", "In Progress", and "Completed"
- Allow users to create new task cards with a title, description, and priority
- Implement drag and drop functionality to move tasks between columns
- Add the ability to edit task details by double-clicking
- Implement a right-click context menu for additional actions (delete, duplicate, prioritize)
- Add keyboard accessibility (arrow keys to navigate, enter to select, etc.)
- Store tasks in localStorage to persist between page reloads
- Add filtering and sorting options for tasks
- Use event delegation for efficient event handling
- Properly manage all event listeners to prevent memory leaks
This exercise will give you practice with a wide range of event handlers and advanced patterns in a realistic project context.
Summary
- Event handlers are functions that execute in response to events, while event listeners connect events to handlers
- There are three main ways to register event handlers:
addEventListener(), event handler properties, and inline HTML attributes addEventListener()is the preferred method due to its flexibility and support for multiple handlers- The context (
this) in event handlers depends on how the handler is defined and registered - You can prevent default browser behaviors with
preventDefault()and stop event propagation withstopPropagation() - Event delegation improves performance by attaching handlers to parent elements instead of individual children
- Debouncing and throttling control the frequency of handler execution for performance-intensive events
- Proper management of event listeners is crucial for preventing memory leaks, especially in dynamic applications
- Organizing event handling code using patterns like namespacing, modules, or controllers improves maintainability
In the next lecture, we'll explore event propagation in depth, including capturing and bubbling phases, and how to leverage event delegation for efficient DOM interaction.