javascript-today

Advanced Event Handling

Beyond basic event listeners, understanding event propagation, delegation, and advanced patterns will make your code more efficient and maintainable.

Event Propagation

Events propagate through the DOM in three phases:

1. Capture phase: From document → target
2. Target phase: At the target element
3. Bubble phase: From target → document

Event Bubbling

Events bubble up from child to parent:

<div id="outer">
  <div id="middle">
    <button id="inner">Click me</button>
  </div>
</div>
const outer = document.querySelector('#outer');
const middle = document.querySelector('#middle');
const inner = document.querySelector('#inner');

outer.addEventListener('click', () => {
  console.log('Outer clicked');
});

middle.addEventListener('click', () => {
  console.log('Middle clicked');
});

inner.addEventListener('click', () => {
  console.log('Inner clicked');
});

// Clicking button logs:
// "Inner clicked"
// "Middle clicked"
// "Outer clicked"

Stopping Propagation

inner.addEventListener('click', (e) => {
  console.log('Inner clicked');
  e.stopPropagation(); // Stop bubbling
});

// Now only logs: "Inner clicked"

Event Capture Phase

Listen during capture instead of bubble:

// Third parameter = true for capture phase
outer.addEventListener('click', () => {
  console.log('Outer clicked (capture)');
}, true);

inner.addEventListener('click', () => {
  console.log('Inner clicked');
});

// Logs:
// "Outer clicked (capture)" (capture phase, top-down)
// "Inner clicked" (bubble phase, bottom-up)

Event Object

The event object contains useful information:

element.addEventListener('click', (e) => {
  // Target: element that triggered the event
  console.log(e.target); // <button>
  
  // CurrentTarget: element with the listener
  console.log(e.currentTarget); // element
  
  // Event type
  console.log(e.type); // "click"
  
  // Mouse position
  console.log(e.clientX, e.clientY); // Viewport coordinates
  console.log(e.pageX, e.pageY);     // Document coordinates
  
  // Modifier keys
  console.log(e.ctrlKey);  // Ctrl pressed?
  console.log(e.shiftKey); // Shift pressed?
  console.log(e.altKey);   // Alt pressed?
  
  // Button clicked (mouse events)
  console.log(e.button); // 0: left, 1: middle, 2: right
  
  // Timestamp
  console.log(e.timeStamp);
});

target vs currentTarget

<div id="parent">
  <button id="child">Click me</button>
</div>
const parent = document.querySelector('#parent');

parent.addEventListener('click', (e) => {
  console.log('Target:', e.target);           // <button> (what was clicked)
  console.log('CurrentTarget:', e.currentTarget); // <div> (what has the listener)
});

// Clicking button logs:
// Target: <button id="child">
// CurrentTarget: <div id="parent">

Event Delegation

Attach one listener to a parent instead of many listeners to children:

Without Delegation (Inefficient)

// ❌ Add listener to every item
const items = document.querySelectorAll('.item');

items.forEach(item => {
  item.addEventListener('click', () => {
    console.log('Item clicked:', item.textContent);
  });
});

// Problems:
// - 100 items = 100 listeners
// - Doesn't work for dynamically added items

With Delegation (Efficient)

// ✅ One listener on parent
const list = document.querySelector('#list');

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

// Benefits:
// - 1000 items = 1 listener
// - Works for dynamically added items

Practical Delegation Examples

Delete buttons:

<ul id="todo-list">
  <li>
    Task 1
    <button class="delete-btn" data-id="1">Delete</button>
  </li>
  <li>
    Task 2
    <button class="delete-btn" data-id="2">Delete</button>
  </li>
</ul>
const list = document.querySelector('#todo-list');

list.addEventListener('click', (e) => {
  // Find if click was on delete button
  const deleteBtn = e.target.closest('.delete-btn');
  
  if (deleteBtn) {
    const id = deleteBtn.dataset.id;
    const listItem = deleteBtn.closest('li');
    
    console.log('Delete task:', id);
    listItem.remove();
  }
});

Table row actions:

const table = document.querySelector('#data-table');

table.addEventListener('click', (e) => {
  const row = e.target.closest('tr');
  
  if (!row) return;
  
  // Edit button
  if (e.target.classList.contains('edit-btn')) {
    console.log('Edit row:', row.dataset.id);
  }
  
  // Delete button
  if (e.target.classList.contains('delete-btn')) {
    if (confirm('Delete this row?')) {
      row.remove();
    }
  }
});

Preventing Default Behavior

preventDefault()

Stop browser’s default action:

// Prevent form submission
const form = document.querySelector('form');

form.addEventListener('submit', (e) => {
  e.preventDefault(); // Don't submit form
  
  // Handle with JavaScript instead
  const formData = new FormData(e.target);
  console.log('Form data:', Object.fromEntries(formData));
});

// Prevent link navigation
const link = document.querySelector('a');

link.addEventListener('click', (e) => {
  e.preventDefault();
  console.log('Link clicked, but not navigating');
});

// Prevent context menu
document.addEventListener('contextmenu', (e) => {
  e.preventDefault();
  console.log('Right-click disabled');
});

Common Use Cases

// Custom file upload
const fileInput = document.querySelector('input[type="file"]');

fileInput.addEventListener('change', (e) => {
  e.preventDefault();
  const files = e.target.files;
  // Handle files with custom logic
});

// Drag and drop
const dropZone = document.querySelector('#drop-zone');

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault(); // Allow drop
  dropZone.classList.add('drag-over');
});

dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  const files = e.dataTransfer.files;
  console.log('Files dropped:', files);
});

// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
  // Ctrl+S to save
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault();
    console.log('Saving...');
  }
});

Keyboard Events

Key Events

const input = document.querySelector('input');

// Key pressed down
input.addEventListener('keydown', (e) => {
  console.log('Key down:', e.key);
});

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

// Character typed (deprecated, use keydown)
input.addEventListener('keypress', (e) => {
  console.log('Key press:', e.key);
});

Keyboard Properties

document.addEventListener('keydown', (e) => {
  // Key name
  console.log('Key:', e.key);        // "a", "Enter", "Escape", "ArrowUp"
  console.log('Code:', e.code);      // "KeyA", "Enter", "Escape", "ArrowUp"
  
  // Modifiers
  console.log('Ctrl:', e.ctrlKey);
  console.log('Shift:', e.shiftKey);
  console.log('Alt:', e.altKey);
  console.log('Meta:', e.metaKey);  // Command/Windows key
});

Keyboard Shortcuts

// Global shortcuts
document.addEventListener('keydown', (e) => {
  // Ctrl+K
  if (e.ctrlKey && e.key === 'k') {
    e.preventDefault();
    openSearchModal();
  }
  
  // Escape to close
  if (e.key === 'Escape') {
    closeModal();
  }
  
  // Arrow navigation
  if (e.key === 'ArrowUp') {
    navigateUp();
  }
  
  if (e.key === 'ArrowDown') {
    navigateDown();
  }
});

Input Filtering

// Numbers only
const numberInput = document.querySelector('#number');

numberInput.addEventListener('keydown', (e) => {
  // Allow: backspace, delete, arrows, tab
  const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'];
  
  if (allowedKeys.includes(e.key)) {
    return;
  }
  
  // Allow: Ctrl+A, Ctrl+C, Ctrl+V
  if (e.ctrlKey) {
    return;
  }
  
  // Block non-numeric keys
  if (!/^[0-9]$/.test(e.key)) {
    e.preventDefault();
  }
});

Mouse Events

Mouse Event Types

const element = document.querySelector('#box');

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

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

// Mouse button events
element.addEventListener('mousedown', () => {
  console.log('Button pressed');
});

element.addEventListener('mouseup', () => {
  console.log('Button released');
});

// Mouse movement
element.addEventListener('mouseenter', () => {
  console.log('Mouse entered (no bubbling)');
});

element.addEventListener('mouseleave', () => {
  console.log('Mouse left (no bubbling)');
});

element.addEventListener('mouseover', () => {
  console.log('Mouse over (bubbles)');
});

element.addEventListener('mouseout', () => {
  console.log('Mouse out (bubbles)');
});

element.addEventListener('mousemove', (e) => {
  console.log('Mouse position:', e.clientX, e.clientY);
});

Mouse Coordinates

element.addEventListener('click', (e) => {
  // Relative to viewport
  console.log('Client:', e.clientX, e.clientY);
  
  // Relative to page (with scroll)
  console.log('Page:', e.pageX, e.pageY);
  
  // Relative to element
  const rect = element.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;
  console.log('Element:', x, y);
});

Drag Example

let isDragging = false;
let offsetX, offsetY;

const draggable = document.querySelector('#draggable');

draggable.addEventListener('mousedown', (e) => {
  isDragging = true;
  
  const rect = draggable.getBoundingClientRect();
  offsetX = e.clientX - rect.left;
  offsetY = e.clientY - rect.top;
  
  draggable.style.cursor = 'grabbing';
});

document.addEventListener('mousemove', (e) => {
  if (!isDragging) return;
  
  draggable.style.left = (e.clientX - offsetX) + 'px';
  draggable.style.top = (e.clientY - offsetY) + 'px';
});

document.addEventListener('mouseup', () => {
  isDragging = false;
  draggable.style.cursor = 'grab';
});

Custom Events

Create and dispatch custom events:

// Create custom event
const customEvent = new CustomEvent('userLogin', {
  detail: {
    username: 'alice',
    timestamp: Date.now()
  }
});

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

// Dispatch event
document.dispatchEvent(customEvent);

Practical Custom Events

// Component communication
class TodoList {
  addTodo(todo) {
    // ... add todo logic
    
    // Notify other components
    const event = new CustomEvent('todoAdded', {
      detail: { todo }
    });
    
    this.element.dispatchEvent(event);
  }
}

const todoList = new TodoList();

todoList.element.addEventListener('todoAdded', (e) => {
  console.log('New todo:', e.detail.todo);
  updateStats();
});

Event Performance

Debouncing

Limit how often a function executes:

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

// Usage: Search as user types
const searchInput = document.querySelector('#search');

const handleSearch = debounce((e) => {
  console.log('Searching for:', e.target.value);
  // Expensive search operation
}, 300);

searchInput.addEventListener('input', handleSearch);

Throttling

Execute at most once per time period:

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

DO:

  • Use event delegation for lists
  • Remove event listeners when elements are removed
  • Use preventDefault() appropriately
  • Debounce/throttle expensive operations
  • Check e.target in delegated events

DON’T:

  • Attach many listeners to many elements
  • Forget to remove listeners (memory leaks)
  • Stop propagation unnecessarily
  • Put heavy logic in mousemove/scroll handlers
  • Use inline event handlers (onclick="…")

Summary

Concept Method Use Case
Stop bubbling e.stopPropagation() Prevent parent handlers
Prevent default e.preventDefault() Stop form submit, link click
Event delegation Listener on parent Efficient list handling
Target element e.target What was clicked
Current element e.currentTarget What has the listener
Debounce Custom function Search input, resize
Throttle Custom function Scroll, mousemove
Custom events CustomEvent Component communication