Form Events and Validation

Building Interactive and Secure Forms with JavaScript

Understanding Form Events

Forms are the primary way users interact with web applications. Think of form events as conversations between your users and your application - each interaction (typing, selecting, submitting) is a message that your code can respond to. Proper form handling and validation ensures a smooth, secure user experience.

graph TD A[User Interaction] --> B{Form Event} B --> C[Input Event] B --> D[Change Event] B --> E[Submit Event] B --> F[Focus/Blur Events] C --> G[Real-time Validation] D --> H[Field Validation] E --> I[Form Validation] F --> J[UI Feedback] G --> K[User Feedback] H --> K I --> K J --> K style A fill:#f9f,stroke:#333,stroke-width:4px style K fill:#bfb,stroke:#333,stroke-width:4px

Common Form Events

Forms trigger various events during user interaction, each serving a specific purpose.

Input Events

// input event - fires on every change
const searchInput = document.querySelector('#search');

searchInput.addEventListener('input', (e) => {
    console.log('Current value:', e.target.value);
    // Perfect for real-time search, validation, or filtering
    performSearch(e.target.value);
});

// change event - fires when value is committed
const selectElement = document.querySelector('#category');

selectElement.addEventListener('change', (e) => {
    console.log('Selected:', e.target.value);
    // Good for dropdown selections, radio buttons
    updateCategory(e.target.value);
});

// Real-time character counter
const textarea = document.querySelector('#message');
const charCount = document.querySelector('#char-count');
const maxChars = 280;

textarea.addEventListener('input', (e) => {
    const remaining = maxChars - e.target.value.length;
    charCount.textContent = `${remaining} characters remaining`;
    
    // Visual feedback
    if (remaining < 20) {
        charCount.style.color = 'red';
    } else {
        charCount.style.color = 'green';
    }
});

// Input event with debouncing for performance
function debounce(func, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => func.apply(this, args), delay);
    };
}

const searchBox = document.querySelector('#search-box');
const debouncedSearch = debounce((query) => {
    // Expensive search operation
    fetch(`/api/search?q=${encodeURIComponent(query)}`)
        .then(response => response.json())
        .then(results => displayResults(results));
}, 300);

searchBox.addEventListener('input', (e) => {
    debouncedSearch(e.target.value);
});

Focus and Blur Events

// Focus events for UI enhancement
const formInputs = document.querySelectorAll('input, textarea');

formInputs.forEach(input => {
    input.addEventListener('focus', (e) => {
        // Highlight the focused field
        e.target.parentElement.classList.add('focused');
        
        // Show helper text
        const helper = e.target.parentElement.querySelector('.helper-text');
        if (helper) {
            helper.style.display = 'block';
        }
    });
    
    input.addEventListener('blur', (e) => {
        // Remove highlight
        e.target.parentElement.classList.remove('focused');
        
        // Hide helper text
        const helper = e.target.parentElement.querySelector('.helper-text');
        if (helper) {
            helper.style.display = 'none';
        }
        
        // Validate on blur
        validateField(e.target);
    });
});

// Focus management for accessibility
class FormFocusManager {
    constructor(form) {
        this.form = form;
        this.focusableElements = form.querySelectorAll(
            'input, select, textarea, button, [tabindex]:not([tabindex="-1"])'
        );
        
        this.setupFocusManagement();
    }
    
    setupFocusManagement() {
        // Trap focus within form (useful for modals)
        this.form.addEventListener('keydown', (e) => {
            if (e.key === 'Tab') {
                const focusable = Array.from(this.focusableElements);
                const first = focusable[0];
                const last = focusable[focusable.length - 1];
                
                if (e.shiftKey && document.activeElement === first) {
                    e.preventDefault();
                    last.focus();
                } else if (!e.shiftKey && document.activeElement === last) {
                    e.preventDefault();
                    first.focus();
                }
            }
        });
    }
    
    focusFirstElement() {
        this.focusableElements[0]?.focus();
    }
    
    focusFirstInvalid() {
        const firstInvalid = this.form.querySelector('.invalid');
        if (firstInvalid) {
            firstInvalid.focus();
            return true;
        }
        return false;
    }
}

Submit Event

// Basic form submission
const form = document.querySelector('#contact-form');

form.addEventListener('submit', (e) => {
    e.preventDefault(); // Prevent default form submission
    
    // Get form data
    const formData = new FormData(form);
    
    // Validate form
    if (validateForm(form)) {
        // Submit data
        submitForm(formData);
    }
});

// Advanced form submission handling
class FormHandler {
    constructor(form) {
        this.form = form;
        this.submitButton = form.querySelector('button[type="submit"]');
        this.setupSubmitHandler();
    }
    
    setupSubmitHandler() {
        this.form.addEventListener('submit', async (e) => {
            e.preventDefault();
            
            if (!this.validateForm()) {
                return;
            }
            
            this.setLoadingState(true);
            
            try {
                const formData = new FormData(this.form);
                const response = await this.submitData(formData);
                
                if (response.ok) {
                    this.handleSuccess(response);
                } else {
                    this.handleError(response);
                }
            } catch (error) {
                this.handleError(error);
            } finally {
                this.setLoadingState(false);
            }
        });
    }
    
    validateForm() {
        // Implementation in validation section
        return true;
    }
    
    setLoadingState(loading) {
        if (loading) {
            this.submitButton.disabled = true;
            this.submitButton.innerHTML = ' Submitting...';
            this.form.classList.add('submitting');
        } else {
            this.submitButton.disabled = false;
            this.submitButton.textContent = 'Submit';
            this.form.classList.remove('submitting');
        }
    }
    
    async submitData(formData) {
        const response = await fetch(this.form.action, {
            method: this.form.method || 'POST',
            body: formData
        });
        
        return response;
    }
    
    handleSuccess(response) {
        this.form.reset();
        this.showMessage('Form submitted successfully!', 'success');
        
        // Redirect if needed
        if (response.redirected) {
            window.location.href = response.url;
        }
    }
    
    handleError(error) {
        this.showMessage('An error occurred. Please try again.', 'error');
        console.error('Form submission error:', error);
    }
    
    showMessage(message, type) {
        const messageEl = document.createElement('div');
        messageEl.className = `form-message ${type}`;
        messageEl.textContent = message;
        
        this.form.insertBefore(messageEl, this.form.firstChild);
        
        // Remove message after 5 seconds
        setTimeout(() => {
            messageEl.remove();
        }, 5000);
    }
}

Form Validation

Validation ensures data integrity and provides immediate feedback to users. There are three types of validation: HTML5 built-in, JavaScript custom, and server-side validation.

HTML5 Validation


Custom JavaScript Validation

// Comprehensive form validator
class FormValidator {
    constructor(form, rules) {
        this.form = form;
        this.rules = rules;
        this.errors = {};
        
        this.setupValidation();
    }
    
