DOM Event Model and Types

Understanding How JavaScript Responds to User Interactions

Introduction to DOM Events

So far, we've explored how to create, modify, and style DOM elements. Now we'll learn about DOM events—the mechanism that allows your web applications to respond to user interactions and other occurrences in the browser environment.

Events are the cornerstone of interactive web applications. They establish the connection between user actions and your code, enabling you to create responsive, dynamic interfaces.

Real-World Analogy: The Doorbell System

Think of DOM events like a doorbell system in a house:

  • The doorbell button is like an element in your HTML (e.g., a button)
  • The action of pressing corresponds to the event type (e.g., 'click')
  • The wiring represents the event listener that connects the action to a response
  • The chime sound is like your event handler function that executes when triggered
  • The person who answers the door is like your JavaScript code responding to the event

Just as you might install different doorbells for different entrances, you can attach different event listeners to various elements on your page.

graph LR A[User Action] -->|Triggers| B[Event] B -->|Captured by| C[Event Listener] C -->|Executes| D[Event Handler Function] D -->|May Modify| E[DOM] classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px;

The DOM Event Model

Modern browsers implement the DOM Level 3 Events specification, which defines a standard model for working with events. Let's explore the key components of this model:

Key Event Concepts

Event
An object representing an occurrence (like a user clicking a button, a page finishing loading, etc.)
Event Target
The object on which the event occurred or is targeted
Event Listener
A function registered to be called when a specific event occurs
Event Handler
The function that contains the code to be executed in response to the event
Event Propagation
The process by which events travel through the DOM tree (capturing and bubbling phases)
Event Object
Contains information about the event, such as its type, target, timestamp, etc.

Basic Event Registration

There are multiple ways to register event listeners in JavaScript:

Method 1: addEventListener (Recommended)

// Select the element
const button = document.getElementById('submit-button');

// Add an event listener
button.addEventListener('click', function(event) {
    console.log('Button was clicked!');
    console.log('Event object:', event);
});

Method 2: Event Handler Property

// Select the element
const input = document.getElementById('username');

// Assign a function to the event handler property
input.onchange = function(event) {
    console.log('Input value changed to:', this.value);
};

Method 3: Inline HTML Attribute (Not Recommended)

<button onclick="handleClick()">Click Me</button>

<script>
function handleClick() {
    console.log('Button was clicked!');
}
</script>

Best Practice

addEventListener() is preferred because it:

  • Allows attaching multiple listeners to the same event
  • Enables event capturing (discussed later)
  • Provides better separation of HTML and JavaScript
  • Allows for easier removal of listeners

The Event Object

When an event occurs, the browser creates an Event object containing information about the event. This object is automatically passed to your event handler function:

document.getElementById('my-button').addEventListener('click', function(event) {
    console.log('Event type:', event.type); // "click"
    console.log('Target element:', event.target); // The element that was clicked
    console.log('Current target:', event.currentTarget); // The element the listener is attached to
    console.log('Time stamp:', event.timeStamp); // When the event occurred
    console.log('Mouse coordinates:', event.clientX, event.clientY); // Mouse position
    
    // Prevent the default browser action for this event
    event.preventDefault();
    
    // Stop the event from propagating further
    event.stopPropagation();
});

Common Event Object Properties

Property Description Example Value
type The type of event "click", "keydown", "submit"
target The element that triggered the event The button element that was clicked
currentTarget The element the listener is attached to (may differ from target during bubbling) The parent element with the event listener
timeStamp When the event occurred (milliseconds since page load) 1234.56
preventDefault() Method to prevent the default browser action N/A (function)
stopPropagation() Method to stop event bubbling/capturing N/A (function)

Event Object Type-Specific Properties

Different event types provide additional properties specific to the type of interaction:

Mouse Events

