Labs ICT
Pro Login

DOM Events

DOM events are the foundation of interactive web applications, allowing JavaScript to respond to user interactions, browser events, and custom application events. Understanding event handling is crucial for creating dynamic, responsive user interfaces and building modern web applications.

In this tutorial, we'll explore event types, event handling patterns, event delegation, modern event APIs, and best practices for working with DOM events in JavaScript.

Understanding Events

Events are signals that something has occurred in the DOM, such as user interactions or browser state changes.

Event Basics

// Event object properties
function handleEvent(event) {
  console.log('Event type:', event.type);
  console.log('Target element:', event.target);
  console.log('Current target:', event.currentTarget);
  console.log('Timestamp:', event.timeStamp);
  console.log('Bubbles:', event.bubbles);
  console.log('Cancelable:', event.cancelable);
}

// Event phases
// 1. Capturing phase - Event travels down from window to target
// 2. Target phase - Event reaches the target element
// 3. Bubbling phase - Event travels up from target to window

button.addEventListener('click', function(event) {
  console.log('Event phase:', event.eventPhase);
  // 1: CAPTURING_PHASE
  // 2: AT_TARGET
  // 3: BUBBLING_PHASE
}, true); // Use capture phase

// Event flow example
document.getElementById('parent').addEventListener('click', function(event) { console.log('Parent clicked (bubbling)'); }, false); // Bubble phase document.getElementById('parent').addEventListener('click', function(event) { console.log('Parent clicked (capturing)'); }, true); // Capture phase document.getElementById('child').addEventListener('click', function(event) { console.log('Child clicked'); });

Event Object

// Event object properties
button.addEventListener('click', function(event) {
  // Basic properties
  console.log('Type:', event.type); // 'click'
  console.log('Target:', event.target); // Element that was clicked
  console.log('Current target:', event.currentTarget); // Element with listener
  console.log('Time:', event.timeStamp); // When event occurred
  console.log('View:', event.view); // Window object
  
  // Position properties (mouse events)
  console.log('Client X:', event.clientX); // Viewport X
  console.log('Client Y:', event.clientY); // Viewport Y
  console.log('Screen X:', event.screenX); // Screen X
  console.log('Screen Y:', event.screenY); // Screen Y
  
  // Keyboard properties (keyboard events)
  console.log('Key:', event.key); // 'a', 'Enter', etc.
  console.log('Code:', event.keyCode); // Key code (deprecated)
  console.log('Which:', event.which); // Key code (deprecated)
  console.log('Alt:', event.altKey); // Alt key pressed
  console.log('Ctrl:', event.ctrlKey); // Ctrl key pressed
  console.log('Shift:', event.shiftKey); // Shift key pressed
  console.log('Meta:', event.metaKey); // Meta/Cmd key pressed
});

// Event methods
button.addEventListener('click', function(event) {
  // Prevent default behavior
  event.preventDefault();
  
  // Stop event propagation
  event.stopPropagation();
  
  // Stop immediate propagation
  event.stopImmediatePropagation();
  
  // Check if default was prevented
  console.log('Default prevented:', event.defaultPrevented);
});

Event Listeners

Methods for adding and removing event listeners.

addEventListener

// Basic event listener
const button = document.getElementById('myButton');

function handleClick(event) {
  console.log('Button clicked!');
  console.log('Event:', event);
}

button.addEventListener('click', handleClick);

// Event listener with options
button.addEventListener('click', handleClick, {
  once: true, // Remove after first trigger
  passive: true, // Improve performance for scroll/touch
  capture: false, // Use bubble phase (default)
  signal: abortSignal // Abort signal for cancellation
});

// Multiple events on same element
button.addEventListener('click', handleClick);
button.addEventListener('mouseover', handleMouseOver);
button.addEventListener('mouseout', handleMouseOut);

// Event listener with arrow function
button.addEventListener('click', (event) => {
  console.log('Arrow function:', this); // 'this' is button
});

// Event listener with regular function
button.addEventListener('click', function(event) {
  console.log('Regular function:', this); // 'this' is button
});