    setupValidation() {
        // Real-time validation on input
        Object.keys(this.rules).forEach(fieldName => {
            const field = this.form.elements[fieldName];
            if (field) {
                field.addEventListener('input', () => {
                    this.validateField(fieldName);
                });
                
                field.addEventListener('blur', () => {
                    this.validateField(fieldName);
                });
            }
        });
        
        // Form submission validation
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            
            if (this.validateForm()) {
                this.submitForm();
            }
        });
    }
    
    validateField(fieldName) {
        const field = this.form.elements[fieldName];
        const rules = this.rules[fieldName];
        const value = field.value;
        
        // Clear previous errors
        delete this.errors[fieldName];
        
        // Check each rule
        for (const rule of rules) {
            const result = rule.validator(value, field);
            
            if (!result) {
                this.errors[fieldName] = rule.message;
                this.showFieldError(field, rule.message);
                return false;
            }
        }
        
        this.clearFieldError(field);
        return true;
    }
    
    validateForm() {
        let isValid = true;
        
        Object.keys(this.rules).forEach(fieldName => {
            if (!this.validateField(fieldName)) {
                isValid = false;
            }
        });
        
        return isValid;
    }
    
    showFieldError(field, message) {
        // Remove existing error message
        this.clearFieldError(field);
        
        // Add error class
        field.classList.add('invalid');
        field.setAttribute('aria-invalid', 'true');
        
        // Create error message
        const errorElement = document.createElement('div');
        errorElement.className = 'error-message';
        errorElement.textContent = message;
        errorElement.id = `${field.name}-error`;
        
        // Associate error with field
        field.setAttribute('aria-describedby', errorElement.id);
        
        // Insert error message
        field.parentNode.insertBefore(errorElement, field.nextSibling);
    }
    
    clearFieldError(field) {
        field.classList.remove('invalid');
        field.removeAttribute('aria-invalid');
        field.removeAttribute('aria-describedby');
        
        const errorElement = field.parentNode.querySelector('.error-message');
        if (errorElement) {
            errorElement.remove();
        }
    }
    
    async submitForm() {
        const formData = new FormData(this.form);
        
        try {
            const response = await fetch(this.form.action, {
                method: 'POST',
                body: formData
            });
            
            if (response.ok) {
                this.form.reset();
                this.showSuccess('Form submitted successfully!');
            } else {
                const error = await response.json();
                this.showError(error.message || 'Submission failed');
            }
        } catch (error) {
            this.showError('Network error. Please try again.');
        }
    }
    
    showSuccess(message) {
        // Implementation
    }
    
    showError(message) {
        // Implementation
    }
}

// Validation rules
const validationRules = {
    username: [
        {
            validator: value => value.length >= 3,
            message: 'Username must be at least 3 characters long'
        },
        {
            validator: value => /^[a-zA-Z0-9_]+$/.test(value),
            message: 'Username can only contain letters, numbers, and underscores'
        }
    ],
    email: [
        {
            validator: value => value.length > 0,
            message: 'Email is required'
        },
        {
            validator: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
            message: 'Please enter a valid email address'
        }
    ],
    password: [
        {
            validator: value => value.length >= 8,
            message: 'Password must be at least 8 characters long'
        },
        {
            validator: value => /[A-Z]/.test(value),
            message: 'Password must contain at least one uppercase letter'
        },
        {
            validator: value => /[a-z]/.test(value),
            message: 'Password must contain at least one lowercase letter'
        },
        {
            validator: value => /[0-9]/.test(value),
            message: 'Password must contain at least one number'
        },
        {
            validator: value => /[^A-Za-z0-9]/.test(value),
            message: 'Password must contain at least one special character'
        }
    ],
    confirmPassword: [
        {
            validator: (value, field) => {
                const password = field.form.elements.password.value;
                return value === password;
            },
            message: 'Passwords do not match'
        }
    ]
};

// Initialize validator
const form = document.querySelector('#registration-form');
const validator = new FormValidator(form, validationRules);

Advanced Validation Patterns

// Cross-field validation
class CrossFieldValidator {
    constructor(form) {
        this.form = form;
        this.setupValidation();
    }
    
    setupValidation() {
        // Password confirmation
        const password = this.form.elements.password;
        const confirmPassword = this.form.elements.confirmPassword;
        
        confirmPassword.addEventListener('input', () => {
            if (confirmPassword.value !== password.value) {
                confirmPassword.setCustomValidity('Passwords do not match');
            } else {
                confirmPassword.setCustomValidity('');
            }
        });
        
        // Date range validation
        const startDate = this.form.elements.startDate;
        const endDate = this.form.elements.endDate;
        
        endDate.addEventListener('change', () => {
            if (new Date(endDate.value) < new Date(startDate.value)) {
                endDate.setCustomValidity('End date must be after start date');
            } else {
                endDate.setCustomValidity('');
            }
        });
    }
}

// Async validation (e.g., checking username availability)
class AsyncValidator {
    constructor(field, checkEndpoint) {
        this.field = field;
        this.checkEndpoint = checkEndpoint;
        this.timeout = null;
        
        this.setupValidation();
    }
    
    setupValidation() {
        this.field.addEventListener('input', () => {
            clearTimeout(this.timeout);
            
            // Debounce the check
            this.timeout = setTimeout(() => {
                this.checkAvailability();
            }, 500);
        });
    }
    
    async checkAvailability() {
        const value = this.field.value;
        
        if (value.length < 3) {
            return;
        }
        
        try {
            const response = await fetch(`${this.checkEndpoint}?value=${encodeURIComponent(value)}`);
            const result = await response.json();
            
            if (result.available) {
                this.showSuccess('Available!');
            } else {
                this.showError('Already taken');
            }
        } catch (error) {
            console.error('Availability check failed:', error);
        }
    }
    
    showSuccess(message) {
        this.field.classList.remove('invalid');
        this.field.classList.add('valid');
        // Update message UI
    }
    
    showError(message) {
        this.field.classList.remove('valid');
        this.field.classList.add('invalid');
        // Update message UI
    }
}