document.getElementById('image').addEventListener('mousemove', function(event) {
    // Mouse coordinates relative to the viewport
    console.log('Client coordinates:', event.clientX, event.clientY);
    
    // Mouse coordinates relative to the document
    console.log('Page coordinates:', event.pageX, event.pageY);
    
    // Mouse coordinates relative to the target element
    console.log('Element coordinates:', event.offsetX, event.offsetY);
    
    // Which mouse button was pressed (0: left, 1: middle, 2: right)
    console.log('Button pressed:', event.button);
    
    // Modifier keys held during the event
    console.log('Ctrl key?', event.ctrlKey);
    console.log('Shift key?', event.shiftKey);
    console.log('Alt key?', event.altKey);
});

Keyboard Events

document.getElementById('input-field').addEventListener('keydown', function(event) {
    // The key that was pressed
    console.log('Key pressed:', event.key);
    
    // Key code (deprecated but still used in some contexts)
    console.log('Key code:', event.keyCode);
    
    // For special keys like Ctrl, Shift, Alt
    console.log('Ctrl key?', event.ctrlKey);
    console.log('Shift key?', event.shiftKey);
    console.log('Alt key?', event.altKey);
    
    // Prevent typing in the field for certain keys
    if (event.key === '@' || event.key === '#') {
        event.preventDefault();
        console.log('Special character blocked');
    }
});

Form Events

document.getElementById('signup-form').addEventListener('submit', function(event) {
    // Prevent the form from submitting normally
    event.preventDefault();
    
    // Form data
    const formData = new FormData(this);
    
    // Log form values
    for (const [name, value] of formData.entries()) {
        console.log(`${name}: ${value}`);
    }
    
    // Validate and submit via AJAX instead
    validateAndSubmit(formData);
});

Types of DOM Events

The DOM provides a rich set of event types to capture various user interactions and browser occurrences. Let's explore the most common types:

Mouse Events

Event Type Triggered When Common Use Cases
click Element is clicked Button actions, menu selection, toggling elements
dblclick Element is double-clicked Opening items, activating edit mode
mousedown Mouse button is pressed down Drag operations, drawing tools
mouseup Mouse button is released Drag operations, drawing tools
mousemove Mouse is moved over an element Interactive visualizations, hover effects
mouseenter Mouse enters an element Tooltips, highlight effects (doesn't bubble)
mouseleave Mouse leaves an element Removing tooltips, resetting hover states (doesn't bubble)
mouseover Mouse enters an element or its children Similar to mouseenter, but bubbles to parent elements
mouseout Mouse leaves an element or enters child elements Similar to mouseleave, but bubbles to parent elements
contextmenu Right mouse button is clicked Custom context menus, alternative actions

Example: Creating a Custom Tooltip

const tooltipTrigger = document.getElementById('info-icon');
const tooltip = document.getElementById('tooltip');

tooltipTrigger.addEventListener('mouseenter', function(event) {
    // Position the tooltip near the mouse
    const triggerRect = this.getBoundingClientRect();
    
    tooltip.style.left = (triggerRect.right + 10) + 'px';
    tooltip.style.top = triggerRect.top + 'px';
    
    // Show the tooltip
    tooltip.classList.add('visible');
});

tooltipTrigger.addEventListener('mouseleave', function() {
    // Hide the tooltip
    tooltip.classList.remove('visible');
});

Keyboard Events

Event Type Triggered When Common Use Cases
keydown Key is pressed down Keyboard shortcuts, form validation, games
keyup Key is released Form validation, detecting typing completion
keypress Key is pressed (character keys only; deprecated) Detecting character input (prefer keydown now)

Example: Keyboard Shortcuts

document.addEventListener('keydown', function(event) {
    // Check for Ctrl+S (Save)
    if (event.ctrlKey && event.key === 's') {
        event.preventDefault(); // Prevent browser's save dialog
        saveDocument();
        console.log('Document saved via keyboard shortcut');
    }
    
    // Check for Esc key (Cancel)
    if (event.key === 'Escape') {
        closeModal();
        console.log('Modal closed via Escape key');
    }
});

Form Events

Event Type Triggered When Common Use Cases
submit Form is submitted Form validation, AJAX submission
reset Form is reset Confirming form reset, cleanup actions
change Input value changes and element loses focus Form validation, updating dependent fields
input Input value changes (immediately) Real-time validation, character counting
focus Element receives focus Showing help text, visual highlighting
blur Element loses focus Validation, saving changes
select Text is selected in input/textarea Copy to clipboard functionality, formatting options

Example: Real-time Character Counter

const textarea = document.getElementById('message');
const counter = document.getElementById('char-counter');
const maxLength = parseInt(textarea.getAttribute('maxlength'));

textarea.addEventListener('input', function() {
    const remaining = maxLength - this.value.length;
    counter.textContent = `${remaining} characters remaining`;
    
    // Change color when getting close to the limit
    if (remaining < 20) {
        counter.classList.add('warning');
    } else {
        counter.classList.remove('warning');
    }
});

Document and Window Events

Event Type Triggered When Common Use Cases
DOMContentLoaded HTML is loaded and DOM is constructed Initializing UI, loading initial data
load Page and all resources are fully loaded Operations that need all resources (images, etc.)
resize Window size changes Responsive adjustments, layout recalculations
scroll Document or element is scrolled Infinite scrolling, scroll-based animations
beforeunload Page is about to be unloaded Warning about unsaved changes
unload Page is being unloaded Saving state, logging activity
online/offline Browser's network connection changes Offline mode handling, connectivity warnings

Example: Initialization and Window Events

// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
    console.log('DOM is ready, initializing app...');
    initApp();
});