// Event listener with object method
const handler = {
  element: null,
  
  init(element) {
    this.element = element;
    this.element.addEventListener('click', this.handleClick);
  },
  
  handleClick(event) {
    console.log('Method:', this.element); // 'this' is handler object
  },
  
  destroy() {
    if (this.element) {
      this.element.removeEventListener('click', this.handleClick);
    }
  }
};

handler.init(button);

removeEventListener

// Remove specific listener
function handleClick(event) {
  console.log('Handling click');
}

button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);

// Remove anonymous function (need reference)
const handler = function(event) {
  console.log('Anonymous handler');
};

button.addEventListener('click', handler);
button.removeEventListener('click', handler);

// Remove all listeners of type
function removeAllListeners(element, eventType) {
  // Clone element to remove all listeners
  const clone = element.cloneNode(true);
  element.parentNode.replaceChild(clone, element);
}

// Remove listener with capture option
button.addEventListener('click', handler, true); // Capture phase
button.removeEventListener('click', handler, true); // Must match options

// Safe removal
function safeRemoveListener(element, eventType, handler) {
  try {
    element.removeEventListener(eventType, handler);
  } catch (error) {
    console.error('Error removing listener:', error);
  }
}

Mouse Events

Events triggered by mouse interactions.

Mouse Event Types

// Click events
element.addEventListener('click', function(event) {
  console.log('Click at:', event.clientX, event.clientY);
});

// Double click
element.addEventListener('dblclick', function(event) {
  console.log('Double click');
});

// Mouse down/up
element.addEventListener('mousedown', function(event) {
  console.log('Mouse down:', event.button); // 0=left, 1=middle, 2=right
});

element.addEventListener('mouseup', function(event) {
  console.log('Mouse up:', event.button);
});

// Mouse move
element.addEventListener('mousemove', function(event) {
  console.log('Mouse moved to:', event.clientX, event.clientY);
  
  // Throttle mouse move events for performance
});

// Mouse enter/leave
element.addEventListener('mouseenter', function(event) {
  console.log('Mouse entered element');
});

element.addEventListener('mouseleave', function(event) {
  console.log('Mouse left element');
});

// Mouse over/out (bubbles)
element.addEventListener('mouseover', function(event) {
  console.log('Mouse over element');
});

element.addEventListener('mouseout', function(event) {
  console.log('Mouse out of element');
});

// Context menu
element.addEventListener('contextmenu', function(event) {
  event.preventDefault(); // Prevent default context menu
  console.log('Context menu requested');
});

// Wheel event
element.addEventListener('wheel', function(event) {
  console.log('Wheel delta:', event.deltaY);
  
  // Prevent page scroll
  if (event.ctrlKey) {
    event.preventDefault();
  }
});

Mouse Event Properties

// Mouse position properties
element.addEventListener('mousemove', function(event) {
  console.log('Client coordinates:', event.clientX, event.clientY);
  console.log('Screen coordinates:', event.screenX, event.screenY);
  console.log('Page coordinates:', event.pageX, event.pageY);
  
  // Calculate relative position
  const rect = event.target.getBoundingClientRect();
  const relativeX = event.clientX - rect.left;
  const relativeY = event.clientY - rect.top;
  
  console.log('Relative position:', relativeX, relativeY);
});

// Mouse button properties
element.addEventListener('mousedown', function(event) {
  console.log('Button:', event.button); // 0=left, 1=middle, 2=right
  console.log('Buttons:', event.buttons); // Bitmask of pressed buttons
});

// Modifier keys
element.addEventListener('click', function(event) {
  console.log('Alt:', event.altKey);
  console.log('Ctrl:', event.ctrlKey);
  console.log('Shift:', event.shiftKey);
  console.log('Meta:', event.metaKey);
  
  // Check for common combinations
  if (event.ctrlKey && event.key === 's') {
    console.log('Ctrl+S pressed');
    event.preventDefault(); // Prevent save dialog
  }
});

// Related target
element.addEventListener('click', function(event) {
  console.log('Target:', event.target);
  console.log('Related target:', event.relatedTarget);
  
  // For mouseover, relatedTarget is the element being left
  // For mouseout, relatedTarget is the element being entered
});

Keyboard Events

Events triggered by keyboard interactions.

Keyboard Event Types

// Key events
element.addEventListener('keydown', function(event) {
  console.log('Key pressed down:', event.key);
});