// Custom validation frameworks
const ValidationFramework = {
    rules: {
        required: value => value.trim().length > 0,
        email: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
        minLength: (value, length) => value.length >= length,
        maxLength: (value, length) => value.length <= length,
        pattern: (value, pattern) => new RegExp(pattern).test(value),
        numeric: value => !isNaN(value) && value !== '',
        alphanumeric: value => /^[a-zA-Z0-9]+$/.test(value),
        phone: value => /^\+?[\d\s-()]+$/.test(value),
        url: value => {
            try {
                new URL(value);
                return true;
            } catch {
                return false;
            }
        },
        creditCard: value => {
            // Basic credit card validation
            const cleaned = value.replace(/\D/g, '');
            return cleaned.length >= 13 && cleaned.length <= 19;
        },
        date: value => !isNaN(Date.parse(value)),
        matches: (value, otherFieldName, form) => {
            const otherField = form.elements[otherFieldName];
            return value === otherField.value;
        }
    },
    
    validate(field, rules) {
        const errors = [];
        
        rules.forEach(rule => {
            if (typeof rule === 'string') {
                // Simple rule
                if (!this.rules[rule](field.value)) {
                    errors.push(`${field.name} is invalid`);
                }
            } else {
                // Rule with parameters
                const [ruleName, ...params] = rule;
                if (!this.rules[ruleName](field.value, ...params)) {
                    errors.push(`${field.name} is invalid`);
                }
            }
        });
        
        return errors;
    }
};

Working with Form Components

Different form elements require specific handling approaches.

Input Types

// Text inputs
const textInput = document.querySelector('input[type="text"]');
textInput.addEventListener('input', (e) => {
    // Sanitize input
    e.target.value = e.target.value.replace(/[<>]/g, '');
});

// Number inputs
const numberInput = document.querySelector('input[type="number"]');
numberInput.addEventListener('input', (e) => {
    // Ensure valid number
    const value = parseInt(e.target.value);
    if (isNaN(value)) {
        e.target.value = '';
    }
});

// Date inputs
const dateInput = document.querySelector('input[type="date"]');
dateInput.addEventListener('change', (e) => {
    const selectedDate = new Date(e.target.value);
    const today = new Date();
    
    if (selectedDate > today) {
        alert('Cannot select future date');
        e.target.value = '';
    }
});

// File inputs
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (e) => {
    const files = e.target.files;
    
    // Validate file types
    const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
    const maxSize = 5 * 1024 * 1024; // 5MB
    
    Array.from(files).forEach(file => {
        if (!allowedTypes.includes(file.type)) {
            alert(`Invalid file type: ${file.type}`);
            e.target.value = '';
            return;
        }
        
        if (file.size > maxSize) {
            alert(`File too large: ${file.name}`);
            e.target.value = '';
            return;
        }
        
        // Preview image files
        if (file.type.startsWith('image/')) {
            const reader = new FileReader();
            reader.onload = (e) => {
                const preview = document.createElement('img');
                preview.src = e.target.result;
                preview.className = 'file-preview';
                document.querySelector('#preview-container').appendChild(preview);
            };
            reader.readAsDataURL(file);
        }
    });
});

// Password visibility toggle
function createPasswordToggle(passwordInput) {
    const toggleButton = document.createElement('button');
    toggleButton.type = 'button';
    toggleButton.textContent = '👁️';
    toggleButton.className = 'password-toggle';
    
    toggleButton.addEventListener('click', () => {
        if (passwordInput.type === 'password') {
            passwordInput.type = 'text';
            toggleButton.textContent = '👁️‍🗨️';
        } else {
            passwordInput.type = 'password';
            toggleButton.textContent = '👁️';
        }
    });
    
    passwordInput.parentNode.appendChild(toggleButton);
}

Select Elements

// Basic select handling
const select = document.querySelector('select');
select.addEventListener('change', (e) => {
    const selectedOption = e.target.options[e.target.selectedIndex];
    console.log('Selected:', selectedOption.value, selectedOption.text);
});

// Dynamic select options
class DynamicSelect {
    constructor(selectElement, dataSource) {
        this.select = selectElement;
        this.dataSource = dataSource;
        
        this.initialize();
    }
    
    async initialize() {
        // Load initial options
        const data = await this.dataSource.getData();
        this.populateOptions(data);
        
        // Setup filtering if searchable
        if (this.select.dataset.searchable) {
            this.makeSearchable();
        }
    }
    
    populateOptions(data) {
        // Clear existing options
        this.select.innerHTML = '';
        
        // Add default option
        const defaultOption = document.createElement('option');
        defaultOption.value = '';
        defaultOption.textContent = 'Select an option';
        this.select.appendChild(defaultOption);
        
        // Add data options
        data.forEach(item => {
            const option = document.createElement('option');
            option.value = item.value;
            option.textContent = item.label;
            
            if (item.data) {
                Object.entries(item.data).forEach(([key, value]) => {
                    option.dataset[key] = value;
                });
            }
            
            this.select.appendChild(option);
        });
    }
    
    makeSearchable() {
        // Create search input
        const searchInput = document.createElement('input');
        searchInput.type = 'text';
        searchInput.placeholder = 'Search...';
        searchInput.className = 'select-search';
        
        // Insert before select
        this.select.parentNode.insertBefore(searchInput, this.select);
        
        // Filter options on input
        searchInput.addEventListener('input', (e) => {
            const searchTerm = e.target.value.toLowerCase();
            const options = this.select.options;
            
            for (let i = 1; i < options.length; i++) {
                const option = options[i];
                const text = option.textContent.toLowerCase();
                
                if (text.includes(searchTerm)) {
                    option.style.display = '';
                } else {
                    option.style.display = 'none';
                }
            }
        });
    }
}

// Dependent selects (e.g., Country -> State -> City)
class DependentSelects {
    constructor(selects, dataSource) {
        this.selects = selects; // Array of select elements
        this.dataSource = dataSource;
        
        this.initialize();
    }
    
    initialize() {
        this.selects.forEach((select, index) => {
            if (index < this.selects.length - 1) {
                select.addEventListener('change', () => {
                    this.updateDependentSelect(index);
                });
            }
        });
        
        // Load initial data for first select
        this.loadSelectData(0);
    }
    
    async loadSelectData(index, parentValue = null) {
        const select = this.selects[index];
        const data = await this.dataSource.getData(index, parentValue);
        
        // Clear select
        select.innerHTML = '';
        
        // Populate options
        data.forEach(item => {
            const option = document.createElement('option');
            option.value = item.value;
            option.textContent = item.label;
            select.appendChild(option);
        });
        
        // Clear subsequent selects
        for (let i = index + 1; i < this.selects.length; i++) {
            this.selects[i].innerHTML = '';
            this.selects[i].disabled = true;
        }
    }
    
    async updateDependentSelect(index) {
        const currentSelect = this.selects[index];
        const nextSelect = this.selects[index + 1];
        
        if (currentSelect.value) {
            nextSelect.disabled = false;
            await this.loadSelectData(index + 1, currentSelect.value);
        } else {
            nextSelect.disabled = true;
            nextSelect.innerHTML = '';
        }
    }
}

Checkboxes and Radio Buttons

// Checkbox group handling
class CheckboxGroup {
    constructor(container) {
        this.container = container;
        this.checkboxes = container.querySelectorAll('input[type="checkbox"]');
        this.selectAllCheckbox = container.querySelector('.select-all');
        
        this.initialize();
    }
    