// Handle loading of all resources
window.addEventListener('load', function() {
    console.log('All resources loaded');
    document.getElementById('loading-screen').style.display = 'none';
});

// Responsive layout adjustments
let resizeTimeout;
window.addEventListener('resize', function() {
    // Debounce resize events to improve performance
    clearTimeout(resizeTimeout);
    resizeTimeout = setTimeout(function() {
        console.log('Window resized, adjusting layout...');
        adjustLayout();
    }, 250);
});

// Handle network connectivity changes
window.addEventListener('online', function() {
    console.log('Back online');
    document.getElementById('connection-status').textContent = 'Online';
    document.getElementById('connection-status').className = 'status-online';
    syncOfflineChanges();
});

window.addEventListener('offline', function() {
    console.log('Connection lost');
    document.getElementById('connection-status').textContent = 'Offline';
    document.getElementById('connection-status').className = 'status-offline';
});

Media Events

Event Type Triggered When Common Use Cases
play/pause Media playback starts/pauses UI updates, analytics tracking
ended Media playback completes Playing next item, showing recommendations
timeupdate Current playback position changes Updating progress bar, synchronized transcripts
volumechange Volume is changed or muted Updating volume UI, saving preferences
loadeddata Media data is loaded Showing preview, enabling playback controls

Example: Custom Video Player Controls

const video = document.getElementById('video-player');
const playButton = document.getElementById('play-button');
const progressBar = document.getElementById('progress-bar');
const timeDisplay = document.getElementById('time-display');

// Toggle play/pause
playButton.addEventListener('click', function() {
    if (video.paused) {
        video.play();
        playButton.textContent = 'Pause';
    } else {
        video.pause();
        playButton.textContent = 'Play';
    }
});

// Update progress bar and time display
video.addEventListener('timeupdate', function() {
    // Update progress bar position
    const progress = (video.currentTime / video.duration) * 100;
    progressBar.style.width = progress + '%';
    
    // Update time display
    const minutes = Math.floor(video.currentTime / 60);
    const seconds = Math.floor(video.currentTime % 60);
    timeDisplay.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
});

// Handle video completion
video.addEventListener('ended', function() {
    playButton.textContent = 'Replay';
    progressBar.style.width = '100%';
});

Custom Events

In addition to the built-in events, you can create and dispatch your own custom events:

Creating and Dispatching Custom Events

// Create a custom event
const productAddedEvent = new CustomEvent('productAdded', {
    detail: {
        productId: 12345,
        productName: 'Wireless Headphones',
        price: 79.99,
        quantity: 1
    },
    bubbles: true,     // Whether the event bubbles up through the DOM
    cancelable: true   // Whether the event can be canceled
});

// Dispatch the event on an element
document.getElementById('shopping-cart').dispatchEvent(productAddedEvent);