element.addEventListener('keyup', function(event) {
  console.log('Key released:', event.key);
});

element.addEventListener('keypress', function(event) {
  console.log('Key pressed (character):', event.key);
  // Note: keypress is deprecated, use keydown/keyup
});

// Input events (for form elements)
input.addEventListener('input', function(event) {
  console.log('Input changed:', event.target.value);
});

input.addEventListener('beforeinput', function(event) {
  console.log('Input about to change:', event.data);
});

input.addEventListener('change', function(event) {
  console.log('Input value changed:', event.target.value);
});

// Composition events (for complex input)
input.addEventListener('compositionstart', function(event) {
  console.log('Composition started');
});

input.addEventListener('compositionupdate', function(event) {
  console.log('Composition updating:', event.data);
});

input.addEventListener('compositionend', function(event) {
  console.log('Composition ended:', event.data);
});

Keyboard Event Properties

// Key identification
element.addEventListener('keydown', function(event) {
  console.log('Key:', event.key); // 'a', 'Enter', 'ArrowUp', etc.
  console.log('Code:', event.code); // 'KeyA', 'Enter', 'ArrowUp', etc.
  console.log('Location:', event.location); // 0=general, 1=left, 2=right, 3=numpad
  console.log('Which:', event.which); // Numeric key code (deprecated)
  console.log('KeyCode:', event.keyCode); // Numeric key code (deprecated)
});

// Modifier keys
element.addEventListener('keydown', function(event) {
  console.log('Alt:', event.altKey);
  console.log('Ctrl:', event.ctrlKey);
  console.log('Shift:', event.shiftKey);
  console.log('Meta:', event.metaKey);
  
  // Check modifier combinations
  if (event.ctrlKey && event.key === 'c') {
    console.log('Copy shortcut');
  }
  
  if (event.altKey && event.key === 'Tab') {
    console.log('Alt+Tab pressed');
  }
});

// Input method
element.addEventListener('keydown', function(event) {
  console.log('Input method:', event.inputMethod);
  // Possible values: 'ime', 'keyboard', 'paste', 'drop', 'handwriting'
});

// Repeat detection
element.addEventListener('keydown', function(event) {
  console.log('Key repeating:', event.repeat);
  
  if (event.repeat) {
    console.log('Key is being held down');
  }
});

Touch Events

Events triggered by touch interactions on mobile devices.

Touch Event Types

// Touch events
element.addEventListener('touchstart', function(event) {
  console.log('Touch started');
});

element.addEventListener('touchmove', function(event) {
  console.log('Touch moving');
});

element.addEventListener('touchend', function(event) {
  console.log('Touch ended');
});

element.addEventListener('touchcancel', function(event) {
  console.log('Touch cancelled');
});

// Pointer events (unified mouse/touch)
element.addEventListener('pointerdown', function(event) {
  console.log('Pointer down:', event.pointerType);
  // pointerType: 'mouse', 'pen', 'touch'
});

element.addEventListener('pointermove', function(event) {
  console.log('Pointer moving');
});

element.addEventListener('pointerup', function(event) {
  console.log('Pointer up');
});

element.addEventListener('pointercancel', function(event) {
  console.log('Pointer cancelled');
});

Touch Event Properties

// Touch points
element.addEventListener('touchstart', function(event) {
  console.log('Number of touches:', event.touches.length);
  
  // Iterate through all touch points
  for (let i = 0; i < event.touches.length; i++) {
    const touch = event.touches[i];
    console.log(`Touch ${i}:`, touch.identifier, touch.clientX, touch.clientY);
  }
});

// Touch lists
element.addEventListener('touchmove', function(event) {
  console.log('Active touches:', event.touches);
  console.log('Changed touches:', event.changedTouches);
  console.log('Target touches:', event.targetTouches);
});

// Touch properties
element.addEventListener('touchstart', function(event) {
  const touch = event.touches[0];
  
  console.log('Touch ID:', touch.identifier);
  console.log('Force:', touch.force || 0); // Pressure (0-1)
  console.log('Radius X:', touch.radiusX || 0);
  console.log('Radius Y:', touch.radiusY || 0);
  console.log('Rotation angle:', touch.rotationAngle || 0);
});

