Skip to main content

Module 14: Events and Event Handling

Events are actions that happen in the browser—clicks, key presses, page loads, etc. Handling events properly is crucial for creating interactive web applications.


1. Understanding Events

1.1 What are Events?

Events are signals that something has happened in the browser. They can be triggered by:

  • User actions (click, keyboard, mouse movement)
  • Browser actions (page load, resize)
  • Network actions (data loaded)
  • Timer actions (setTimeout, setInterval)

1.2 Event Types

// Mouse events
'click', 'dblclick', 'mousedown', 'mouseup', 'mousemove',
'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'contextmenu'

// Keyboard events
'keydown', 'keyup', 'keypress'

// Form events
'submit', 'change', 'input', 'focus', 'blur', 'reset'

// Window events
'load', 'resize', 'scroll', 'beforeunload', 'unload'

// Media events
'play', 'pause', 'ended', 'volumechange', 'timeupdate'

// Drag events
'drag', 'dragstart', 'dragend', 'dragenter', 'dragleave', 'drop'

2. Event Listeners

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

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

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

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

2.2 Multiple Listeners

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

// Can add multiple listeners
button.addEventListener('click', function() {
console.log('First handler');
});

button.addEventListener('click', function() {
console.log('Second handler');
});

// Both will execute

2.3 Event Listener Options

const element = document.querySelector('.item');

// Once: Remove after first execution
element.addEventListener('click', handleClick, { once: true });

// Capture phase
element.addEventListener('click', handleClick, { capture: true });

// Passive: Can't preventDefault
element.addEventListener('touchstart', handleTouch, { passive: true });

// All options
element.addEventListener('click', handleClick, {
once: true,
capture: false,
passive: false
});

2.4 Inline Handlers (Avoid)

<!-- ❌ Not recommended -->
<button onclick="alert('Clicked!')">Click me</button>
<button onclick="handleClick()">Click me</button>
// ❌ Not recommended
button.onclick = function() {
console.log('Clicked');
};

// Only one handler possible
button.onclick = function() {
console.log('This overwrites the previous handler');
};
Use addEventListener

Always use addEventListener instead of inline handlers or onclick properties. It's more flexible and maintainable.


3. Event Object

3.1 Event Properties

element.addEventListener('click', function(event) {
console.log('Event type:', event.type); // "click"
console.log('Target:', event.target); // Element that triggered event
console.log('Current target:', event.currentTarget); // Element listener is attached to
console.log('Timestamp:', event.timeStamp); // When event occurred
console.log('Is trusted:', event.isTrusted); // User-initiated vs script
});

3.2 Mouse Event Properties

element.addEventListener('click', function(event) {
// Position relative to viewport
console.log('Client X:', event.clientX);
console.log('Client Y:', event.clientY);

// Position relative to page
console.log('Page X:', event.pageX);
console.log('Page Y:', event.pageY);

// Position relative to screen
console.log('Screen X:', event.screenX);
console.log('Screen Y:', event.screenY);

// Mouse button
console.log('Button:', event.button); // 0=left, 1=middle, 2=right

// Modifier keys
console.log('Ctrl:', event.ctrlKey);
console.log('Shift:', event.shiftKey);
console.log('Alt:', event.altKey);
console.log('Meta:', event.metaKey); // Cmd/Windows key
});

3.3 Keyboard Event Properties

element.addEventListener('keydown', function(event) {
console.log('Key:', event.key); // "a", "Enter", "ArrowUp"
console.log('Code:', event.code); // "KeyA", "Enter", "ArrowUp"
console.log('Key code:', event.keyCode); // Deprecated

// Modifier keys
console.log('Ctrl:', event.ctrlKey);
console.log('Shift:', event.shiftKey);
console.log('Alt:', event.altKey);

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

if (event.ctrlKey && event.key === 's') {
event.preventDefault();
console.log('Ctrl+S pressed');
}
});

4. Event Flow

4.1 Capturing and Bubbling

<div id="outer">
<div id="middle">
<button id="inner">Click me</button>
</div>
</div>
// Event flow: Capturing → Target → Bubbling

// Capturing phase (root to target)
document.getElementById('outer').addEventListener('click', () => {
console.log('Outer (capture)');
}, true);

document.getElementById('middle').addEventListener('click', () => {
console.log('Middle (capture)');
}, true);

// Bubbling phase (target to root)
document.getElementById('inner').addEventListener('click', () => {
console.log('Inner (target)');
});

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

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

// Output when clicking button:
// Outer (capture)
// Middle (capture)
// Inner (target)
// Middle (bubble)
// Outer (bubble)

4.2 stopPropagation()