// Listen for the custom event
document.getElementById('shopping-cart').addEventListener('productAdded', function(event) {
    console.log('Product added to cart:', event.detail.productName);
    updateCartCount(event.detail.quantity);
    updateCartTotal(event.detail.price * event.detail.quantity);
});

// You can also create simpler events
const simpleEvent = new Event('refresh', {
    bubbles: true,
    cancelable: false
});

document.getElementById('data-container').dispatchEvent(simpleEvent);

Use Cases for Custom Events

  • Component Communication: Allow separate components to communicate without tight coupling
  • Application State Changes: Broadcast when important state changes occur
  • Custom UI Interactions: Create specialized events for your application's unique interactions
  • Plugin/Extension APIs: Allow third-party code to hook into your application's lifecycle
  • Polyfills: Implement missing browser events in older browsers

Example: Component Communication

// User Profile Component
class UserProfile {
    constructor() {
        this.loadUserData();
        
        // Listen for theme changes from other components
        document.addEventListener('themeChanged', this.updateTheme.bind(this));
    }
    
    updateTheme(event) {
        console.log('Profile updating theme to:', event.detail.theme);
        this.applyTheme(event.detail.theme);
    }
    
    applyTheme(theme) {
        // Apply theme-specific styles to the profile
        document.getElementById('user-profile').className = `profile-card theme-${theme}`;
    }
    
    loadUserData() {
        // Load user data
    }
}

// Settings Component
class SettingsPanel {
    constructor() {
        // Set up theme selector
        document.getElementById('theme-selector')
            .addEventListener('change', this.themeChangeHandler.bind(this));
    }
    
    themeChangeHandler(event) {
        const selectedTheme = event.target.value;
        
        // Save the theme preference
        localStorage.setItem('user-theme', selectedTheme);
        
        // Broadcast the theme change to other components
        const themeEvent = new CustomEvent('themeChanged', {
            detail: {
                theme: selectedTheme,
                timestamp: new Date().toISOString()
            },
            bubbles: true
        });
        
        document.dispatchEvent(themeEvent);
    }
}

// Initialize components
const profile = new UserProfile();
const settings = new SettingsPanel();

Event Performance and Best Practices

Event Delegation

Instead of attaching event listeners to many individual elements, attach one listener to a parent element and use the event.target property to determine which child element triggered the event:

Inefficient (Many Listeners):

// Adding a listener to each button
document.querySelectorAll('.product-card .add-to-cart').forEach(button => {
    button.addEventListener('click', function(event) {
        const productId = this.dataset.productId;
        addToCart(productId);
    });
});

Efficient (Event Delegation):

// One listener on the parent container
document.getElementById('product-list').addEventListener('click', function(event) {
    // Check if the clicked element is an add-to-cart button
    if (event.target.classList.contains('add-to-cart')) {
        const productId = event.target.dataset.productId;
        addToCart(productId);
    }
});

Benefits of Event Delegation:

  • Reduces memory usage by creating fewer event listeners
  • Works for dynamically added elements without needing to attach new listeners
  • Simplifies code by centralizing event handling
  • Improves performance, especially for large lists or tables

Debouncing and Throttling

For events that can fire rapidly (like scroll, resize, mousemove), use debouncing or throttling to limit how often your handler function executes:

Debounce Function

// Execute the function only after the user stops triggering events for a set time
function debounce(func, wait) {
    let timeout;
    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
    };
}

// Example: Search input that only triggers API calls after typing stops
const searchInput = document.getElementById('search-input');
const searchHandler = debounce(function(event) {
    const query = event.target.value;
    console.log('Searching for:', query);
    searchAPI(query);
}, 500); // Wait 500ms after typing stops

searchInput.addEventListener('input', searchHandler);

Throttle Function