// Gesture detection
class TouchGesture {
  constructor(element) {
    this.element = element;
    this.startX = 0;
    this.startY = 0;
    this.startTime = 0;
    
    this.element.addEventListener('touchstart', this.handleStart.bind(this));
    this.element.addEventListener('touchmove', this.handleMove.bind(this));
    this.element.addEventListener('touchend', this.handleEnd.bind(this));
  }
  
  handleStart(event) {
    const touch = event.touches[0];
    this.startX = touch.clientX;
    this.startY = touch.clientY;
    this.startTime = event.timeStamp;
  }
  
  handleMove(event) {
    const touch = event.touches[0];
    const deltaX = touch.clientX - this.startX;
    const deltaY = touch.clientY - this.startY;
    
    console.log('Swipe delta:', deltaX, deltaY);
  }
  
  handleEnd(event) {
    const touch = event.changedTouches[0];
    const deltaX = touch.clientX - this.startX;
    const deltaY = touch.clientY - this.startY;
    const deltaTime = event.timeStamp - this.startTime;
    
    // Detect swipe
    if (Math.abs(deltaX) > 50 && deltaTime < 500) {
      console.log('Horizontal swipe detected');
      this.element.dispatchEvent(new CustomEvent('swipe', {
        detail: { direction: deltaX > 0 ? 'right' : 'left' }
      }));
    }
  }
}

Form Events

Events specific to form elements and user input.

Form Event Types

// Form events
const form = document.getElementById('myForm');

form.addEventListener('submit', function(event) {
  event.preventDefault(); // Prevent default form submission
  console.log('Form submitted');
  
  // Custom submission logic
  submitForm(this);
});

form.addEventListener('reset', function(event) {
  console.log('Form reset');
});

// Input events
const input = document.getElementById('myInput');

input.addEventListener('input', function(event) {
  console.log('Input value:', event.target.value);
  console.log('Input type:', event.inputType);
});

input.addEventListener('change', function(event) {
  console.log('Input changed:', event.target.value);
});

// Focus events
input.addEventListener('focus', function(event) {
  console.log('Input focused');
  this.classList.add('focused');
});

input.addEventListener('blur', function(event) {
  console.log('Input lost focus');
  this.classList.remove('focused');
});

// Focus events (bubbling)
input.addEventListener('focusin', function(event) {
  console.log('Element about to receive focus');
});

input.addEventListener('focusout', function(event) {
  console.log('Element about to lose focus');
});

// Validation events
input.addEventListener('invalid', function(event) {
  console.log('Input invalid:', event.target.validationMessage);
  
  // Show custom error message
  showError(event.target, event.target.validationMessage);
});

// Select events
const select = document.getElementById('mySelect');

select.addEventListener('change', function(event) {
  console.log('Selected value:', event.target.value);
  console.log('Selected text:', event.target.options[event.target.selectedIndex].text);
});

// Textarea events
const textarea = document.getElementById('myTextarea');

textarea.addEventListener('input', function(event) {
  console.log('Current length:', event.target.value.length);
  console.log('Remaining characters:', event.target.maxLength - event.target.value.length);
});

Form Validation Events

// Custom validation with events
class FormValidator {
  constructor(form) {
    this.form = form;
    this.fields = form.querySelectorAll('input, select, textarea');
    
    this.form.addEventListener('submit', this.handleSubmit.bind(this));
    this.fields.forEach(field => {
      field.addEventListener('input', this.validateField.bind(this));
      field.addEventListener('blur', this.validateField.bind(this));
    });
  }
  
  validateField(event) {
    const field = event.target;
    const isValid = this.checkField(field);
    
    if (isValid) {
      field.classList.remove('invalid');
      field.classList.add('valid');
    } else {
      field.classList.remove('valid');
      field.classList.add('invalid');
    }
  }
  
  checkField(field) {
    const value = field.value.trim();
    const type = field.type;
    const required = field.hasAttribute('required');
    
    // Required field validation
    if (required && !value) {
      field.setCustomValidity('This field is required');
      return false;
    }
    
    // Type-specific validation
    switch (type) {
      case 'email':
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(value)) {
          field.setCustomValidity('Please enter a valid email');
          return false;
        }
        break;
        
      case 'tel':
        const phoneRegex = /^\+?[\d\s-()]{10,}$/;
        if (!phoneRegex.test(value)) {
          field.setCustomValidity('Please enter a valid phone number');
          return false;
        }
        break;
    }
    
