⚑ Event Handling

Events are actions or occurrences that happen in the browserβ€”like clicks, key presses, mouse movements, or page loads. Event handling allows you to respond to these user interactions and create dynamic, interactive web applications.

πŸ’‘ Key Concept

Event handling is the foundation of interactivity on the web. Every button click, form submission, and animation trigger relies on events.

🎯 What Are Events?

Events are signals that something has happened in the browser. They can be triggered by the user (clicks, typing) or by the browser itself (page load, timer completion).

Common Event Types

Category Events Description
Mouse click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave Mouse interactions
Keyboard keydown, keyup, keypress Keyboard input
Form submit, input, change, focus, blur Form interactions
Document DOMContentLoaded, load, resize, scroll Page lifecycle
Drag & Drop dragstart, drag, dragend, drop Drag and drop
Touch touchstart, touchmove, touchend Mobile touch events

πŸ“Œ Adding Event Listeners

There are three ways to add event listeners in JavaScript. The modern, recommended approach is addEventListener().

Method 1: addEventListener() (Recommended)

addEventListener() - Modern Approach
// Syntax
element.addEventListener(eventType, handlerFunction, options);

// Example: Click event
const button = document.querySelector('#myButton');

button.addEventListener('click', function() {
  console.log('Button clicked!');
});

// Arrow function syntax
button.addEventListener('click', () => {
  console.log('Button clicked!');
});

// Named function (allows removal later)
function handleClick() {
  console.log('Button clicked!');
}
button.addEventListener('click', handleClick);

// Multiple listeners on same element
button.addEventListener('click', () => {
  console.log('First listener');
});
button.addEventListener('click', () => {
  console.log('Second listener');
});
// Both will execute!

Method 2: Inline Event Handlers (Not Recommended)

Inline HTML Attributes
<!-- ❌ Not recommended - mixes HTML and JavaScript -->
<button onclick="alert('Clicked!')">Click Me</button>

<!-- ❌ Not recommended - hard to maintain -->
<button onclick="handleClick()">Click Me</button>
<script>
  function handleClick() {
    alert('Clicked!');
  }
</script>

Method 3: DOM Property (Limited)

DOM Event Properties
// ⚠️ Limited - can only have ONE handler per event type
const button = document.querySelector('#myButton');

button.onclick = function() {
  console.log('First handler');
};

// This REPLACES the previous handler
button.onclick = function() {
  console.log('Second handler');  // Only this one will run
};
πŸ’‘ Best Practice

Always use addEventListener(). It allows multiple listeners, is more flexible, and provides better control over event handling.

πŸ“¦ The Event Object

When an event occurs, the browser creates an event object containing information about the event and passes it to the handler function.

Event Object Properties
button.addEventListener('click', function(event) {
  // event (or 'e') is the event object

  console.log(event.type);           // "click"
  console.log(event.target);         // Element that triggered the event
  console.log(event.currentTarget);  // Element listener is attached to
  console.log(event.timeStamp);      // When event occurred
});

// Commonly named 'e' or 'evt'
button.addEventListener('click', (e) => {
  console.log(e.target);
});

// Mouse event properties
element.addEventListener('click', (e) => {
  console.log(e.clientX);    // Mouse X coordinate (viewport)
  console.log(e.clientY);    // Mouse Y coordinate (viewport)
  console.log(e.pageX);      // Mouse X coordinate (document)
  console.log(e.pageY);      // Mouse Y coordinate (document)
  console.log(e.button);     // Which mouse button (0=left, 1=middle, 2=right)
});

// Keyboard event properties
input.addEventListener('keydown', (e) => {
  console.log(e.key);         // Key that was pressed ("a", "Enter", "Escape")
  console.log(e.code);        // Physical key code ("KeyA", "Enter")
  console.log(e.shiftKey);    // true if Shift was held
  console.log(e.ctrlKey);     // true if Ctrl was held
  console.log(e.altKey);      // true if Alt was held
  console.log(e.metaKey);     // true if Meta/Cmd was held
});

πŸ–±οΈ Mouse Events

Mouse Event Examples
const box = document.querySelector('#box');

// Click events
box.addEventListener('click', () => {
  console.log('Single click');
});

box.addEventListener('dblclick', () => {
  console.log('Double click');
});

// Mouse down and up
box.addEventListener('mousedown', () => {
  console.log('Mouse button pressed');
});

box.addEventListener('mouseup', () => {
  console.log('Mouse button released');
});

// Mouse enter and leave
box.addEventListener('mouseenter', () => {
  console.log('Mouse entered element');
  box.style.backgroundColor = 'lightblue';
});

box.addEventListener('mouseleave', () => {
  console.log('Mouse left element');
  box.style.backgroundColor = '';
});