element.addEventListener('click', function(event) {
console.log('Element clicked');
event.stopPropagation(); // Stop bubbling to parent elements
});

parent.addEventListener('click', function() {
console.log('This will not run if child stopPropagation');
});

4.3 stopImmediatePropagation()

element.addEventListener('click', function(event) {
console.log('First handler');
event.stopImmediatePropagation(); // Stop all other handlers
});

element.addEventListener('click', function() {
console.log('This will not run');
});

5. preventDefault()

5.1 Preventing Default Behavior

// Prevent form submission
form.addEventListener('submit', function(event) {
event.preventDefault();
console.log('Form submission prevented');

// Custom handling
const formData = new FormData(event.target);
// ... process data
});

// Prevent link navigation
link.addEventListener('click', function(event) {
event.preventDefault();
console.log('Link click prevented');
});

// Prevent context menu
document.addEventListener('contextmenu', function(event) {
event.preventDefault();
console.log('Right-click menu prevented');
});

5.2 Conditional preventDefault

input.addEventListener('keypress', function(event) {
// Only allow numbers
if (!/[0-9]/.test(event.key)) {
event.preventDefault();
}
});

link.addEventListener('click', function(event) {
if (!isLoggedIn) {
event.preventDefault();
showLoginPrompt();
}
});

6. Event Delegation

6.1 Why Event Delegation?

// ❌ Bad: Add listener to each item
const items = document.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', handleClick);
});

// ✅ Good: Single listener on parent
const container = document.querySelector('.container');
container.addEventListener('click', function(event) {
if (event.target.matches('.item')) {
handleClick(event);
}
});

6.2 Delegation Pattern

// Handle multiple element types
container.addEventListener('click', function(event) {
const target = event.target;

if (target.matches('.delete-btn')) {
deleteItem(target);
} else if (target.matches('.edit-btn')) {
editItem(target);
} else if (target.matches('.item')) {
selectItem(target);
}
});

6.3 Closest() for Nested Elements

list.addEventListener('click', function(event) {
// Find closest .item ancestor
const item = event.target.closest('.item');

if (item) {
console.log('Item clicked:', item);
}
});
Event Delegation Benefits
  • Better performance (fewer listeners)
  • Works with dynamically added elements
  • Less memory usage
  • Easier to manage

7. Common Event Patterns

7.1 Click Events

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

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

// Prevent double-click selection
button.addEventListener('mousedown', function(event) {
if (event.detail > 1) {
event.preventDefault();
}
});

7.2 Form Events

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

// Change event (fires on blur/enter)
select.addEventListener('change', function(event) {
console.log('Selected:', event.target.value);
});

// Submit event
form.addEventListener('submit', function(event) {
event.preventDefault();

const formData = new FormData(event.target);
const data = Object.fromEntries(formData);
console.log('Form data:', data);
});

// Focus/blur
input.addEventListener('focus', function() {
this.classList.add('focused');
});

input.addEventListener('blur', function() {
this.classList.remove('focused');
});

7.3 Keyboard Events

// Keydown (fires when key is pressed)
document.addEventListener('keydown', function(event) {
console.log('Key pressed:', event.key);

// Keyboard shortcuts
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
saveDocument();
}

if (event.key === 'Escape') {
closeModal();
}

// Arrow keys
if (event.key.startsWith('Arrow')) {
handleArrowKey(event.key);
}
});

// Keyup (fires when key is released)
input.addEventListener('keyup', function(event) {
if (event.key === 'Enter') {
submitForm();
}
});

7.4 Mouse Events

// Hover effects
element.addEventListener('mouseenter', function() {
this.classList.add('hovered');
});

element.addEventListener('mouseleave', function() {
this.classList.remove('hovered');
});

// Mouse movement
document.addEventListener('mousemove', function(event) {
console.log(`Mouse at ${event.clientX}, ${event.clientY}`);
});

// Drag tracking
let isDragging = false;

element.addEventListener('mousedown', function() {
isDragging = true;
});

document.addEventListener('mousemove', function(event) {
if (isDragging) {
updatePosition(event.clientX, event.clientY);
}
});

document.addEventListener('mouseup', function() {
isDragging = false;
});

7.5 Window Events

// Page load
window.addEventListener('load', function() {
console.log('Page fully loaded');
});

// DOM ready (modern)
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM ready');
initializeApp();
});

// Window resize
window.addEventListener('resize', function() {
console.log('Window resized:', window.innerWidth, window.innerHeight);
});

// Scroll
window.addEventListener('scroll', function() {
console.log('Scroll position:', window.scrollY);
});

// Before unload (leaving page)
window.addEventListener('beforeunload', function(event) {
if (hasUnsavedChanges) {
event.preventDefault();
event.returnValue = ''; // Show confirmation dialog
}
});