    field.setCustomValidity('');
    return true;
  }
  
  handleSubmit(event) {
    event.preventDefault();
    
    // Validate all fields
    let isValid = true;
    this.fields.forEach(field => {
      if (!this.checkField(field)) {
        isValid = false;
      }
    });
    
    if (isValid) {
      console.log('Form is valid, submitting...');
      this.submitForm();
    } else {
      console.log('Form has validation errors');
      this.showErrors();
    }
  }
  
  submitForm() {
    // Custom submission logic
    const formData = new FormData(this.form);
    
    fetch('/api/submit', {
      method: 'POST',
      body: formData
    })
    .then(response => response.json())
    .then(data => {
      console.log('Form submitted successfully:', data);
      this.showSuccess();
    })
    .catch(error => {
      console.error('Form submission error:', error);
      this.showError(error.message);
    });
  }
}

Event Delegation

Event delegation is a powerful pattern for handling events efficiently.

Basic Delegation

// Instead of this:
document.querySelectorAll('.button').forEach(button => {
  button.addEventListener('click', function(event) {
    console.log('Button clicked:', event.target);
  });
});

// Use event delegation:
document.addEventListener('click', function(event) {
  if (event.target.matches('.button')) {
    console.log('Button clicked:', event.target);
  }
});

// More specific delegation
const container = document.getElementById('container');
container.addEventListener('click', function(event) {
  const button = event.target.closest('.button');
  if (button) {
    console.log('Container button clicked:', button);
  }
});

// Multiple event types delegation
document.addEventListener('click', handleDelegatedClick);
document.addEventListener('focus', handleDelegatedFocus);

function handleDelegatedClick(event) {
  if (event.target.matches('.interactive')) {
    console.log('Interactive element clicked');
  }
}

function handleDelegatedFocus(event) {
  if (event.target.matches('input, textarea')) {
    console.log('Input element focused');
  }
}

Advanced Delegation

// Delegation utility class
class EventDelegator {
  constructor(container) {
    this.container = container;
    this.handlers = new Map();
  }
  
  on(selector, eventType, handler) {
    const key = `${eventType}:${selector}`;
    
    if (!this.handlers.has(key)) {
      this.handlers.set(key, []);
    }
    
    this.handlers.get(key).push(handler);
    
    // Only add one listener per combination
    if (this.handlers.get(key).length === 1) {
      this.container.addEventListener(eventType, this.handleEvent.bind(this));
    }
  }
  
  handleEvent(event) {
    // Find matching handlers
    this.handlers.forEach((handlers, key) => {
      const [eventType, selector] = key.split(':');
      
      if (eventType === event.type) {
        handlers.forEach(handler => {
          const target = event.target.closest(selector);
          if (target) {
            handler.call(target, event);
          }
        });
      }
    });
  }
  
  off(selector, eventType, handler) {
    const key = `${eventType}:${selector}`;
    const handlers = this.handlers.get(key) || [];
    
    const index = handlers.indexOf(handler);
    if (index > -1) {
      handlers.splice(index, 1);
    }
    
    if (handlers.length === 0) {
      this.container.removeEventListener(eventType, this.handleEvent.bind(this));
      this.handlers.delete(key);
    }
  }
}

// Usage
const delegator = new EventDelegator(document.body);

delegator.on('.button', 'click', function(event) {
  console.log('Button clicked:', this);
});

delegator.on('.link', 'click', function(event) {
  console.log('Link clicked:', this);
  event.preventDefault();
});

Custom Events

Creating and dispatching custom events for application communication.

Creating Custom Events

// CustomEvent constructor
const customEvent = new CustomEvent('custom-event', {
  detail: { message: 'Hello from custom event', data: { id: 123 } },
  bubbles: true,
  cancelable: true
});

// Dispatch custom event
element.dispatchEvent(customEvent);

// Listen for custom event
element.addEventListener('custom-event', function(event) {
  console.log('Custom event received:', event.detail);
});