// Mouse move (fires continuously while moving)
box.addEventListener('mousemove', (e) => {
  console.log(`Mouse at: ${e.clientX}, ${e.clientY}`);
});

// Context menu (right-click)
box.addEventListener('contextmenu', (e) => {
  e.preventDefault();  // Prevent default context menu
  console.log('Right-clicked!');
});
Interactive Demo: Mouse Tracker
const tracker = document.querySelector('#tracker');
const display = document.querySelector('#display');

tracker.addEventListener('mousemove', (e) => {
  const x = e.offsetX;  // Mouse X relative to element
  const y = e.offsetY;  // Mouse Y relative to element
  display.textContent = `Position: (${x}, ${y})`;
});

⌨️ Keyboard Events

Keyboard Event Examples
const input = document.querySelector('#textInput');

// keydown - fires when key is pressed
input.addEventListener('keydown', (e) => {
  console.log('Key pressed:', e.key);

  // Check for specific keys
  if (e.key === 'Enter') {
    console.log('Enter key pressed!');
  }

  if (e.key === 'Escape') {
    console.log('Escape pressed!');
  }
});

// keyup - fires when key is released
input.addEventListener('keyup', (e) => {
  console.log('Key released:', e.key);
});

// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
  // Ctrl/Cmd + S (Save)
  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
    e.preventDefault();  // Prevent browser save dialog
    console.log('Save shortcut triggered!');
  }

  // Ctrl/Cmd + K
  if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
    e.preventDefault();
    console.log('Search shortcut!');
  }
});

// Arrow keys
document.addEventListener('keydown', (e) => {
  switch(e.key) {
    case 'ArrowUp':
      console.log('Up arrow');
      break;
    case 'ArrowDown':
      console.log('Down arrow');
      break;
    case 'ArrowLeft':
      console.log('Left arrow');
      break;
    case 'ArrowRight':
      console.log('Right arrow');
      break;
  }
});
⚠️ keypress is Deprecated

The keypress event is deprecated. Use keydown or keyup instead.

πŸ“ Form Events

Form Event Examples
const form = document.querySelector('#myForm');
const input = document.querySelector('#username');
const select = document.querySelector('#country');

// Submit event
form.addEventListener('submit', (e) => {
  e.preventDefault();  // Prevent page reload

  console.log('Form submitted!');

  // Get form data
  const formData = new FormData(form);
  const data = Object.fromEntries(formData);
  console.log(data);
});

// Input event (fires on every change)
input.addEventListener('input', (e) => {
  console.log('Current value:', e.target.value);

  // Real-time validation
  if (e.target.value.length < 3) {
    console.log('Username too short');
  }
});

// Change event (fires when input loses focus)
input.addEventListener('change', (e) => {
  console.log('Input changed to:', e.target.value);
});

// Focus and blur events
input.addEventListener('focus', () => {
  console.log('Input focused');
  input.style.borderColor = 'blue';
});

input.addEventListener('blur', () => {
  console.log('Input lost focus');
  input.style.borderColor = '';
});

// Select/dropdown change
select.addEventListener('change', (e) => {
  console.log('Selected:', e.target.value);
});

// Checkbox/radio events
const checkbox = document.querySelector('#agree');
checkbox.addEventListener('change', (e) => {
  console.log('Checked:', e.target.checked);
});

πŸ“„ Document Events

Document Lifecycle Events
// DOMContentLoaded - fires when HTML is parsed (before images load)
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM is ready!');
  // Safe to manipulate DOM here
});

// load - fires when entire page is loaded (including images)
window.addEventListener('load', () => {
  console.log('Page fully loaded!');
});

// beforeunload - fires before page unload (leaving page)
window.addEventListener('beforeunload', (e) => {
  e.preventDefault();
  e.returnValue = '';  // Show confirmation dialog
});

// Resize event
window.addEventListener('resize', () => {
  console.log('Window resized:', window.innerWidth, window.innerHeight);
});

// Scroll event
window.addEventListener('scroll', () => {
  console.log('Page scrolled:', window.scrollY);
});

// Scroll with throttling (better performance)
let isScrolling;
window.addEventListener('scroll', () => {
  clearTimeout(isScrolling);
  isScrolling = setTimeout(() => {
    console.log('Scroll stopped');
  }, 150);
});

🎈 Event Propagation (Bubbling & Capturing)

When an event occurs on an element, it propagates through the DOM in two phases: capturing (down) and bubbling (up).

Event Bubbling Example
<div id="parent">
  <button id="child">Click Me</button>
</div>

<script>
const parent = document.querySelector('#parent');
const child = document.querySelector('#child');

// Event bubbles up from child to parent
child.addEventListener('click', () => {
  console.log('Child clicked');
});

parent.addEventListener('click', () => {
  console.log('Parent clicked');
});