8. Custom Events

8.1 Creating Custom Events

// Create custom event
const event = new CustomEvent('userLogin', {
detail: {
username: 'john_doe',
timestamp: Date.now()
},
bubbles: true,
cancelable: true
});

// Dispatch event
element.dispatchEvent(event);

// Listen for custom event
element.addEventListener('userLogin', function(event) {
console.log('User logged in:', event.detail.username);
});

8.2 Event Communication Pattern

// Publisher
class DataService {
loadData() {
fetch('/api/data')
.then(response => response.json())
.then(data => {
const event = new CustomEvent('dataLoaded', {
detail: { data }
});
document.dispatchEvent(event);
});
}
}

// Subscriber
document.addEventListener('dataLoaded', function(event) {
console.log('Data received:', event.detail.data);
updateUI(event.detail.data);
});

9. Debouncing and Throttling

9.1 Debounce (Wait for Pause)

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 debouncedSearch = debounce(function(event) {
console.log('Searching for:', event.target.value);
performSearch(event.target.value);
}, 300);

searchInput.addEventListener('input', debouncedSearch);

9.2 Throttle (Limit Frequency)

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 throttledScroll = throttle(function() {
console.log('Scroll position:', window.scrollY);
updateScrollIndicator();
}, 100);

window.addEventListener('scroll', throttledScroll);
Performance Optimization

Use debouncing for search/input and throttling for scroll/resize events to improve performance.


10. Practical Examples

10.1 Form Validation

const form = document.querySelector('#registrationForm');
const emailInput = document.querySelector('#email');
const passwordInput = document.querySelector('#password');

emailInput.addEventListener('blur', function() {
const email = this.value;
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

if (!isValid) {
this.classList.add('error');
showError(this, 'Please enter a valid email');
} else {
this.classList.remove('error');
clearError(this);
}
});

form.addEventListener('submit', function(event) {
event.preventDefault();

if (validateForm()) {
submitForm();
}
});

10.2 Drag and Drop

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

draggable.addEventListener('dragstart', function(event) {
event.dataTransfer.setData('text/plain', event.target.id);
event.target.classList.add('dragging');
});

draggable.addEventListener('dragend', function(event) {
event.target.classList.remove('dragging');
});

dropzone.addEventListener('dragover', function(event) {
event.preventDefault(); // Allow drop
this.classList.add('drag-over');
});

dropzone.addEventListener('dragleave', function() {
this.classList.remove('drag-over');
});

dropzone.addEventListener('drop', function(event) {
event.preventDefault();
this.classList.remove('drag-over');

const id = event.dataTransfer.getData('text/plain');
const element = document.getElementById(id);
this.appendChild(element);
});

10.3 Infinite Scroll

let page = 1;
let loading = false;

window.addEventListener('scroll', function() {
if (loading) return;

const scrollPosition = window.scrollY + window.innerHeight;
const threshold = document.documentElement.scrollHeight - 200;

if (scrollPosition >= threshold) {
loading = true;
loadMoreContent();
}
});

async function loadMoreContent() {
page++;
const response = await fetch(`/api/items?page=${page}`);
const items = await response.json();

appendItems(items);
loading = false;
}

10.4 Keyboard Navigation

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

document.addEventListener('keydown', function(event) {
if (event.key === 'ArrowDown') {
event.preventDefault();
currentIndex = Math.min(currentIndex + 1, items.length - 1);
focusItem(currentIndex);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
currentIndex = Math.max(currentIndex - 1, 0);
focusItem(currentIndex);
} else if (event.key === 'Enter') {
items[currentIndex].click();
}
});

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

Summary

In this module, you learned:

  • ✅ Understanding event types and the event object
  • ✅ Adding and removing event listeners
  • ✅ Event flow: capturing, target, and bubbling phases
  • ✅ preventDefault() and stopPropagation()
  • ✅ Event delegation pattern
  • ✅ Common event patterns (click, form, keyboard, mouse)
  • ✅ Custom events for component communication
  • ✅ Performance optimization with debouncing and throttling
  • ✅ Practical examples: forms, drag-drop, infinite scroll
Next Steps

In Module 15, you'll learn about Browser APIs including localStorage, fetch, and more.


Practice Exercises

  1. Build a modal that closes on Escape key or outside click
  2. Create an autocomplete search with debouncing
  3. Implement a carousel with keyboard navigation
  4. Build a context menu that appears on right-click
  5. Create a sortable list with drag-and-drop
  6. Implement form validation with real-time feedback
  7. Build an image gallery with keyboard shortcuts
  8. Create a custom dropdown with keyboard support

Additional Resources