// Custom event with different phases
const bubblingEvent = new CustomEvent('bubbling-event', {
  detail: { message: 'Bubbling' },
  bubbles: true
});

const nonBubblingEvent = new CustomEvent('non-bubbling-event', {
  detail: { message: 'Non-bubbling' },
  bubbles: false
});

// Event namespacing for organization
const appEvents = {
  USER_LOGIN: 'app:user-login',
  USER_LOGOUT: 'app:user-logout',
  DATA_LOADED: 'app:data-loaded',
  ERROR_OCCURRED: 'app:error-occurred'
};

// Dispatch namespaced events
function dispatchUserLogin(userData) {
  const event = new CustomEvent(appEvents.USER_LOGIN, {
    detail: userData
  });
  document.dispatchEvent(event);
}

function dispatchError(error) {
  const event = new CustomEvent(appEvents.ERROR_OCCURRED, {
    detail: { error, timestamp: Date.now() }
  });
  document.dispatchEvent(event);
}

EventTarget and Event Buses

// EventTarget for custom event emitters
class EventEmitter {
  constructor() {
    this.listeners = new Map();
  }
  
  on(eventType, listener) {
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, []);
    }
    
    this.listeners.get(eventType).push(listener);
  }
  
  off(eventType, listener) {
    if (!this.listeners.has(eventType)) return;
    
    const listeners = this.listeners.get(eventType);
    const index = listeners.indexOf(listener);
    
    if (index > -1) {
      listeners.splice(index, 1);
    }
  }
  
  emit(eventType, data) {
    if (!this.listeners.has(eventType)) return;
    
    const event = new CustomEvent(eventType, {
      detail: data
    });
    
    this.listeners.get(eventType).forEach(listener => {
      listener(event);
    });
  }
}

// Usage
const emitter = new EventEmitter();

emitter.on('message', function(event) {
  console.log('Message received:', event.detail);
});

emitter.emit('message', { text: 'Hello World' });

// Application event bus
class EventBus {
  constructor() {
    this.events = new Map();
  }
  
  subscribe(eventType, callback) {
    if (!this.events.has(eventType)) {
      this.events.set(eventType, []);
    }
    
    this.events.get(eventType).push(callback);
  }
  
  unsubscribe(eventType, callback) {
    if (!this.events.has(eventType)) return;
    
    const callbacks = this.events.get(eventType);
    const index = callbacks.indexOf(callback);
    
    if (index > -1) {
      callbacks.splice(index, 1);
    }
  }
  
  publish(eventType, data) {
    if (!this.events.has(eventType)) return;
    
    this.events.get(eventType).forEach(callback => {
      callback(data);
    });
  }
}

// Usage
const eventBus = new EventBus();

eventBus.subscribe('user:login', function(userData) {
  console.log('User logged in:', userData);
});

eventBus.publish('user:login', { id: 123, name: 'John' });

Modern Event APIs

Modern browser APIs for advanced event handling.

Passive Event Listeners

// Passive listeners for better scroll performance
// Check for passive event listener support
const supportsPassive = (() => {
  let supports = false;
  try {
    const opts = Object.defineProperty({}, 'passive', {
      get: function() { supports = true; }
    });
    window.addEventListener('test', null, opts);
    window.removeEventListener('test', null, opts);
  } catch (e) {}
  return supports;
})();

// Add passive scroll listener
function addPassiveScrollListener(element, callback) {
  if (supportsPassive) {
    element.addEventListener('scroll', callback, { passive: true });
  } else {
    // Fallback for older browsers
    let ticking = false;
    
    element.addEventListener('scroll', function(event) {
      if (!ticking) {
        requestAnimationFrame(function() {
          callback(event);
          ticking = false;
        });
        ticking = true;
      }
    });
  }
}

// Usage
addPassiveScrollListener(window, function(event) {
  console.log('Scroll position:', window.scrollY);
});

// Passive touch listener
element.addEventListener('touchstart', function(event) {
  console.log('Touch started');
}, { passive: true });

AbortController

// AbortController for canceling operations
const controller = new AbortController();
const signal = controller.signal;

// Add listener with abort signal
element.addEventListener('click', function(event) {
  console.log('Click handler');
}, { signal });