// When you click the button, both handlers fire:
// Output:
// Child clicked
// Parent clicked
</script>

Stopping Propagation

stopPropagation()
child.addEventListener('click', (e) => {
  console.log('Child clicked');
  e.stopPropagation();  // Stop event from bubbling up
});

parent.addEventListener('click', () => {
  console.log('Parent clicked');  // This won't fire now
});

// stopImmediatePropagation() - stops other listeners on same element too
child.addEventListener('click', (e) => {
  console.log('First listener');
  e.stopImmediatePropagation();
});

child.addEventListener('click', () => {
  console.log('Second listener');  // This won't fire
});

Capturing Phase

Event Capturing (useCapture)
// Third parameter: useCapture (default false)
parent.addEventListener('click', () => {
  console.log('Parent (capturing)');
}, true);  // true = capturing phase

child.addEventListener('click', () => {
  console.log('Child');
});

parent.addEventListener('click', () => {
  console.log('Parent (bubbling)');
}, false);  // false = bubbling phase (default)

// Click order:
// Parent (capturing) β†’ Child β†’ Parent (bubbling)
πŸ“ Event Flow

Complete event flow: Capturing Phase (window β†’ target) β†’ Target Phase β†’ Bubbling Phase (target β†’ window)

🎯 Event Delegation

Event delegation uses event bubbling to handle events on multiple children with a single listener on a parent element. This is more efficient and works with dynamically added elements.

Event Delegation Pattern
// ❌ Bad: Adding listener to each item
const items = document.querySelectorAll('.item');
items.forEach((item) => {
  item.addEventListener('click', () => {
    console.log('Item clicked');
  });
});
// Problem: Doesn't work for items added later

// βœ… Good: Single listener on parent
const list = document.querySelector('#list');

list.addEventListener('click', (e) => {
  // Check if clicked element matches selector
  if (e.target.matches('.item')) {
    console.log('Item clicked:', e.target.textContent);
  }
});

// Works for dynamically added items too!
const newItem = document.createElement('li');
newItem.className = 'item';
newItem.textContent = 'New Item';
list.appendChild(newItem);  // Click listener automatically works!

// More complex delegation with closest()
list.addEventListener('click', (e) => {
  const item = e.target.closest('.item');
  if (item) {
    console.log('Item clicked:', item.dataset.id);
  }

  const deleteBtn = e.target.closest('.delete-btn');
  if (deleteBtn) {
    const item = deleteBtn.closest('.item');
    item.remove();
  }
});

πŸ—‘οΈ Removing Event Listeners

removeEventListener()
// Must use named function to remove listener
function handleClick() {
  console.log('Clicked!');
}

const button = document.querySelector('#myButton');

// Add listener
button.addEventListener('click', handleClick);

// Remove listener
button.removeEventListener('click', handleClick);

// ❌ Won't work - different function reference
button.addEventListener('click', () => console.log('Click'));
button.removeEventListener('click', () => console.log('Click'));

// Pattern: One-time event listener
function handleOnce() {
  console.log('This runs only once');
  button.removeEventListener('click', handleOnce);
}
button.addEventListener('click', handleOnce);

// Modern alternative: once option
button.addEventListener('click', () => {
  console.log('This runs only once');
}, { once: true });

🚫 Preventing Default Behavior

preventDefault()
// Prevent form submission
form.addEventListener('submit', (e) => {
  e.preventDefault();
  console.log('Form not submitted - handling with JS');
});

// Prevent link navigation
link.addEventListener('click', (e) => {
  e.preventDefault();
  console.log('Link not followed');
});

// Prevent context menu
element.addEventListener('contextmenu', (e) => {
  e.preventDefault();
  console.log('Custom context menu here');
});

// Prevent text selection
element.addEventListener('selectstart', (e) => {
  e.preventDefault();
});

// Check if preventDefault was called
element.addEventListener('click', (e) => {
  if (e.defaultPrevented) {
    console.log('Default already prevented');
  }
});

🎨 Custom Events

Creating and Dispatching Custom Events
// Create custom event
const myEvent = new CustomEvent('userLoggedIn', {
  detail: {
    username: 'john_doe',
    timestamp: Date.now()
  },
  bubbles: true,
  cancelable: true
});

// Listen for custom event
document.addEventListener('userLoggedIn', (e) => {
  console.log('User logged in:', e.detail.username);
  console.log('At:', new Date(e.detail.timestamp));
});

// Dispatch custom event
document.dispatchEvent(myEvent);

// Practical example: Component communication
class ShoppingCart {
  constructor() {
    this.items = [];
  }

  addItem(item) {
    this.items.push(item);

    // Dispatch event when item added
    const event = new CustomEvent('itemAdded', {
      detail: { item, totalItems: this.items.length }
    });
    document.dispatchEvent(event);
  }
}

