Learning Objectives

  • Use closures in event handlers effectively
  • Understand callback patterns with closures
  • Create reusable event handler factories
  • Manage event handler state with closures

Closures in Event Handlers

Event handlers are one of the most common places where closures shine. They allow handlers to access variables from their creation context.

function createButton(label, onClick) {
    const button = document.createElement('button');
    button.textContent = label;
    
    // Closure: handler has access to label
    button.addEventListener('click', function() {
        console.log(`${label} button clicked`);
        onClick();
    });
    
    return button;
}

const saveButton = createButton('Save', () => {
    console.log('Saving data...');
});

const cancelButton = createButton('Cancel', () => {
    console.log('Cancelling...');
});

document.body.append(saveButton, cancelButton);

Maintaining State in Event Handlers

Use closures to maintain state across multiple event invocations:

function createCounter() {
    let count = 0;
    const button = document.createElement('button');
    button.textContent = 'Click me!';
    
    button.addEventListener('click', function() {
        count++;
        button.textContent = `Clicked ${count} times`;
    });
    
    return button;
}

const counter1 = createCounter();
const counter2 = createCounter();

document.body.append(counter1, counter2);
// Each button maintains its own count!

Event Handler Factory

Create reusable event handler factories:

function createClickHandler(config) {
    let clickCount = 0;
    const { maxClicks = Infinity, onMaxReached, onEachClick } = config;
    
    return function(event) {
        clickCount++;
        
        if (onEachClick) {
            onEachClick(clickCount, event);
        }
        
        if (clickCount >= maxClicks) {
            event.target.disabled = true;
            if (onMaxReached) {
                onMaxReached(clickCount);
            }
        }
    };
}

// Create a button that can only be clicked 3 times
const limitedButton = document.createElement('button');
limitedButton.textContent = 'Limited Clicks';

limitedButton.addEventListener('click', createClickHandler({
    maxClicks: 3,
    onEachClick: (count) => {
        console.log(`Click ${count}`);
    },
    onMaxReached: () => {
        console.log('Max clicks reached!');
        limitedButton.textContent = 'Disabled';
    }
}));

document.body.appendChild(limitedButton);

Practical Example: Form Validation

function createValidator(rules) {
    const errors = {};
    
    return function(fieldName, value) {
        const fieldRules = rules[fieldName];
        if (!fieldRules) return true;
        
        errors[fieldName] = [];
        
        for (const rule of fieldRules) {
            if (!rule.test(value)) {
                errors[fieldName].push(rule.message);
            }
        }
        
        return errors[fieldName].length === 0;
    };
}

const validate = createValidator({
    username: [
        {
            test: v => v.length >= 3,
            message: 'Username must be at least 3 characters'
        },
        {
            test: v => /^[a-zA-Z0-9_]+$/.test(v),
            message: 'Username can only contain letters, numbers, and underscores'
        }
    ],
    email: [
        {
            test: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
            message: 'Invalid email format'
        }
    ]
});

// Use in event handlers
document.getElementById('username').addEventListener('blur', function(e) {
    const isValid = validate('username', e.target.value);
    if (!isValid) {
        console.log('Validation errors:', validate.errors);
    }
});

Practical Example: Debouncing

Use closures to implement debouncing for search inputs:

function debounce(fn, delay) {
    let timeoutId;
    
    return function(...args) {
        clearTimeout(timeoutId);
        
        timeoutId = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

// Search handler
function searchAPI(query) {
    console.log('Searching for:', query);
    // Make API call...
}

const debouncedSearch = debounce(searchAPI, 300);

document.getElementById('search').addEventListener('input', function(e) {
    debouncedSearch(e.target.value);
    // Only calls searchAPI after user stops typing for 300ms
});

Practical Example: Throttling

Implement throttling for scroll events:

function throttle(fn, limit) {
    let inThrottle;
    
    return function(...args) {
        if (!inThrottle) {
            fn.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

function handleScroll() {
    console.log('Scroll position:', window.scrollY);
    // Update UI based on scroll...
}

const throttledScroll = throttle(handleScroll, 100);

window.addEventListener('scroll', throttledScroll);
// Only calls handleScroll once every 100ms

Practical Example: Event Delegation

function createDelegatedHandler(selector, handler) {
    return function(event) {
        const target = event.target.closest(selector);
        if (target) {
            handler.call(target, event);
        }
    };
}

// Handle clicks on any button inside the container
document.getElementById('container').addEventListener('click',
    createDelegatedHandler('button', function(event) {
        console.log('Button clicked:', this.textContent);
        // 'this' refers to the matched button
    })
);

Practical Example: Once Handler

Create an event handler that only fires once:

function once(fn) {
    let called = false;
    let result;
    
    return function(...args) {
        if (!called) {
            called = true;
            result = fn.apply(this, args);
        }
        return result;
    };
}

const button = document.getElementById('submit');
button.addEventListener('click', once(function() {
    console.log('Form submitted!');
    // This will only log once, no matter how many times clicked
}));

Practical Example: Event Emitter

function createEventEmitter() {
    const events = {};
    
    return {
        on(event, handler) {
            if (!events[event]) {
                events[event] = [];
            }
            events[event].push(handler);
            
            // Return unsubscribe function
            return () => {
                const index = events[event].indexOf(handler);
                if (index > -1) {
                    events[event].splice(index, 1);
                }
            };
        },
        
        emit(event, data) {
            if (!events[event]) return;
            events[event].forEach(handler => handler(data));
        },
        
        once(event, handler) {
            const unsubscribe = this.on(event, (data) => {
                handler(data);
                unsubscribe();
            });
            return unsubscribe;
        }
    };
}

const emitter = createEventEmitter();

// Subscribe to events
const unsubscribe = emitter.on('data', (data) => {
    console.log('Received:', data);
});

emitter.emit('data', { value: 42 });
unsubscribe();
emitter.emit('data', { value: 100 }); // Won't log

Practical Example: Click Counter with Reset

function createClickCounter(element) {
    let clicks = 0;
    let lastClickTime = null;
    
    element.addEventListener('click', function() {
        clicks++;
        lastClickTime = Date.now();
        updateDisplay();
    });
    
    function updateDisplay() {
        element.textContent = `Clicks: ${clicks}`;
    }
    
    function reset() {
        clicks = 0;
        lastClickTime = null;
        updateDisplay();
    }
    
    function getStats() {
        return {
            clicks,
            lastClickTime,
            averageClickRate: lastClickTime ? 
                clicks / ((Date.now() - lastClickTime) / 1000) : 0
        };
    }
    
    updateDisplay();
    
    return {
        reset,
        getStats
    };
}

const button = document.getElementById('myButton');
const counter = createClickCounter(button);

// Add reset button
const resetBtn = document.getElementById('reset');
resetBtn.addEventListener('click', () => counter.reset());

Key Takeaways

  • ✅ Event handlers naturally create closures
  • ✅ Closures allow handlers to maintain private state
  • ✅ Use closures for debouncing and throttling
  • ✅ Create reusable handler factories with closures
  • ✅ Each handler instance has its own independent state

Next Steps

Now that you understand closures with event handlers, in the next lesson we'll explore how closures work with setTimeout and setInterval for managing timed operations.