// Abort the operation
function abortOperation() {
  controller.abort();
  console.log('Operation aborted');
}

// Check if operation was aborted
if (signal.aborted) {
  console.log('Operation was aborted');
}

// Use with fetch
function fetchDataWithAbort(url) {
  const controller = new AbortController();
  
  fetch(url, { signal: controller.signal })
    .then(response => response.json())
    .then(data => console.log('Data:', data))
    .catch(error => {
      if (error.name === 'AbortError') {
        console.log('Fetch was aborted');
      } else {
        console.error('Fetch error:', error);
      }
    });
  
  // Abort fetch after timeout
  setTimeout(() => controller.abort(), 5000);
  
  return controller;
}

// Usage
const fetchController = fetchDataWithAbort('/api/data');
// Later: fetchController.abort();

Performance and Best Practices

Optimizing event handling for better performance and user experience.

Performance Optimization

// Throttle high-frequency events
function throttle(func, limit) {
  let inThrottle;
  return function() {
    const args = arguments;
    const context = this;
    
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Debounce rapid events
function debounce(func, delay) {
  let timeoutId;
  return function() {
    const args = arguments;
    const context = this;
    
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
}

// Usage for scroll events
window.addEventListener('scroll', throttle(function(event) {
  console.log('Throttled scroll:', window.scrollY);
}, 100));

// Usage for search input
searchInput.addEventListener('input', debounce(function(event) {
  console.log('Debounced search:', event.target.value);
  performSearch(event.target.value);
}, 300));

// Event listener cleanup
class EventManager {
  constructor() {
    this.listeners = new WeakMap();
  }
  
  addListener(element, event, handler) {
    element.addEventListener(event, handler);
    
    if (!this.listeners.has(element)) {
      this.listeners.set(element, []);
    }
    
    this.listeners.get(element).push({ event, handler });
  }
  
  cleanup(element) {
    const listeners = this.listeners.get(element) || [];
    
    listeners.forEach(({ event, handler }) => {
      element.removeEventListener(event, handler);
    });
    
    this.listeners.delete(element);
  }
}

// Usage
const eventManager = new EventManager();
eventManager.addListener(button, 'click', handler);
eventManager.cleanup(button);

Accessibility Considerations

// Keyboard accessibility
button.addEventListener('keydown', function(event) {
  // Handle keyboard interactions for accessibility
  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();
    this.click();
  }
});

// Focus management
function manageFocus(element) {
  element.addEventListener('focus', function() {
    this.setAttribute('aria-expanded', 'true');
  });
  
  element.addEventListener('blur', function() {
    this.setAttribute('aria-expanded', 'false');
  });
}

// Screen reader announcements
function announceToScreenReader(message) {
  const announcement = document.createElement('div');
  announcement.setAttribute('role', 'status');
  announcement.setAttribute('aria-live', 'polite');
  announcement.textContent = message;
  
  document.body.appendChild(announcement);
  
  setTimeout(() => {
    document.body.removeChild(announcement);
  }, 1000);
}

// Usage with events
button.addEventListener('click', function() {
  announceToScreenReader('Button clicked');
});

// Touch accessibility
let lastTouchEnd = 0;
element.addEventListener('touchend', function(event) {
  const now = Date.now();
  
  // Prevent accidental clicks from touch
  if (now - lastTouchEnd < 500) {
    event.preventDefault();
  }
  
  lastTouchEnd = now;
});

Summary

Key Takeaways

  • Events enable interactive and responsive web applications
  • Event delegation improves performance and handles dynamic content
  • Touch events are essential for mobile applications
  • Custom events enable component communication
  • Modern APIs provide better performance and control

Best Practices

  • Use event delegation for dynamic content and performance
  • Throttle/debounce high-frequency events (scroll, resize)
  • Remove event listeners to prevent memory leaks
  • Use passive listeners for scroll/touch events when possible
  • Consider accessibility in event handling

Common Pitfalls

  • Not removing event listeners causing memory leaks
  • Adding too many individual listeners instead of using delegation
  • Not throttling high-frequency events causing performance issues
  • Forgetting to preventDefault when needed
  • Not considering accessibility in event handling