// Listen for cart updates
document.addEventListener('itemAdded', (e) => {
  console.log('Item added:', e.detail.item);
  console.log('Total items:', e.detail.totalItems);
  updateCartBadge(e.detail.totalItems);
});

🎯 Practical Examples

Example 1: Interactive Todo List

Complete Todo List with Events
const todoForm = document.querySelector('#todoForm');
const todoInput = document.querySelector('#todoInput');
const todoList = document.querySelector('#todoList');

// Add todo
todoForm.addEventListener('submit', (e) => {
  e.preventDefault();

  const text = todoInput.value.trim();
  if (!text) return;

  // Create todo item
  const li = document.createElement('li');
  li.innerHTML = `
    <span class="todo-text">${text}</span>
    <button class="complete-btn">βœ“</button>
    <button class="delete-btn">βœ•</button>
  `;

  todoList.appendChild(li);
  todoInput.value = '';
});

// Event delegation for todo actions
todoList.addEventListener('click', (e) => {
  // Complete todo
  if (e.target.matches('.complete-btn')) {
    const li = e.target.closest('li');
    li.classList.toggle('completed');
  }

  // Delete todo
  if (e.target.matches('.delete-btn')) {
    const li = e.target.closest('li');
    li.remove();
  }

  // Toggle by clicking text
  if (e.target.matches('.todo-text')) {
    const li = e.target.closest('li');
    li.classList.toggle('completed');
  }
});

Example 2: Keyboard Navigation

Arrow Key Navigation
const items = document.querySelectorAll('.menu-item');
let currentIndex = 0;

function updateFocus() {
  items.forEach((item, index) => {
    item.classList.toggle('focused', index === currentIndex);
  });
}

document.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowDown') {
    e.preventDefault();
    currentIndex = (currentIndex + 1) % items.length;
    updateFocus();
  }

  if (e.key === 'ArrowUp') {
    e.preventDefault();
    currentIndex = (currentIndex - 1 + items.length) % items.length;
    updateFocus();
  }

  if (e.key === 'Enter') {
    items[currentIndex].click();
  }
});

updateFocus();

Example 3: Drag and Drop

Simple Drag and Drop
const draggables = document.querySelectorAll('.draggable');
const containers = document.querySelectorAll('.container');

// Make elements draggable
draggables.forEach((draggable) => {
  draggable.addEventListener('dragstart', (e) => {
    draggable.classList.add('dragging');
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/html', draggable.innerHTML);
  });

  draggable.addEventListener('dragend', () => {
    draggable.classList.remove('dragging');
  });
});

// Drop zones
containers.forEach((container) => {
  container.addEventListener('dragover', (e) => {
    e.preventDefault();  // Allow drop
    e.dataTransfer.dropEffect = 'move';
    container.classList.add('drag-over');
  });

  container.addEventListener('dragleave', () => {
    container.classList.remove('drag-over');
  });

  container.addEventListener('drop', (e) => {
    e.preventDefault();
    container.classList.remove('drag-over');

    const dragging = document.querySelector('.dragging');
    container.appendChild(dragging);
  });
});

Example 4: Debouncing and Throttling

Performance Optimization
// Debounce - wait until user stops typing
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

// Usage: Search input
const searchInput = document.querySelector('#search');
const handleSearch = debounce((e) => {
  console.log('Searching for:', e.target.value);
  // Make API call here
}, 500);

searchInput.addEventListener('input', handleSearch);

// Throttle - limit execution rate
function throttle(func, limit) {
  let inThrottle;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Usage: Scroll event
const handleScroll = throttle(() => {
  console.log('Scroll position:', window.scrollY);
}, 100);

window.addEventListener('scroll', handleScroll);

βœ… Best Practices

πŸ’‘ Event Handling Best Practices
  • Use addEventListener() instead of inline handlers
  • Always use preventDefault() when handling form submissions
  • Use event delegation for lists and dynamic content
  • Remove event listeners when elements are removed (prevent memory leaks)
  • Use named functions when you need to remove listeners later
  • Debounce or throttle high-frequency events (scroll, resize, input)
  • Use passive event listeners for scroll performance: { passive: true }
  • Check event.target vs event.currentTarget when using delegation
  • Be careful with stopPropagation() - it can break delegation patterns
πŸ“ Summary
  • Events allow you to respond to user interactions and browser actions
  • addEventListener() is the modern, flexible way to handle events
  • Event object contains valuable information about the event
  • Event propagation flows through capturing and bubbling phases
  • Event delegation improves performance and works with dynamic content
  • preventDefault() stops default browser behavior
  • Custom events enable component communication
  • Debouncing and throttling optimize performance for high-frequency events