    initialize() {
        // Handle individual checkboxes
        this.checkboxes.forEach(checkbox => {
            checkbox.addEventListener('change', () => {
                this.updateSelectAll();
                this.triggerChangeEvent();
            });
        });
        
        // Handle select all
        if (this.selectAllCheckbox) {
            this.selectAllCheckbox.addEventListener('change', (e) => {
                this.checkboxes.forEach(checkbox => {
                    checkbox.checked = e.target.checked;
                });
                this.triggerChangeEvent();
            });
        }
    }
    
    updateSelectAll() {
        if (!this.selectAllCheckbox) return;
        
        const checkedCount = this.getCheckedValues().length;
        const totalCount = this.checkboxes.length;
        
        this.selectAllCheckbox.checked = checkedCount === totalCount;
        this.selectAllCheckbox.indeterminate = 
            checkedCount > 0 && checkedCount < totalCount;
    }
    
    getCheckedValues() {
        return Array.from(this.checkboxes)
            .filter(cb => cb.checked)
            .map(cb => cb.value);
    }
    
    triggerChangeEvent() {
        const event = new CustomEvent('checkboxGroupChange', {
            detail: { values: this.getCheckedValues() }
        });
        this.container.dispatchEvent(event);
    }
}

// Radio button group validation
class RadioGroup {
    constructor(name) {
        this.name = name;
        this.radios = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
        
        this.initialize();
    }
    
    initialize() {
        this.radios.forEach(radio => {
            radio.addEventListener('change', () => {
                this.clearValidationErrors();
            });
        });
    }
    
    getValue() {
        const checked = Array.from(this.radios).find(r => r.checked);
        return checked ? checked.value : null;
    }
    
    validate() {
        if (!this.getValue()) {
            this.showError('Please select an option');
            return false;
        }
        return true;
    }
    
    showError(message) {
        const firstRadio = this.radios[0];
        const errorEl = document.createElement('div');
        errorEl.className = 'radio-error';
        errorEl.textContent = message;
        
        firstRadio.parentNode.appendChild(errorEl);
    }
    
    clearValidationErrors() {
        const errors = document.querySelectorAll('.radio-error');
        errors.forEach(error => error.remove());
    }
}

Custom Form Controls

// Custom select dropdown
class CustomSelect {
    constructor(element) {
        this.element = element;
        this.select = element.querySelector('select');
        this.createCustomUI();
    }
    
    createCustomUI() {
        // Hide original select
        this.select.style.display = 'none';
        
        // Create custom UI
        this.customSelect = document.createElement('div');
        this.customSelect.className = 'custom-select';
        
        this.selected = document.createElement('div');
        this.selected.className = 'custom-select-selected';
        this.selected.textContent = this.select.options[this.select.selectedIndex].text;
        
        this.optionsList = document.createElement('div');
        this.optionsList.className = 'custom-select-options';
        
        // Populate options
        Array.from(this.select.options).forEach((option, index) => {
            const customOption = document.createElement('div');
            customOption.className = 'custom-select-option';
            customOption.textContent = option.text;
            customOption.dataset.value = option.value;
            
            customOption.addEventListener('click', () => {
                this.selectOption(index);
            });
            
            this.optionsList.appendChild(customOption);
        });
        
        this.customSelect.appendChild(this.selected);
        this.customSelect.appendChild(this.optionsList);
        this.element.appendChild(this.customSelect);
        
        // Toggle dropdown
        this.selected.addEventListener('click', () => {
            this.customSelect.classList.toggle('open');
        });
        
        // Close on outside click
        document.addEventListener('click', (e) => {
            if (!this.customSelect.contains(e.target)) {
                this.customSelect.classList.remove('open');
            }
        });
    }
    
    selectOption(index) {
        this.select.selectedIndex = index;
        this.selected.textContent = this.select.options[index].text;
        this.customSelect.classList.remove('open');
        
        // Trigger change event
        const event = new Event('change');
        this.select.dispatchEvent(event);
    }
}

// Custom file input
class CustomFileInput {
    constructor(element) {
        this.element = element;
        this.input = element.querySelector('input[type="file"]');
        this.createCustomUI();
    }
    
    createCustomUI() {
        // Hide original input
        this.input.style.display = 'none';
        
        // Create custom button
        this.button = document.createElement('button');
        this.button.type = 'button';
        this.button.className = 'custom-file-button';
        this.button.textContent = 'Choose File';
        
        // Create file name display
        this.fileNameDisplay = document.createElement('span');
        this.fileNameDisplay.className = 'file-name';
        this.fileNameDisplay.textContent = 'No file chosen';
        
        // Create preview container
        this.previewContainer = document.createElement('div');
        this.previewContainer.className = 'file-preview-container';
        
        this.element.appendChild(this.button);
        this.element.appendChild(this.fileNameDisplay);
        this.element.appendChild(this.previewContainer);
        
        // Handle button click
        this.button.addEventListener('click', () => {
            this.input.click();
        });
        
        // Handle file selection
        this.input.addEventListener('change', (e) => {
            this.handleFileSelect(e);
        });
    }
    
    handleFileSelect(e) {
        const files = e.target.files;
        
        if (files.length > 0) {
            this.fileNameDisplay.textContent = 
                files.length === 1 ? files[0].name : `${files.length} files selected`;
            
            // Clear previous previews
            this.previewContainer.innerHTML = '';
            
            // Create previews
            Array.from(files).forEach(file => {
                if (file.type.startsWith('image/')) {
                    const reader = new FileReader();
                    reader.onload = (e) => {
                        const img = document.createElement('img');
                        img.src = e.target.result;
                        img.className = 'file-preview';
                        this.previewContainer.appendChild(img);
                    };
                    reader.readAsDataURL(file);
                }
            });
        } else {
            this.fileNameDisplay.textContent = 'No file chosen';
            this.previewContainer.innerHTML = '';
        }
    }
}

Advanced Form Patterns

Multi-Step Forms

class MultiStepForm {
    constructor(formElement) {
        this.form = formElement;
        this.steps = Array.from(formElement.querySelectorAll('.form-step'));
        this.currentStep = 0;
        this.progressBar = formElement.querySelector('.progress-bar');
        this.stepIndicators = formElement.querySelectorAll('.step-indicator');
        
        this.initialize();
    }
    