// Execute the function at most once per specified time period
function throttle(func, limit) {
    let inThrottle;
    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

// Example: Scroll handler that updates UI at most every 100ms
const scrollHandler = throttle(function() {
    updateScrollBasedUI();
}, 100);

window.addEventListener('scroll', scrollHandler);

When to Use Each:

  • Debounce: When you want to execute only after the "final" event in a series (e.g., resize completion, typing completion)
  • Throttle: When you want to execute regularly during continuous events, but at a controlled rate (e.g., scrolling, dragging)

Removing Event Listeners

Always remove event listeners when they're no longer needed to prevent memory leaks:

// Store a reference to the handler function
function handleClick(event) {
    console.log('Button clicked!');
    
    // Remove the listener after first click
    this.removeEventListener('click', handleClick);
    this.classList.add('clicked');
}

// Add the listener
document.getElementById('one-time-button').addEventListener('click', handleClick);

// For components or elements that may be removed:
class TemporaryComponent {
    constructor(elementId) {
        this.element = document.getElementById(elementId);
        
        // Store bound handlers to reference for removal later
        this.clickHandler = this.handleClick.bind(this);
        this.keyHandler = this.handleKeypress.bind(this);
        
        // Add listeners
        this.element.addEventListener('click', this.clickHandler);
        document.addEventListener('keydown', this.keyHandler);
    }
    
    handleClick(event) {
        console.log('Component clicked');
    }
    
    handleKeypress(event) {
        if (event.key === 'Escape') {
            this.destroy();
        }
    }
    
    destroy() {
        // Clean up all event listeners
        this.element.removeEventListener('click', this.clickHandler);
        document.removeEventListener('keydown', this.keyHandler);
        
        // Remove from DOM
        this.element.remove();
    }
}

Real-World Applications

Application 1: Dynamic Form Validation

Using multiple event types to provide real-time feedback:

document.addEventListener('DOMContentLoaded', function() {
    const form = document.getElementById('registration-form');
    const emailInput = document.getElementById('email');
    const passwordInput = document.getElementById('password');
    const confirmPasswordInput = document.getElementById('confirm-password');
    
    // Validate email as the user types
    emailInput.addEventListener('input', function() {
        validateEmail(this);
    });
    
    // Mark field as visited when user leaves it
    emailInput.addEventListener('blur', function() {
        this.classList.add('touched');
        validateEmail(this);
    });
    
    // Show password strength meter as user types
    passwordInput.addEventListener('input', function() {
        evaluatePasswordStrength(this);
    });
    
    // Check password match on confirmation input
    confirmPasswordInput.addEventListener('input', function() {
        validatePasswordMatch(this, passwordInput);
    });
    
    // Handle form submission
    form.addEventListener('submit', function(event) {
        // Perform final validation
        let isValid = true;
        
        isValid = validateEmail(emailInput) && isValid;
        isValid = evaluatePasswordStrength(passwordInput) && isValid;
        isValid = validatePasswordMatch(confirmPasswordInput, passwordInput) && isValid;
        
        if (!isValid) {
            event.preventDefault();
            showFormError('Please correct the errors in the form.');
        }
    });
    
    function validateEmail(input) {
        const value = input.value.trim();
        const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        const isValid = emailPattern.test(value);
        
        const errorElement = input.nextElementSibling;
        
        if (!value) {
            setInvalid(input, errorElement, 'Email is required');
            return false;
        } else if (!isValid) {
            setInvalid(input, errorElement, 'Please enter a valid email address');
            return false;
        } else {
            setValid(input, errorElement);
            return true;
        }
    }
    
    function evaluatePasswordStrength(input) {
        const value = input.value;
        const strengthMeter = document.getElementById('password-strength');
        
        // Reset status
        input.classList.remove('valid', 'invalid');
        
        if (!value) {
            strengthMeter.textContent = '';
            return false;
        }
        
        // Calculate strength score (0-100)
        let score = 0;
        
        // Length check (up to 40 points)
        score += Math.min(value.length * 4, 40);
        
        // Complexity checks
        if (/[A-Z]/.test(value)) score += 10; // Uppercase
        if (/[a-z]/.test(value)) score += 10; // Lowercase
        if (/[0-9]/.test(value)) score += 10; // Numbers
        if (/[^A-Za-z0-9]/.test(value)) score += 15; // Special characters
        
        // Variety of characters (up to 15 points)
        const uniqueChars = new Set(value.split('')).size;
        score += Math.min(uniqueChars, 15);
        
        // Determine strength level
        let strengthLevel, strengthText;
        
        if (score < 40) {
            strengthLevel = 'weak';
            strengthText = 'Weak';
        } else if (score < 70) {
            strengthLevel = 'medium';
            strengthText = 'Medium';
        } else {
            strengthLevel = 'strong';
            strengthText = 'Strong';
        }
        
        // Update UI
        strengthMeter.textContent = strengthText;
        strengthMeter.className = `strength-${strengthLevel}`;
        
        // Mark as valid only if at least medium strength
        if (strengthLevel === 'weak') {
            input.classList.add('invalid');
            return false;
        } else {
            input.classList.add('valid');
            return true;
        }
    }
    
    function validatePasswordMatch(confirmInput, passwordInput) {
        const confirmValue = confirmInput.value;
        const passwordValue = passwordInput.value;
        const errorElement = confirmInput.nextElementSibling;
        
        if (!confirmValue) {
            setInvalid(confirmInput, errorElement, 'Please confirm your password');
            return false;
        } else if (confirmValue !== passwordValue) {
            setInvalid(confirmInput, errorElement, 'Passwords do not match');
            return false;
        } else {
            setValid(confirmInput, errorElement);
            return true;
        }
    }
    
    function setValid(input, errorElement) {
        input.classList.remove('invalid');
        input.classList.add('valid');
        errorElement.textContent = '';
    }
    
    function setInvalid(input, errorElement, message) {
        input.classList.remove('valid');
        input.classList.add('invalid');
        errorElement.textContent = message;
    }
    
    function showFormError(message) {
        const formError = document.getElementById('form-error');
        formError.textContent = message;
        formError.classList.add('visible');
        
        // Scroll to error message
        formError.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }
});

Application 2: Drag and Drop File Uploader

Combining multiple event types for a rich user interface:

document.addEventListener('DOMContentLoaded', function() {
    const dropZone = document.getElementById('drop-zone');
    const fileInput = document.getElementById('file-input');
    const uploadButton = document.getElementById('upload-button');
    const fileList = document.getElementById('file-list');
    
    // Selected files storage
    const selectedFiles = new Set();
    
    // Click on drop zone to open file picker
    dropZone.addEventListener('click', function() {
        fileInput.click();
    });
    
    // Handle file selection via input
    fileInput.addEventListener('change', function() {
        handleFiles(this.files);
    });
    
    // Drag and drop events
    dropZone.addEventListener('dragover', function(event) {
        event.preventDefault();
        dropZone.classList.add('drag-over');
    });
    
    dropZone.addEventListener('dragleave', function() {
        dropZone.classList.remove('drag-over');
    });
    
    dropZone.addEventListener('drop', function(event) {
        event.preventDefault();
        dropZone.classList.remove('drag-over');
        
        // Access the dropped files
        const files = event.dataTransfer.files;
        handleFiles(files);
    });
    
    // Handle the upload button
    uploadButton.addEventListener('click', function() {
        if (selectedFiles.size === 0) {
            showMessage('Please select files to upload', 'error');
            return;
        }
        
        uploadFiles(Array.from(selectedFiles));
    });
    
    // Process the selected files
    function handleFiles(fileList) {
        if (fileList.length === 0) return;
        
        for (const file of fileList) {
            // Check file type and size
            if (!validateFile(file)) continue;
            
            // Add to selected files
            selectedFiles.add(file);
            
            // Create preview
            createFilePreview(file);
        }
        
        // Update upload button state
        updateUploadButton();
    }
    
    function validateFile(file) {
        // Check file type (allow images and PDFs)
        const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
        if (!allowedTypes.includes(file.type)) {
            showMessage(`File type ${file.type} is not supported`, 'error');
            return false;
        }
        
        // Check file size (max 5MB)
        const maxSize = 5 * 1024 * 1024; // 5MB in bytes
        if (file.size > maxSize) {
            showMessage(`File ${file.name} is too large (max 5MB)`, 'error');
            return false;
        }
        
        return true;
    }
    
    function createFilePreview(file) {
        const item = document.createElement('div');
        item.className = 'file-item';
        item.dataset.name = file.name;
        
        // Create preview image or icon
        let preview;
        if (file.type.startsWith('image/')) {
            preview = document.createElement('img');
            preview.className = 'file-preview';
            const imageUrl = URL.createObjectURL(file);
            preview.src = imageUrl;
            
            // Clean up object URL when preview is loaded
            preview.onload = () => URL.revokeObjectURL(imageUrl);
        } else {
            preview = document.createElement('div');
            preview.className = 'file-icon pdf-icon';
        }
        
        // File info
        const info = document.createElement('div');
        info.className = 'file-info';
        
        const name = document.createElement('div');
        name.className = 'file-name';
        name.textContent = file.name;
        
        const size = document.createElement('div');
        size.className = 'file-size';
        size.textContent = formatFileSize(file.size);
        
        info.appendChild(name);
        info.appendChild(size);
        
        // Remove button
        const removeBtn = document.createElement('button');
        removeBtn.className = 'remove-file';
        removeBtn.innerHTML = '×';
        removeBtn.addEventListener('click', function(event) {
            event.stopPropagation();
            
            // Remove file from selected files
            selectedFiles.delete(file);
            
            // Remove preview
            item.remove();
            
            // Update upload button state
            updateUploadButton();
            
            // Clean up object URL if image
            if (file.type.startsWith('image/')) {
                URL.revokeObjectURL(preview.src);
            }
        });
        
        // Assemble file item
        item.appendChild(preview);
        item.appendChild(info);
        item.appendChild(removeBtn);
        
        // Add to file list
        fileList.appendChild(item);
        
        // Show the file list
        fileList.classList.remove('hidden');
    }
    
    function updateUploadButton() {
        if (selectedFiles.size > 0) {
            uploadButton.disabled = false;
            uploadButton.textContent = `Upload ${selectedFiles.size} File${selectedFiles.size !== 1 ? 's' : ''}`;
        } else {
            uploadButton.disabled = true;
            uploadButton.textContent = 'Upload';
        }
    }
    
    function formatFileSize(bytes) {
        if (bytes < 1024) return bytes + ' bytes';
        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
        return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
    }
    
    function showMessage(text, type) {
        const messageEl = document.getElementById('message');
        messageEl.textContent = text;
        messageEl.className = `message ${type}`;
        messageEl.classList.add('visible');
        
        // Hide after 4 seconds
        setTimeout(() => {
            messageEl.classList.remove('visible');
        }, 4000);
    }
    
    function uploadFiles(files) {
        // In a real app, you would use FormData and fetch/XMLHttpRequest
        // to upload the files to a server
        
        showMessage('Upload started...', 'info');
        
        // Simulate upload progress
        let progress = 0;
        const progressBar = document.getElementById('progress-bar');
        progressBar.style.width = '0%';
        progressBar.parentElement.classList.add('visible');
        
        const interval = setInterval(() => {
            progress += 10;
            progressBar.style.width = progress + '%';
            
            if (progress >= 100) {
                clearInterval(interval);
                
                setTimeout(() => {
                    progressBar.parentElement.classList.remove('visible');
                    showMessage('Files uploaded successfully!', 'success');
                    
                    // Clear all files
                    selectedFiles.clear();
                    fileList.innerHTML = '';
                    updateUploadButton();
                }, 500);
            }
        }, 300);
    }
});

Practical Exercise

Create an interactive "to-do list" application that demonstrates various event types:

  1. Set up a form to add new tasks with fields for title, description, and due date
  2. Implement form validation that provides real-time feedback as the user types
  3. Use event delegation to handle clicks on task items (complete, edit, delete)
  4. Add keyboard shortcuts (e.g., press "n" to focus the new task form)
  5. Implement drag and drop to reorder tasks
  6. Create a filter system that responds to change events
  7. Use local storage to save tasks, triggering storage events
  8. Add custom events to notify when tasks are approaching their due date
  9. Implement debounced search functionality

This exercise will give you practice with a wide range of event types and event handling techniques in a realistic context.

Summary

In the next lecture, we'll explore event handlers and listeners in more depth, including advanced techniques for managing event callbacks.

Additional Resources