Understanding Events: The Heartbeat of Interactive Web Applications
Welcome to our exploration of JavaScript event handling! Now that we've learned how to select and manipulate DOM elements, we're ready to make our web pages truly interactive by responding to user actions.
This lecture belongs to Week 4: Web Fundamentals and is part of our Python Full Stack Developer course. You'll find this content in the folder structure: /04week/04week_4day_d.html
Events are at the core of interactive web applications. They allow our code to listen for and respond to actions like clicks, keypresses, form submissions, and even system events like page loading. Without events, our web pages would be static documents with no way to interact with users.
What Are Events?
In JavaScript, events are signals that something has happened in the browser. These signals can come from various sources:
- User Actions: Clicks, keypresses, mouse movements, form submissions
- Browser Actions: Page loading, window resizing, focus changes
- Network Events: Requests completing, resources loading
- Time-based Events: Timeouts, intervals
- API Events: Geolocation updates, device orientation changes
Postal Service Metaphor: Think of events as a postal service for your web page. Different parts of the page can "mail" notifications when something happens, and your JavaScript code can "subscribe" to receive these notifications at specific addresses (event listeners). When an event occurs, all subscribers get notified and can respond accordingly.
Anatomy of an Event
Events are represented by event objects that contain information about what happened:
- Type: What kind of event occurred (click, keypress, etc.)
- Target: The element that triggered the event
- Timestamp: When the event occurred
- Additional Properties: Event-specific details (cursor position, key pressed, etc.)
- Methods: Functions to control the event's behavior
Common Types of Events
JavaScript can handle a wide variety of events. Here are some of the most commonly used categories:
Mouse Events
click: When an element is clickeddblclick: When an element is double-clickedmousedown: When a mouse button is pressed downmouseup: When a mouse button is releasedmousemove: When the mouse cursor movesmouseover: When the cursor enters an elementmouseout: When the cursor leaves an elementmouseenter: Like mouseover, but doesn't bubblemouseleave: Like mouseout, but doesn't bubble
// Example of handling a click event
document.getElementById('my-button').addEventListener('click', function(event) {
console.log('Button was clicked!');
console.log('Mouse position:', event.clientX, event.clientY);
});
Keyboard Events
keydown: When a key is pressed downkeyup: When a key is releasedkeypress: When a key is pressed (character keys only)
// Example of handling a keydown event
document.addEventListener('keydown', function(event) {
console.log('Key pressed:', event.key);
console.log('Key code:', event.keyCode); // Deprecated but still used
console.log('Modifier keys:', event.ctrlKey, event.shiftKey, event.altKey);
});
Form Events
submit: When a form is submittedchange: When an input's value changes (after losing focus)input: When an input's value changes (immediately)focus: When an element receives focusblur: When an element loses focusreset: When a form is reset
// Example of handling a form submission
document.getElementById('login-form').addEventListener('submit', function(event) {
event.preventDefault(); // Stop the form from actually submitting
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
// Form validation logic
if (username === '' || password === '') {
alert('Please fill in all fields');
} else {
// AJAX form submission or other logic here
console.log('Form submitted with:', username, password);
}
});
Document/Window Events
load: When the page finishes loadingDOMContentLoaded: When the DOM is ready (before resources)resize: When the window is resizedscroll: When the document or element is scrolledbeforeunload: Before the page is unloaded (closing or navigating)
// Example of DOM ready event
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM is fully loaded and parsed');
// Safe to manipulate DOM elements here
});
// Example of window resize event
window.addEventListener('resize', function() {
console.log('Window dimensions:', window.innerWidth, window.innerHeight);
});
Touch Events
touchstart: When a touch point is placed on the screentouchend: When a touch point is removed from the screentouchmove: When a touch point is moved along the screentouchcancel: When a touch point has been disrupted
// Example of handling a touch event
document.getElementById('touch-area').addEventListener('touchstart', function(event) {
console.log('Touch started!');
console.log('Number of touch points:', event.touches.length);
// Prevent scrolling/zooming
event.preventDefault();
});
Event Listeners: Responding to Events
To respond to events, we need to "listen" for them using event listeners. JavaScript provides several ways to attach event listeners to elements:
Method 1: addEventListener (Recommended)
The modern and most flexible way to handle events:
// Basic syntax
element.addEventListener(eventType, handlerFunction, options);
// Example
const button = document.getElementById('submit-button');
button.addEventListener('click', function(event) {
console.log('Button clicked!');
});
// Using a named function
function handleClick(event) {
console.log('Button clicked!');
console.log('Event object:', event);
}
button.addEventListener('click', handleClick);
// With options (third parameter)
button.addEventListener('click', handleClick, {
once: true, // Only trigger once
capture: false, // Use bubbling phase (default)
passive: true // Promise not to call preventDefault()
});
Benefits of addEventListener:
- Allows multiple listeners for the same event
- Can remove specific listeners later
- Provides additional options
- Doesn't override existing listeners
- Clear separation between HTML and JavaScript
Method 2: Event Handler Properties
A simpler but more limited approach:
// Syntax
element.oneventname = handlerFunction;
// Example
const button = document.getElementById('submit-button');
button.onclick = function(event) {
console.log('Button clicked!');
};
// Using a named function
function handleClick(event) {
console.log('Button clicked!');
}
button.onclick = handleClick;
// This will OVERWRITE the previous handler!
button.onclick = function() {
console.log('New handler - the previous one is gone');
};
Limitations:
- Only one handler per event type per element
- Can't use capture phase
- Less control over how events are handled
Method 3: Inline Event Handlers (Not Recommended)
The oldest method, written directly in HTML:
<!-- HTML with inline event handler -->
<button onclick="handleClick(event)">Click Me</button>
<!-- Or with direct code -->
<button onclick="alert('Button clicked!')">Click Me</button>
// JavaScript
function handleClick(event) {
console.log('Button clicked!');
}
Why to avoid this approach:
- Mixes HTML and JavaScript (poor separation of concerns)
- Performance issues with complex handlers
- Limited access to the event object
- Difficult to manage and maintain
- Can lead to security risks like XSS
Removing Event Listeners
For events that should only happen under certain conditions, you may need to remove event listeners:
// Adding a listener
function handleClick(event) {
console.log('Button clicked!');
}
button.addEventListener('click', handleClick);
// Removing the listener - MUST use the same function reference
button.removeEventListener('click', handleClick);
// This WON'T work (anonymous function)
button.addEventListener('click', function() {
console.log('Click handled');
});
// This attempt to remove will fail because it's a different function reference
button.removeEventListener('click', function() {
console.log('Click handled');
});
Tip: Always store function references in variables if you plan to remove them later.
// Correct approach for removing listeners
const handleHover = function() {
console.log('Element hovered');
};
// Add the listener
element.addEventListener('mouseover', handleHover);
// Later, remove the same listener
element.removeEventListener('mouseover', handleHover);
The Event Object: Understanding What Happened
When an event occurs, JavaScript passes an event object to your handler function. This object contains details about the event that can be extremely useful:
Common Event Object Properties
event.type: The type of event (e.g., "click", "keydown")event.target: The element that triggered the eventevent.currentTarget: The element that the listener is attached toevent.timeStamp: When the event occurredevent.bubbles: Whether the event bubbles up through the DOMevent.cancelable: Whether the event can be canceled
document.getElementById('my-button').addEventListener('click', function(event) {
console.log('Event type:', event.type);
console.log('Target element:', event.target);
console.log('Current target:', event.currentTarget);
console.log('Time of event:', event.timeStamp);
});
Event-Specific Properties
Different event types provide additional information relevant to that event:
Mouse Events:
event.clientX/clientY: Coordinates relative to the viewportevent.pageX/pageY: Coordinates relative to the documentevent.screenX/screenY: Coordinates relative to the screenevent.button: Which mouse button was pressedevent.altKey/ctrlKey/shiftKey/metaKey: Modifier keys
Keyboard Events:
event.key: The key value (e.g., "a", "Enter")event.code: The physical key (e.g., "KeyA", "Enter")event.keyCode: Numeric key code (deprecated)event.altKey/ctrlKey/shiftKey/metaKey: Modifier keysevent.repeat: Whether the key is being held down
Form Events:
event.value: Current value of the form elementevent.checked: For checkboxes and radio buttonsevent.selected: For select options
// Mouse event example
document.addEventListener('mousemove', function(event) {
// Update a div showing the current mouse position
document.getElementById('mouse-position').textContent =
`Mouse at: ${event.clientX}, ${event.clientY}`;
});
// Keyboard event example
document.addEventListener('keydown', function(event) {
// Check for specific keys
if (event.key === 'Escape') {
closeModal();
}
// Check for keyboard shortcuts
if (event.ctrlKey && event.key === 's') {
event.preventDefault(); // Prevent browser's save dialog
saveDocument();
}
});
Important Event Methods
The event object also provides methods to control event behavior:
event.preventDefault(): Stops the default browser behaviorevent.stopPropagation(): Stops the event from bubbling/capturingevent.stopImmediatePropagation(): Stops other listeners on the same element
// Prevent default behavior of a link
document.querySelector('a.stay-here').addEventListener('click', function(event) {
event.preventDefault(); // Prevent navigating to the link
console.log('Link was clicked, but page did not navigate');
});
// Prevent form submission
document.querySelector('form').addEventListener('submit', function(event) {
if (!validateForm()) {
event.preventDefault(); // Don't submit if validation fails
}
});
Event Propagation: Bubbling and Capturing
When an event occurs on an element, it doesn't just fire there. Events in JavaScript propagate through the DOM in two phases:
- Capturing Phase: The event travels down from the document root to the target element
- Bubbling Phase: The event bubbles up from the target back to the document root
Ripple Metaphor: Think of event propagation like dropping a stone into a pond. The ripples first travel from the edge of the pond inward (capturing phase), then from the point of impact outward (bubbling phase). Each element the ripple passes can detect and respond to it.
Visual Representation of Event Propagation
CAPTURING PHASE (1) BUBBLING PHASE (2)
↓ ↑
+--------------+ +--------------+
| document | | document |
+--------------+ +--------------+
↓ ↑
+--------------+ +--------------+
| html | | html |
+--------------+ +--------------+
↓ ↑
+--------------+ +--------------+
| body | | body |
+--------------+ +--------------+
↓ ↑
+--------------+ +--------------+
| div | | div |
+--------------+ +--------------+
↓ ↑
+--------------+ +--------------+
| button | | button |
+--------------+ +--------------+
↓ ↑
+======EVENT======+ +======EVENT======+
Example of Event Bubbling
// HTML:
// <div id="outer">
// <div id="middle">
// <button id="inner">Click Me</button>
// </div>
// </div>
// Add click listeners to all elements
document.getElementById('outer').addEventListener('click', function() {
console.log('Outer div clicked');
});
document.getElementById('middle').addEventListener('click', function() {
console.log('Middle div clicked');
});
document.getElementById('inner').addEventListener('click', function() {
console.log('Button clicked');
});
// When the button is clicked, the console will show:
// "Button clicked"
// "Middle div clicked"
// "Outer div clicked"
// This is because the event bubbles up from the target element.
Using the Capturing Phase
By default, event listeners are triggered during the bubbling phase. To use the capturing phase instead, set the third parameter of addEventListener to true or {capture: true}:
// Same HTML structure as above
// Register a listener for the capturing phase
document.getElementById('outer').addEventListener('click', function() {
console.log('Outer div clicked - CAPTURE');
}, true); // true enables capturing phase
document.getElementById('middle').addEventListener('click', function() {
console.log('Middle div clicked - CAPTURE');
}, true);
document.getElementById('inner').addEventListener('click', function() {
console.log('Button clicked - CAPTURE');
}, true);
// When the button is clicked, the console will show:
// "Outer div clicked - CAPTURE"
// "Middle div clicked - CAPTURE"
// "Button clicked - CAPTURE"
// This is the reverse order from bubbling
Stopping Event Propagation
Sometimes you want to prevent an event from continuing its journey through the DOM:
// Prevent event bubbling
document.getElementById('middle').addEventListener('click', function(event) {
console.log('Middle div clicked');
event.stopPropagation(); // Event stops here, won't reach outer div
});
// Stop all handlers on this element
document.getElementById('inner').addEventListener('click', function(event) {
console.log('First button handler');
event.stopImmediatePropagation(); // Prevents other handlers on this element
});
// This handler won't run if the button is clicked
document.getElementById('inner').addEventListener('click', function() {
console.log('Second button handler - this will not run');
});
Important Note: Be careful with stopPropagation(). It can break functionality that relies on event bubbling, like analytics tracking or delegate event handlers. Use it only when necessary.
Event Delegation: Efficient Event Handling
Event delegation is a technique that leverages event bubbling to handle events efficiently. Instead of attaching event listeners to many similar elements, you attach a single listener to a common ancestor.
Management Metaphor: Think of event delegation like a corporate management structure. Instead of the CEO (your JavaScript) trying to directly manage every individual employee (element), they delegate responsibility to department managers (parent elements). When employees have concerns (events), they report to their manager, who can handle common issues and only escalate unique situations to the CEO.
Benefits of Event Delegation
- Memory Efficiency: Fewer event listeners = less memory use
- Dynamic Elements: Works with elements added to the DOM after page load
- Less Code: Simpler, more maintainable code
- Improved Performance: Especially with many similar elements
Traditional Approach vs. Event Delegation
// HTML:
// <ul id="task-list">
// <li><span>Task 1</span> <button class="delete">Delete</button></li>
// <li><span>Task 2</span> <button class="delete">Delete</button></li>
// <li><span>Task 3</span> <button class="delete">Delete</button></li>
// </ul>
// TRADITIONAL APPROACH: Add a listener to each button
const deleteButtons = document.querySelectorAll('#task-list .delete');
deleteButtons.forEach(button => {
button.addEventListener('click', function() {
this.parentElement.remove();
});
});
// Problems with this approach:
// 1. New items added dynamically won't have listeners
// 2. Many event listeners in memory
// 3. Need to update code if structure changes
// EVENT DELEGATION APPROACH: One listener on the parent
document.getElementById('task-list').addEventListener('click', function(event) {
// Check if the clicked element is a delete button
if (event.target.classList.contains('delete')) {
// Find the li parent and remove it
event.target.closest('li').remove();
}
});
Implementing Event Delegation
To implement event delegation effectively:
- Identify a common parent element for all target elements
- Add a single event listener to that parent
- Use
event.targetto determine which specific element was interacted with - Check if the target (or one of its ancestors) matches your selector
- Perform the appropriate action
// HTML:
// <div id="products">
// <div class="product" data-id="1">
// <h3>Product 1</h3>
// <button class="view">View</button>
// <button class="add-to-cart">Add to Cart</button>
// <button class="favorite">Favorite</button>
// </div>
// <div class="product" data-id="2">...</div>
// <div class="product" data-id="3">...</div>
// </div>
document.getElementById('products').addEventListener('click', function(event) {
// Find the product container
const product = event.target.closest('.product');
// If click wasn't within a product, exit early
if (!product) return;
// Get the product ID
const productId = product.dataset.id;
// Determine which button was clicked
if (event.target.classList.contains('view')) {
viewProductDetails(productId);
}
else if (event.target.classList.contains('add-to-cart')) {
addProductToCart(productId);
}
else if (event.target.classList.contains('favorite')) {
toggleFavoriteStatus(productId);
}
});
function viewProductDetails(id) {
console.log(`Viewing details for product ${id}`);
// Navigate to product page or show modal
}
function addProductToCart(id) {
console.log(`Adding product ${id} to cart`);
// Add to cart logic
}
function toggleFavoriteStatus(id) {
console.log(`Toggling favorite status for product ${id}`);
// Update favorite status
}
The closest() method is particularly useful in event delegation. It traverses up from the target element to find the first ancestor that matches the selector, or returns null if none is found.
Custom Events: Creating Your Own Events
JavaScript allows you to create and dispatch your own custom events, which can be useful for building component-based applications:
Creating and Dispatching Custom Events
// Basic custom event
const simpleEvent = new Event('myCustomEvent');
// Dispatch the event
document.getElementById('myElement').dispatchEvent(simpleEvent);
// Custom event with additional data
const detailedEvent = new CustomEvent('productAdded', {
bubbles: true, // Allow event to bubble up the DOM
cancelable: true, // Allow event to be canceled
detail: { // Custom data to pass with the event
productId: 123,
productName: 'Wireless Headphones',
price: 79.99
}
});
// Dispatch the detailed event
document.dispatchEvent(detailedEvent);
// Listen for the custom event
document.addEventListener('productAdded', function(event) {
console.log('Product added!', event.detail);
updateCartCount(event.detail.price);
});
Practical Example: Component Communication
Custom events can be used to create a publish-subscribe pattern between components:
// Shopping cart component
const ShoppingCart = {
items: [],
addItem: function(product) {
this.items.push(product);
// Announce that the cart has changed
const event = new CustomEvent('cartUpdated', {
bubbles: true,
detail: {
itemCount: this.items.length,
totalPrice: this.calculateTotal(),
lastItemAdded: product
}
});
document.dispatchEvent(event);
},
calculateTotal: function() {
return this.items.reduce((total, item) => total + item.price, 0);
}
};
// Cart summary display component
document.addEventListener('DOMContentLoaded', function() {
// Listen for cart updates
document.addEventListener('cartUpdated', function(event) {
// Update the cart display
document.getElementById('cart-count').textContent = event.detail.itemCount;
document.getElementById('cart-total').textContent = `$${event.detail.totalPrice.toFixed(2)}`;
// Show notification about the added item
showNotification(`Added: ${event.detail.lastItemAdded.name}`);
});
});
// Product listing component
document.getElementById('product-list').addEventListener('click', function(event) {
if (event.target.classList.contains('add-to-cart')) {
const productElement = event.target.closest('.product');
const product = {
id: productElement.dataset.id,
name: productElement.dataset.name,
price: parseFloat(productElement.dataset.price)
};
ShoppingCart.addItem(product);
}
});
Component Communication Metaphor: Custom events are like a bulletin board system where different parts of your application can post notices (dispatch events) and interested parties can subscribe to those notices (add event listeners). Components don't need to know about each other directly; they just need to agree on the notice format.
Optimizing Events: Debouncing and Throttling
Some events like scroll, resize, and mousemove can fire many times per second. Processing each event can be expensive and lead to performance issues. Debouncing and throttling are techniques to limit how often your event handlers run.
Debouncing
Debouncing ensures that a function is only executed after a certain amount of time has passed since it was last invoked. Think of it like waiting until someone stops typing before processing their search query.
// Basic debounce function
function debounce(func, delay) {
let timeoutId;
return function(...args) {
// Clear previous timeout
clearTimeout(timeoutId);
// Set new timeout
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Example: Search input that updates results only after typing stops
const searchInput = document.getElementById('search-input');
const debouncedSearch = debounce(function(e) {
console.log('Searching for:', e.target.value);
fetchSearchResults(e.target.value);
}, 300); // Wait 300ms after typing stops
searchInput.addEventListener('input', debouncedSearch);
Elevator Metaphor: Debouncing is like an elevator that waits until no new people have entered for a few seconds before starting its journey. If someone new enters during the waiting period, the timer resets.
Throttling
Throttling limits how often a function can be called over time. It ensures the function executes at a regular interval, regardless of how many times the event is triggered.
// Basic throttle function
function throttle(func, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Example: Limiting how often we respond to scroll events
const handleScroll = throttle(function() {
console.log('Scroll position:', window.scrollY);
checkElementsInViewport();
}, 100); // Execute at most every 100ms
window.addEventListener('scroll', handleScroll);
Highway Toll Metaphor: Throttling is like a highway toll booth that only allows one car through every few seconds, no matter how many cars are waiting. Unlike debouncing, which waits for a quiet period, throttling ensures regular processing at a sustainable rate.
When to Use Each Technique
- Use Debouncing When:
- The final state is what matters (search inputs, form validation)
- You want to wait for a "quiet period" before acting
- Processing during intermediate states would be wasteful
- Use Throttling When:
- Regular updates are needed (infinite scroll, progress updates)
- You need to ensure some feedback during continuous events
- You want to limit the rate of potentially expensive operations
Event Handling Best Practices
Performance Considerations
- Use Event Delegation for groups of similar elements
- Remove Unnecessary Listeners when they're no longer needed
- Debounce/Throttle handlers for frequently firing events
- Keep Handlers Small and focused on their specific task
- Avoid Anonymous Functions if you need to remove listeners later
- Be Careful with this Context when using arrow functions
Accessibility Considerations
- Use Semantic Elements with built-in event handling (buttons, links)
- Add Keyboard Support alongside mouse/touch events
- Don't Rely Solely on Hover events (mobile has no hover)
- Respect User Preferences like reduced motion
- Ensure Focus Management for custom UI components
// Bad: Div acting as a button with only click handling
<div onclick="submitForm()" class="button-like">Submit</div>
// Good: Using a button element with proper keyboard accessibility
<button type="button" onclick="submitForm()">Submit</button>
// Even better: Separate JavaScript from HTML
<button type="button" id="submit-button">Submit</button>
// JavaScript
document.getElementById('submit-button').addEventListener('click', submitForm);
Code Organization and Maintainability
- Separate Logic from Handlers - event handlers should call functions, not contain complex logic
- Namespace Your Events especially when creating custom events
- Document Your Events including what data they contain
- Use Consistent Patterns across your application
// Poor organization - logic inside event handler
button.addEventListener('click', function(event) {
// Complex logic here...
const userData = fetchUserData();
if (userData.isLoggedIn) {
if (userData.hasPurchased) {
showPremiumContent();
} else {
if (userData.freeTrialsRemaining > 0) {
offerFreeTrial();
} else {
showPurchaseDialog();
}
}
} else {
redirectToLogin();
}
});
// Better organization - separate logic from handler
button.addEventListener('click', handleAccessRequest);
function handleAccessRequest(event) {
const userData = fetchUserData();
if (!userData.isLoggedIn) {
return redirectToLogin();
}
if (userData.hasPurchased) {
return showPremiumContent();
}
userData.freeTrialsRemaining > 0 ? offerFreeTrial() : showPurchaseDialog();
}
Browser Compatibility Considerations
While modern browsers have largely standardized event handling, there are still some things to be aware of:
- Old IE Support: IE < 9 used
attachEventinstead ofaddEventListener - Event Object Differences: Properties like
event.targetvsevent.srcElement - Default Prevention:
preventDefault()vsreturnValue = false - Touch Events: Not all browsers support the same touch event properties
- Passive Listeners: A newer feature not supported in older browsers
For modern web development, you generally don't need to worry about most of these issues, but if you need to support older browsers, consider using a polyfill or feature detection library.
Using Feature Detection
// Check if addEventListener is supported
const addEvent = function(element, event, handler) {
if (element.addEventListener) {
element.addEventListener(event, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + event, handler);
} else {
element['on' + event] = handler;
}
};
// Example usage
addEvent(document.getElementById('my-button'), 'click', function() {
console.log('Button clicked!');
});
For most modern applications, you can just use addEventListener directly, as it's supported in all current browsers.
Complete Practical Examples
Example 1: Tabbed Interface
// HTML:
// <div class="tabs">
// <div class="tab-buttons">
// <button class="tab-button active" data-tab="tab1">Tab 1</button>
// <button class="tab-button" data-tab="tab2">Tab 2</button>
// <button class="tab-button" data-tab="tab3">Tab 3</button>
// </div>
// <div class="tab-content">
// <div id="tab1" class="tab-panel active">Content for Tab 1</div>
// <div id="tab2" class="tab-panel">Content for Tab 2</div>
// <div id="tab3" class="tab-panel">Content for Tab 3</div>
// </div>
// </div>
// CSS (simplified):
// .tab-panel { display: none; }
// .tab-panel.active { display: block; }
// .tab-button { background: #f0f0f0; }
// .tab-button.active { background: #fff; }
// JavaScript with event delegation
document.querySelector('.tab-buttons').addEventListener('click', function(event) {
// Check if a tab button was clicked
if (event.target.classList.contains('tab-button')) {
// Get the tab ID from data attribute
const tabId = event.target.dataset.tab;
// Remove active class from all buttons and panels
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active');
});
document.querySelectorAll('.tab-panel').forEach(panel => {
panel.classList.remove('active');
});
// Add active class to clicked button and corresponding panel
event.target.classList.add('active');
document.getElementById(tabId).classList.add('active');
}
});
Example 2: Image Gallery with Keyboard Navigation
// HTML:
// <div class="gallery">
// <div class="main-image">
// <img id="current-image" src="image1.jpg" alt="Gallery Image 1">
// </div>
// <div class="thumbnails">
// <img class="thumb active" data-src="image1.jpg" src="thumb1.jpg" alt="Thumbnail 1">
// <img class="thumb" data-src="image2.jpg" src="thumb2.jpg" alt="Thumbnail 2">
// <img class="thumb" data-src="image3.jpg" src="thumb3.jpg" alt="Thumbnail 3">
// <img class="thumb" data-src="image4.jpg" src="thumb4.jpg" alt="Thumbnail 4">
// </div>
// <div class="controls">
// <button id="prev">Previous</button>
// <button id="next">Next</button>
// </div>
// </div>
// JavaScript
document.addEventListener('DOMContentLoaded', function() {
const currentImage = document.getElementById('current-image');
const thumbnails = document.querySelectorAll('.thumb');
const prevButton = document.getElementById('prev');
const nextButton = document.getElementById('next');
let currentIndex = 0;
// Function to change the active image
function setActiveImage(index) {
// Wrap around if index is out of bounds
if (index < 0) index = thumbnails.length - 1;
if (index >= thumbnails.length) index = 0;
// Update currentIndex
currentIndex = index;
// Update main image
const newSrc = thumbnails[index].dataset.src;
currentImage.src = newSrc;
currentImage.alt = thumbnails[index].alt;
// Update active thumbnail
thumbnails.forEach(thumb => thumb.classList.remove('active'));
thumbnails[index].classList.add('active');
}
// Thumbnail click event using event delegation
document.querySelector('.thumbnails').addEventListener('click', function(event) {
if (event.target.classList.contains('thumb')) {
const clickedIndex = Array.from(thumbnails).indexOf(event.target);
setActiveImage(clickedIndex);
}
});
// Previous button click
prevButton.addEventListener('click', function() {
setActiveImage(currentIndex - 1);
});
// Next button click
nextButton.addEventListener('click', function() {
setActiveImage(currentIndex + 1);
});
// Keyboard navigation
document.addEventListener('keydown', function(event) {
switch (event.key) {
case 'ArrowLeft':
setActiveImage(currentIndex - 1);
break;
case 'ArrowRight':
setActiveImage(currentIndex + 1);
break;
}
});
});
Example 3: Drag and Drop Interface
// HTML:
// <div class="kanban-board">
// <div id="todo" class="column">
// <h3>To Do</h3>
// <div class="task" draggable="true" data-id="task1">Task 1</div>
// <div class="task" draggable="true" data-id="task2">Task 2</div>
// </div>
// <div id="doing" class="column">
// <h3>Doing</h3>
// <div class="task" draggable="true" data-id="task3">Task 3</div>
// </div>
// <div id="done" class="column">
// <h3>Done</h3>
// </div>
// </div>
document.addEventListener('DOMContentLoaded', function() {
// Get all draggable tasks
const tasks = document.querySelectorAll('.task');
const columns = document.querySelectorAll('.column');
let draggedTask = null;
// Add event listeners to tasks
tasks.forEach(task => {
// When drag starts
task.addEventListener('dragstart', function(event) {
draggedTask = task;
event.dataTransfer.setData('text/plain', task.dataset.id);
// Add a class for styling
setTimeout(() => {
task.classList.add('dragging');
}, 0);
});
// When drag ends
task.addEventListener('dragend', function() {
draggedTask = null;
task.classList.remove('dragging');
});
});
// Add event listeners to columns
columns.forEach(column => {
// When dragged task enters a column
column.addEventListener('dragenter', function(event) {
event.preventDefault(); // Needed for drop to work
column.classList.add('drag-over');
});
// When dragged task is over a column
column.addEventListener('dragover', function(event) {
event.preventDefault(); // Needed for drop to work
});
// When dragged task leaves a column
column.addEventListener('dragleave', function() {
column.classList.remove('drag-over');
});
// When task is dropped in a column
column.addEventListener('drop', function(event) {
event.preventDefault();
column.classList.remove('drag-over');
// Get the dragged task ID
const taskId = event.dataTransfer.getData('text/plain');
const task = document.querySelector(`.task[data-id="${taskId}"]`);
// Append the task to the column
column.appendChild(task);
// Create a custom event to notify about task movement
const taskMovedEvent = new CustomEvent('taskMoved', {
bubbles: true,
detail: {
taskId: taskId,
fromColumn: task.parentElement.id,
toColumn: column.id
}
});
task.dispatchEvent(taskMovedEvent);
});
});
// Listen for task movement
document.addEventListener('taskMoved', function(event) {
console.log(`Task ${event.detail.taskId} moved from ${event.detail.fromColumn} to ${event.detail.toColumn}`);
// Here you would typically update your data model or send to server
updateTaskStatus(event.detail.taskId, event.detail.toColumn);
});
function updateTaskStatus(taskId, newStatus) {
// Example: send to server
console.log(`Updating task ${taskId} status to ${newStatus}`);
// In a real app, you might use fetch:
// fetch('/api/tasks/' + taskId, {
// method: 'PATCH',
// body: JSON.stringify({ status: newStatus }),
// headers: { 'Content-Type': 'application/json' }
// });
}
});
Practice Exercises
Exercise 1: Event Delegation Todo List
Create a simple todo list application that allows users to:
- Add new tasks through a form
- Mark tasks as complete by clicking on them
- Delete tasks with a delete button
- Filter tasks (all, active, completed)
Use event delegation for all interactions and include proper keyboard accessibility.
Exercise 2: Interactive Form Validation
Create a registration form with real-time validation:
- Validate each field as users type (email format, password strength, etc.)
- Show validation feedback (success/error icons, messages)
- Disable the submit button until all fields are valid
- Use custom events to announce form state changes
Implement both debounced validation (for typing) and immediate validation (on blur).
Exercise 3: Custom Event Framework
Build a simple pub/sub (publish-subscribe) system using custom events:
- Create functions to publish events with data
- Allow multiple subscribers to listen for events
- Support namespaced events (e.g., "user.login", "user.logout")
- Build a small demo application showing components communicating
Use this framework to demonstrate decoupled communication between parts of an application.
Lecture Summary
Today we've explored the world of JavaScript event handling, the key to creating dynamic and interactive web applications. We've covered:
- The fundamentals of events and the event object
- Various types of events (mouse, keyboard, form, document/window)
- Different methods for adding event listeners
- Event propagation through capture and bubbling phases
- Event delegation for efficient handling of multiple elements
- Creating and working with custom events
- Performance optimization with debouncing and throttling
- Best practices for maintainable and accessible event handling
- Real-world examples of interactive interfaces
With these skills, you can now create rich, interactive web experiences that respond naturally to user actions and system events. In our next session, we'll explore more advanced ways to enhance your web applications with dynamic features.