    initialize() {
        // Hide all steps except first
        this.steps.forEach((step, index) => {
            step.style.display = index === 0 ? 'block' : 'none';
        });
        
        // Add navigation buttons
        this.steps.forEach((step, index) => {
            if (index > 0) {
                const prevButton = document.createElement('button');
                prevButton.type = 'button';
                prevButton.textContent = 'Previous';
                prevButton.className = 'prev-button';
                prevButton.addEventListener('click', () => this.prevStep());
                step.appendChild(prevButton);
            }
            
            if (index < this.steps.length - 1) {
                const nextButton = document.createElement('button');
                nextButton.type = 'button';
                nextButton.textContent = 'Next';
                nextButton.className = 'next-button';
                nextButton.addEventListener('click', () => this.nextStep());
                step.appendChild(nextButton);
            } else {
                const submitButton = document.createElement('button');
                submitButton.type = 'submit';
                submitButton.textContent = 'Submit';
                submitButton.className = 'submit-button';
                step.appendChild(submitButton);
            }
        });
        
        // Form submission
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            this.submitForm();
        });
        
        this.updateProgress();
    }
    
    nextStep() {
        if (this.validateCurrentStep()) {
            if (this.currentStep < this.steps.length - 1) {
                this.steps[this.currentStep].style.display = 'none';
                this.currentStep++;
                this.steps[this.currentStep].style.display = 'block';
                this.updateProgress();
                
                // Focus first input in new step
                const firstInput = this.steps[this.currentStep].querySelector('input, select, textarea');
                if (firstInput) firstInput.focus();
            }
        }
    }
    
    prevStep() {
        if (this.currentStep > 0) {
            this.steps[this.currentStep].style.display = 'none';
            this.currentStep--;
            this.steps[this.currentStep].style.display = 'block';
            this.updateProgress();
        }
    }
    
    validateCurrentStep() {
        const currentStepElement = this.steps[this.currentStep];
        const inputs = currentStepElement.querySelectorAll('input, select, textarea');
        let isValid = true;
        
        inputs.forEach(input => {
            if (!input.checkValidity()) {
                input.reportValidity();
                isValid = false;
            }
        });
        
        return isValid;
    }
    
    updateProgress() {
        const progress = ((this.currentStep + 1) / this.steps.length) * 100;
        
        if (this.progressBar) {
            this.progressBar.style.width = `${progress}%`;
        }
        
        this.stepIndicators.forEach((indicator, index) => {
            indicator.classList.toggle('active', index === this.currentStep);
            indicator.classList.toggle('completed', index < this.currentStep);
        });
    }
    
    async submitForm() {
        const formData = new FormData(this.form);
        
        try {
            const response = await fetch(this.form.action, {
                method: 'POST',
                body: formData
            });
            
            if (response.ok) {
                this.showSuccess();
            } else {
                this.showError();
            }
        } catch (error) {
            this.showError();
        }
    }
    
    showSuccess() {
        // Show success message
        const successMessage = document.createElement('div');
        successMessage.className = 'success-message';
        successMessage.textContent = 'Form submitted successfully!';
        this.form.appendChild(successMessage);
        
        // Hide form steps
        this.steps.forEach(step => step.style.display = 'none');
    }
    
    showError() {
        // Show error message
        const errorMessage = document.createElement('div');
        errorMessage.className = 'error-message';
        errorMessage.textContent = 'An error occurred. Please try again.';
        this.form.insertBefore(errorMessage, this.form.firstChild);
    }
}

Auto-Save Forms

class AutoSaveForm {
    constructor(form, options = {}) {
        this.form = form;
        this.options = {
            saveInterval: 30000, // 30 seconds
            storageKey: 'form-autosave',
            debounceDelay: 1000,
            ...options
        };
        
        this.initialize();
    }
    
    initialize() {
        // Load saved data
        this.loadSavedData();
        
        // Setup auto-save
        this.setupAutoSave();
        
        // Clear saved data on successful submission
        this.form.addEventListener('submit', () => {
            this.clearSavedData();
        });
    }
    
    setupAutoSave() {
        let saveTimeout;
        
        // Save on input changes (debounced)
        this.form.addEventListener('input', () => {
            clearTimeout(saveTimeout);
            saveTimeout = setTimeout(() => {
                this.saveFormData();
            }, this.options.debounceDelay);
        });
        
        // Periodic save
        setInterval(() => {
            this.saveFormData();
        }, this.options.saveInterval);
        
        // Save before page unload
        window.addEventListener('beforeunload', () => {
            this.saveFormData();
        });
    }
    
    saveFormData() {
        const formData = new FormData(this.form);
        const data = {};
        
        for (let [key, value] of formData.entries()) {
            data[key] = value;
        }
        
        localStorage.setItem(this.options.storageKey, JSON.stringify(data));
        this.showSaveIndicator();
    }
    
    loadSavedData() {
        const savedData = localStorage.getItem(this.options.storageKey);
        
        if (savedData) {
            const data = JSON.parse(savedData);
            
            Object.entries(data).forEach(([key, value]) => {
                const field = this.form.elements[key];
                if (field) {
                    if (field.type === 'checkbox' || field.type === 'radio') {
                        field.checked = field.value === value;
                    } else {
                        field.value = value;
                    }
                }
            });
            
            this.showRestoreMessage();
        }
    }
    
    clearSavedData() {
        localStorage.removeItem(this.options.storageKey);
    }
    
    showSaveIndicator() {
        let indicator = this.form.querySelector('.save-indicator');
        
        if (!indicator) {
            indicator = document.createElement('div');
            indicator.className = 'save-indicator';
            this.form.appendChild(indicator);
        }
        
        indicator.textContent = 'Saved';
        indicator.style.opacity = '1';
        
        setTimeout(() => {
            indicator.style.opacity = '0';
        }, 2000);
    }
    
    showRestoreMessage() {
        const message = document.createElement('div');
        message.className = 'restore-message';
        message.innerHTML = `
            Restored previously saved data. 
            
        `;
        
        this.form.insertBefore(message, this.form.firstChild);
        
        message.querySelector('.clear-saved').addEventListener('click', () => {
            this.clearSavedData();
            this.form.reset();
            message.remove();
        });
    }
}

Form Accessibility

Creating accessible forms ensures all users can interact with your application effectively.

// Accessible form patterns
class AccessibleForm {
    constructor(form) {
        this.form = form;
        this.setupAccessibility();
    }
    
    setupAccessibility() {
        // Ensure all inputs have labels
        this.ensureLabels();
        
        // Add ARIA attributes
        this.addAriaAttributes();
        
        // Setup keyboard navigation
        this.setupKeyboardNav();
        
        // Error announcements
        this.setupErrorAnnouncements();
    }
    
    ensureLabels() {
        const inputs = this.form.querySelectorAll('input, select, textarea');
        
        inputs.forEach(input => {
            if (!input.id) {
                input.id = `field-${Math.random().toString(36).substr(2, 9)}`;
            }
            
            let label = this.form.querySelector(`label[for="${input.id}"]`);
            
            if (!label && input.parentElement.tagName === 'LABEL') {
                // Input is wrapped in label
                return;
            }
            
            if (!label) {
                // Create label from placeholder or name
                label = document.createElement('label');
                label.htmlFor = input.id;
                label.textContent = input.placeholder || input.name;
                input.parentNode.insertBefore(label, input);
            }
        });
    }
    
    addAriaAttributes() {
        // Required fields
        const requiredFields = this.form.querySelectorAll('[required]');
        requiredFields.forEach(field => {
            field.setAttribute('aria-required', 'true');
        });
        
        // Field descriptions
        const descriptions = this.form.querySelectorAll('.field-description');
        descriptions.forEach(desc => {
            const field = desc.previousElementSibling;
            if (field && field.tagName === 'INPUT') {
                const id = `desc-${Math.random().toString(36).substr(2, 9)}`;
                desc.id = id;
                field.setAttribute('aria-describedby', id);
            }
        });
    }
    
    setupKeyboardNav() {
        this.form.addEventListener('keydown', (e) => {
            // Enter key submits form (except in textarea)
            if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') {
                e.preventDefault();
                
                const submitButton = this.form.querySelector('[type="submit"]');
                if (submitButton) {
                    submitButton.click();
                }
            }
        });
    }
    
    setupErrorAnnouncements() {
        // Create live region for error announcements
        const liveRegion = document.createElement('div');
        liveRegion.setAttribute('aria-live', 'polite');
        liveRegion.setAttribute('aria-atomic', 'true');
        liveRegion.className = 'sr-only'; // Visually hidden
        this.form.appendChild(liveRegion);
        
        // Announce errors
        this.form.addEventListener('invalid', (e) => {
            e.preventDefault();
            
            const field = e.target;
            const errorMessage = field.validationMessage;
            
            liveRegion.textContent = `${field.name}: ${errorMessage}`;
            
            // Focus first invalid field
            if (!this.form.querySelector('.invalid')) {
                field.focus();
            }
            
            field.classList.add('invalid');
        }, true);
    }
}

// Screen reader friendly form messages
class FormMessages {
    constructor(form) {
        this.form = form;
        this.messageContainer = this.createMessageContainer();
    }
    
    createMessageContainer() {
        const container = document.createElement('div');
        container.className = 'form-messages';
        container.setAttribute('role', 'status');
        container.setAttribute('aria-live', 'polite');
        
        this.form.insertBefore(container, this.form.firstChild);
        return container;
    }
    
    showMessage(message, type = 'info') {
        const messageElement = document.createElement('div');
        messageElement.className = `message ${type}`;
        messageElement.textContent = message;
        
        // Add appropriate role
        if (type === 'error') {
            messageElement.setAttribute('role', 'alert');
        }
        
        this.messageContainer.appendChild(messageElement);
        
        // Auto-remove after delay
        setTimeout(() => {
            messageElement.remove();
        }, 5000);
    }
    
    clearMessages() {
        this.messageContainer.innerHTML = '';
    }
}

Practice Exercises

  1. Create a registration form with real-time validation
  2. Build a multi-step order form with progress indicator
  3. Implement an auto-complete search input
  4. Create a file upload form with drag-and-drop support

Exercise Solutions (Try First!)

Click to see solutions
// 1. Registration Form with Real-time Validation
class RegistrationForm {
    constructor(formId) {
        this.form = document.getElementById(formId);
        this.validators = {
            username: [
                { test: value => value.length >= 3, message: 'Username must be at least 3 characters' },
                { test: value => /^[a-zA-Z0-9_]+$/.test(value), message: 'Username can only contain letters, numbers, and underscores' }
            ],
            email: [
                { test: value => value.includes('@'), message: 'Please enter a valid email' },
                { test: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value), message: 'Please enter a valid email address' }
            ],
            password: [
                { test: value => value.length >= 8, message: 'Password must be at least 8 characters' },
                { test: value => /[A-Z]/.test(value), message: 'Password must contain an uppercase letter' },
                { test: value => /[0-9]/.test(value), message: 'Password must contain a number' }
            ],
            confirmPassword: [
                { 
                    test: value => value === this.form.elements.password.value, 
                    message: 'Passwords do not match' 
                }
            ]
        };
        
        this.setupValidation();
    }
    
    setupValidation() {
        Object.keys(this.validators).forEach(fieldName => {
            const field = this.form.elements[fieldName];
            
            field.addEventListener('input', () => this.validateField(fieldName));
            field.addEventListener('blur', () => this.validateField(fieldName));
        });
        
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            if (this.validateForm()) {
                this.submitForm();
            }
        });
    }
    
    validateField(fieldName) {
        const field = this.form.elements[fieldName];
        const validators = this.validators[fieldName];
        let isValid = true;
        
        for (const validator of validators) {
            if (!validator.test(field.value)) {
                this.showError(field, validator.message);
                isValid = false;
                break;
            }
        }
        
        if (isValid) {
            this.clearError(field);
        }
        
        return isValid;
    }
    
    validateForm() {
        let isValid = true;
        
        Object.keys(this.validators).forEach(fieldName => {
            if (!this.validateField(fieldName)) {
                isValid = false;
            }
        });
        
        return isValid;
    }
    
    showError(field, message) {
        const errorId = `${field.name}-error`;
        let errorElement = document.getElementById(errorId);
        
        if (!errorElement) {
            errorElement = document.createElement('div');
            errorElement.id = errorId;
            errorElement.className = 'error-message';
            field.parentNode.insertBefore(errorElement, field.nextSibling);
        }
        
        errorElement.textContent = message;
        field.classList.add('invalid');
        field.setAttribute('aria-invalid', 'true');
        field.setAttribute('aria-describedby', errorId);
    }
    
    clearError(field) {
        const errorElement = document.getElementById(`${field.name}-error`);
        if (errorElement) {
            errorElement.remove();
        }
        
        field.classList.remove('invalid');
        field.removeAttribute('aria-invalid');
        field.removeAttribute('aria-describedby');
    }
    
    async submitForm() {
        const formData = new FormData(this.form);
        
        try {
            const response = await fetch('/api/register', {
                method: 'POST',
                body: formData
            });
            
            if (response.ok) {
                alert('Registration successful!');
                this.form.reset();
            } else {
                alert('Registration failed. Please try again.');
            }
        } catch (error) {
            console.error('Error:', error);
            alert('An error occurred. Please try again.');
        }
    }
}

// 2. Multi-step Order Form
class OrderForm {
    constructor(formId) {
        this.form = document.getElementById(formId);
        this.steps = [];
        this.currentStep = 0;
        
        this.initializeSteps();
        this.createProgressIndicator();
        this.setupNavigation();
    }
    
    initializeSteps() {
        const stepElements = this.form.querySelectorAll('.form-step');
        
        stepElements.forEach((element, index) => {
            this.steps.push({
                element: element,
                title: element.dataset.title,
                isValid: false
            });
            
            if (index !== 0) {
                element.style.display = 'none';
            }
        });
    }
    
    createProgressIndicator() {
        const progressBar = document.createElement('div');
        progressBar.className = 'progress-bar';
        
        this.steps.forEach((step, index) => {
            const indicator = document.createElement('div');
            indicator.className = 'step-indicator';
            indicator.textContent = index + 1;
            indicator.title = step.title;
            
            if (index === 0) {
                indicator.classList.add('active');
            }
            
            progressBar.appendChild(indicator);
        });
        
        this.form.insertBefore(progressBar, this.form.firstChild);
        this.progressIndicators = progressBar.querySelectorAll('.step-indicator');
    }
    
    setupNavigation() {
        const navigation = document.createElement('div');
        navigation.className = 'form-navigation';
        
        this.prevButton = document.createElement('button');
        this.prevButton.type = 'button';
        this.prevButton.textContent = 'Previous';
        this.prevButton.addEventListener('click', () => this.previousStep());
        
        this.nextButton = document.createElement('button');
        this.nextButton.type = 'button';
        this.nextButton.textContent = 'Next';
        this.nextButton.addEventListener('click', () => this.nextStep());
        
        this.submitButton = document.createElement('button');
        this.submitButton.type = 'submit';
        this.submitButton.textContent = 'Submit Order';
        this.submitButton.style.display = 'none';
        
        navigation.appendChild(this.prevButton);
        navigation.appendChild(this.nextButton);
        navigation.appendChild(this.submitButton);
        
        this.form.appendChild(navigation);
        this.updateNavigation();
        
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            this.submitOrder();
        });
    }
    
    updateNavigation() {
        this.prevButton.disabled = this.currentStep === 0;
        
        if (this.currentStep === this.steps.length - 1) {
            this.nextButton.style.display = 'none';
            this.submitButton.style.display = 'inline-block';
        } else {
            this.nextButton.style.display = 'inline-block';
            this.submitButton.style.display = 'none';
        }
    }
    
    updateProgressIndicator() {
        this.progressIndicators.forEach((indicator, index) => {
            indicator.classList.remove('active', 'completed');
            
            if (index === this.currentStep) {
                indicator.classList.add('active');
            } else if (index < this.currentStep) {
                indicator.classList.add('completed');
            }
        });
    }
    
    validateCurrentStep() {
        const currentStepElement = this.steps[this.currentStep].element;
        const inputs = currentStepElement.querySelectorAll('input, select, textarea');
        let isValid = true;
        
        inputs.forEach(input => {
            if (!input.checkValidity()) {
                input.reportValidity();
                isValid = false;
            }
        });
        
        this.steps[this.currentStep].isValid = isValid;
        return isValid;
    }
    
    nextStep() {
        if (this.validateCurrentStep() && this.currentStep < this.steps.length - 1) {
            this.steps[this.currentStep].element.style.display = 'none';
            this.currentStep++;
            this.steps[this.currentStep].element.style.display = 'block';
            
            this.updateNavigation();
            this.updateProgressIndicator();
            
            // Focus first input in new step
            const firstInput = this.steps[this.currentStep].element.querySelector('input, select, textarea');
            if (firstInput) {
                firstInput.focus();
            }
        }
    }
    
    previousStep() {
        if (this.currentStep > 0) {
            this.steps[this.currentStep].element.style.display = 'none';
            this.currentStep--;
            this.steps[this.currentStep].element.style.display = 'block';
            
            this.updateNavigation();
            this.updateProgressIndicator();
        }
    }
    
    async submitOrder() {
        if (!this.validateCurrentStep()) {
            return;
        }
        
        const formData = new FormData(this.form);
        
        try {
            const response = await fetch('/api/orders', {
                method: 'POST',
                body: formData
            });
            
            if (response.ok) {
                const order = await response.json();
                alert(`Order placed successfully! Order ID: ${order.id}`);
                this.form.reset();
                
                // Reset to first step
                this.steps[this.currentStep].element.style.display = 'none';
                this.currentStep = 0;
                this.steps[0].element.style.display = 'block';
                this.updateNavigation();
                this.updateProgressIndicator();
            } else {
                alert('Failed to place order. Please try again.');
            }
        } catch (error) {
            console.error('Error:', error);
            alert('An error occurred. Please try again.');
        }
    }
}

// 3. Auto-complete Search Input
class AutocompleteSearch {
    constructor(inputId, options = {}) {
        this.input = document.getElementById(inputId);
        this.options = {
            minChars: 2,
            delay: 300,
            maxResults: 10,
            searchEndpoint: '/api/search',
            ...options
        };
        
        this.resultsContainer = this.createResultsContainer();
        this.selectedIndex = -1;
        this.results = [];
        this.setupEventListeners();
    }
    
    createResultsContainer() {
        const container = document.createElement('div');
        container.className = 'autocomplete-results';
        container.style.display = 'none';
        
        this.input.parentNode.style.position = 'relative';
        this.input.parentNode.appendChild(container);
        
        return container;
    }
    
    setupEventListeners() {
        let debounceTimer;
        
        this.input.addEventListener('input', (e) => {
            clearTimeout(debounceTimer);
            const query = e.target.value;
            
            if (query.length < this.options.minChars) {
                this.hideResults();
                return;
            }
            
            debounceTimer = setTimeout(() => {
                this.searchQuery(query);
            }, this.options.delay);
        });
        
        this.input.addEventListener('keydown', (e) => {
            switch (e.key) {
                case 'ArrowDown':
                    e.preventDefault();
                    this.selectNext();
                    break;
                case 'ArrowUp':
                    e.preventDefault();
                    this.selectPrevious();
                    break;
                case 'Enter':
                    if (this.selectedIndex >= 0) {
                        e.preventDefault();
                        this.selectResult(this.results[this.selectedIndex]);
                    }
                    break;
                case 'Escape':
                    this.hideResults();
                    break;
            }
        });
        
        this.input.addEventListener('blur', (e) => {
            // Delay to allow click events on results
            setTimeout(() => {
                this.hideResults();
            }, 200);
        });
        
        document.addEventListener('click', (e) => {
            if (!this.input.parentNode.contains(e.target)) {
                this.hideResults();
            }
        });
    }
    
    async searchQuery(query) {
        try {
            const response = await fetch(`${this.options.searchEndpoint}?q=${encodeURIComponent(query)}`);
            const data = await response.json();
            
            this.results = data.results.slice(0, this.options.maxResults);
            this.displayResults(this.results);
        } catch (error) {
            console.error('Search error:', error);
        }
    }
    
    displayResults(results) {
        if (results.length === 0) {
            this.hideResults();
            return;
        }
        
        this.resultsContainer.innerHTML = '';
        this.selectedIndex = -1;
        
        results.forEach((result, index) => {
            const item = document.createElement('div');
            item.className = 'autocomplete-item';
            item.textContent = result.text;
            item.dataset.value = result.value;
            
            item.addEventListener('click', () => {
                this.selectResult(result);
            });
            
            item.addEventListener('mouseover', () => {
                this.highlightItem(index);
            });
            
            this.resultsContainer.appendChild(item);
        });
        
        this.resultsContainer.style.display = 'block';
    }
    
    highlightItem(index) {
        const items = this.resultsContainer.children;
        
        Array.from(items).forEach((item, i) => {
            item.classList.toggle('highlighted', i === index);
        });
        
        this.selectedIndex = index;
    }
    
    selectNext() {
        const maxIndex = this.results.length - 1;
        const nextIndex = this.selectedIndex < maxIndex ? this.selectedIndex + 1 : 0;
        this.highlightItem(nextIndex);
    }
    
    selectPrevious() {
        const maxIndex = this.results.length - 1;
        const prevIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : maxIndex;
        this.highlightItem(prevIndex);
    }
    
    selectResult(result) {
        this.input.value = result.text;
        this.input.dataset.value = result.value;
        
        // Trigger change event
        const event = new Event('change', { bubbles: true });
        this.input.dispatchEvent(event);
        
        this.hideResults();
    }
    
    hideResults() {
        this.resultsContainer.style.display = 'none';
        this.selectedIndex = -1;
        this.results = [];
    }
}

// 4. File Upload with Drag and Drop
class DragDropUploader {
    constructor(containerId, options = {}) {
        this.container = document.getElementById(containerId);
        this.options = {
            maxFiles: 5,
            maxSize: 5 * 1024 * 1024, // 5MB
            acceptedTypes: ['image/*', 'application/pdf'],
            uploadEndpoint: '/api/upload',
            ...options
        };
        
        this.files = new Map();
        this.setupUI();
        this.setupEventListeners();
    }
    
    setupUI() {
        this.container.innerHTML = `
            

Drag and drop files here or click to select

`; this.dropZone = this.container.querySelector('.drop-zone'); this.fileInput = this.container.querySelector('.file-input'); this.fileList = this.container.querySelector('.file-list'); this.selectButton = this.container.querySelector('.select-files-btn'); this.uploadButton = this.container.querySelector('.upload-all-btn'); this.clearButton = this.container.querySelector('.clear-all-btn'); } setupEventListeners() { // File input change this.fileInput.addEventListener('change', (e) => { this.handleFiles(Array.from(e.target.files)); }); // Select files button this.selectButton.addEventListener('click', () => { this.fileInput.click(); }); // Drag and drop events this.dropZone.addEventListener('dragover', (e) => { e.preventDefault(); this.dropZone.classList.add('drag-over'); }); this.dropZone.addEventListener('dragleave', () => { this.dropZone.classList.remove('drag-over'); }); this.dropZone.addEventListener('drop', (e) => { e.preventDefault(); this.dropZone.classList.remove('drag-over'); const files = Array.from(e.dataTransfer.files); this.handleFiles(files); }); // Upload and clear buttons this.uploadButton.addEventListener('click', () => { this.uploadAll(); }); this.clearButton.addEventListener('click', () => { this.clearAll(); }); } handleFiles(files) { const validFiles = files.filter(file => this.validateFile(file)); validFiles.forEach(file => { if (this.files.size < this.options.maxFiles) { this.addFile(file); } else { alert(`Maximum ${this.options.maxFiles} files allowed`); } }); this.updateButtons(); } validateFile(file) { // Check file size if (file.size > this.options.maxSize) { alert(`File "${file.name}" is too large. Maximum size is ${this.formatFileSize(this.options.maxSize)}`); return false; } // Check file type const isValidType = this.options.acceptedTypes.some(type => { if (type.endsWith('/*')) { return file.type.startsWith(type.slice(0, -2)); } return file.type === type; }); if (!isValidType) { alert(`File type "${file.type}" is not accepted`); return false; } return true; } addFile(file) { const id = Date.now() + Math.random(); this.files.set(id, { file: file, progress: 0, status: 'pending' }); this.renderFileItem(id, file); } renderFileItem(id, file) { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.dataset.id = id; fileItem.innerHTML = `
${file.name} ${this.formatFileSize(file.size)}
0%
`; // Add remove handler fileItem.querySelector('.remove-file-btn').addEventListener('click', () => { this.removeFile(id); }); this.fileList.appendChild(fileItem); // Add preview for images if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (e) => { const preview = document.createElement('img'); preview.className = 'file-preview'; preview.src = e.target.result; fileItem.insertBefore(preview, fileItem.firstChild); }; reader.readAsDataURL(file); } } removeFile(id) { this.files.delete(id); const fileItem = this.fileList.querySelector(`[data-id="${id}"]`); if (fileItem) { fileItem.remove(); } this.updateButtons(); } clearAll() { this.files.clear(); this.fileList.innerHTML = ''; this.updateButtons(); } updateButtons() { const hasFiles = this.files.size > 0; this.uploadButton.disabled = !hasFiles; this.clearButton.disabled = !hasFiles; } async uploadAll() { this.uploadButton.disabled = true; for (const [id, fileData] of this.files) { if (fileData.status === 'pending') { await this.uploadFile(id, fileData); } } this.uploadButton.disabled = false; } async uploadFile(id, fileData) { const formData = new FormData(); formData.append('file', fileData.file); try { const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const progress = Math.round((e.loaded / e.total) * 100); this.updateProgress(id, progress); } }); xhr.addEventListener('load', () => { if (xhr.status === 200) { this.updateFileStatus(id, 'completed'); } else { this.updateFileStatus(id, 'error'); } }); xhr.addEventListener('error', () => { this.updateFileStatus(id, 'error'); }); xhr.open('POST', this.options.uploadEndpoint); xhr.send(formData); this.updateFileStatus(id, 'uploading'); } catch (error) { console.error('Upload error:', error); this.updateFileStatus(id, 'error'); } } updateProgress(id, progress) { const fileItem = this.fileList.querySelector(`[data-id="${id}"]`); if (fileItem) { const progressFill = fileItem.querySelector('.progress-fill'); const progressText = fileItem.querySelector('.progress-text'); progressFill.style.width = `${progress}%`; progressText.textContent = `${progress}%`; } const fileData = this.files.get(id); if (fileData) { fileData.progress = progress; } } updateFileStatus(id, status) { const fileItem = this.fileList.querySelector(`[data-id="${id}"]`); if (fileItem) { fileItem.dataset.status = status; if (status === 'completed') { fileItem.classList.add('upload-complete'); const removeBtn = fileItem.querySelector('.remove-file-btn'); removeBtn.textContent = '✓'; removeBtn.disabled = true; } else if (status === 'error') { fileItem.classList.add('upload-error'); } } const fileData = this.files.get(id); if (fileData) { fileData.status = status; } } formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } }

Summary

Form events and validation are crucial for creating interactive, user-friendly web applications:

Key takeaways:

Master form handling to create professional, user-friendly web applications!