What are Events?
Events are actions or occurrences that happen in the browser, such as clicking, typing, or loading a page. JavaScript can "listen" for these events and execute code in response.
📝 Adding Event Listeners
addEventListener Method
// Basic syntax: element.addEventListener(event, callback, options)
let button = document.querySelector('#myButton');
button.addEventListener('click', function() {
console.log('Button clicked!');
});
// Arrow function (no own 'this')
button.addEventListener('click', () => {
console.log('Clicked with arrow function');
});
// Named function (reusable, can be removed)
function handleClick() {
console.log('Handled by named function');
}
button.addEventListener('click', handleClick);
// Multiple listeners on same event
button.addEventListener('click', function() {
console.log('First listener');
});
button.addEventListener('click', function() {
console.log('Second listener');
});
// Different events on same element
button.addEventListener('click', () => console.log('Clicked'));
button.addEventListener('mouseenter', () => console.log('Mouse entered'));
button.addEventListener('mouseleave', () => console.log('Mouse left'));
Removing Event Listeners
// Must use named function to remove
function handleClick() {
console.log('Clicked');
}
let button = document.querySelector('button');
button.addEventListener('click', handleClick);
// Remove later
button.removeEventListener('click', handleClick);
// Can't remove anonymous functions
button.addEventListener('click', function() {
console.log('Cannot remove this');
});
// One-time listeners (ES2015)
button.addEventListener('click', handleClick, { once: true });
// Automatically removed after first trigger
Old Methods (avoid)
// onclick property (overwrites previous handlers)
button.onclick = function() {
console.log('Clicked');
};
// This overwrites the above
button.onclick = function() {
console.log('New handler');
};
// Inline HTML (bad practice)
//
// Why avoid: Only one handler per event, mixes HTML and JavaScript
🎯 Common Event Types
Mouse Events
let element = document.querySelector('.box');
// Click events
element.addEventListener('click', e => console.log('Clicked'));
element.addEventListener('dblclick', e => console.log('Double clicked'));
element.addEventListener('contextmenu', e => console.log('Right clicked'));
// Mouse movement
element.addEventListener('mouseenter', e => console.log('Mouse entered'));
element.addEventListener('mouseleave', e => console.log('Mouse left'));
element.addEventListener('mouseover', e => console.log('Mouse over (bubbles)'));
element.addEventListener('mouseout', e => console.log('Mouse out (bubbles)'));
element.addEventListener('mousemove', e => {
console.log(`Position: ${e.clientX}, ${e.clientY}`);
});
// Mouse buttons
element.addEventListener('mousedown', e => console.log('Button pressed'));
element.addEventListener('mouseup', e => console.log('Button released'));
Keyboard Events
let input = document.querySelector('input');
// Keyboard events
input.addEventListener('keydown', e => {
console.log('Key pressed:', e.key);
console.log('Key code:', e.code);
});
input.addEventListener('keyup', e => {
console.log('Key released:', e.key);
});
input.addEventListener('keypress', e => {
// Deprecated, use keydown instead
console.log('Key pressed (old)');
});
// Check modifier keys
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault(); // Prevent browser save
console.log('Ctrl+S pressed');
}
if (e.shiftKey) console.log('Shift held');
if (e.altKey) console.log('Alt held');
if (e.metaKey) console.log('Cmd/Win held');
});
// Specific keys
document.addEventListener('keydown', e => {
if (e.key === 'Enter') console.log('Enter pressed');
if (e.key === 'Escape') console.log('Escape pressed');
if (e.key === 'ArrowUp') console.log('Up arrow');
if (e.code === 'Space') console.log('Spacebar');
});
Form Events
let form = document.querySelector('form');
let input = document.querySelector('input');
// Submit event (on form, not button)
form.addEventListener('submit', e => {
e.preventDefault(); // Prevent page reload
console.log('Form submitted');
// Get form data
let formData = new FormData(form);
for (let [key, value] of formData) {
console.log(key, value);
}
});
// Input events
input.addEventListener('input', e => {
// Fires on every change
console.log('Current value:', e.target.value);
});
input.addEventListener('change', e => {
// Fires when element loses focus after change
console.log('Changed to:', e.target.value);
});
input.addEventListener('focus', e => {
console.log('Input focused');
e.target.classList.add('focused');
});
input.addEventListener('blur', e => {
console.log('Input lost focus');
e.target.classList.remove('focused');
});
// Select event (for text selection)
input.addEventListener('select', e => {
console.log('Text selected');
});
Page Events
// Page loaded (DOM ready)
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM fully loaded');
// Initialize your app here
});
// Everything loaded (images, styles, etc.)
window.addEventListener('load', () => {
console.log('Page fully loaded');
});
// Before unload (user leaving)
window.addEventListener('beforeunload', e => {
// Show confirmation dialog
e.preventDefault();
e.returnValue = ''; // Chrome requires this
});
// Page unloaded
window.addEventListener('unload', () => {
console.log('Page unloading');
});
// Window resized
window.addEventListener('resize', () => {
console.log('Window size:', window.innerWidth, window.innerHeight);
});
// Page scrolled
window.addEventListener('scroll', () => {
console.log('Scroll position:', window.scrollY);
});
📦 The Event Object
element.addEventListener('click', event => {
// Event properties
console.log(event.type); // 'click'
console.log(event.target); // Element that triggered event
console.log(event.currentTarget); // Element listener is attached to
console.log(event.timeStamp); // When event occurred
// Mouse events
console.log(event.clientX); // X relative to viewport
console.log(event.clientY); // Y relative to viewport
console.log(event.pageX); // X relative to document
console.log(event.pageY); // Y relative to document
console.log(event.screenX); // X relative to screen
console.log(event.screenY); // Y relative to screen
console.log(event.button); // Mouse button (0=left, 1=middle, 2=right)
// Keyboard events
console.log(event.key); // Key value ('a', 'Enter', 'Shift')
console.log(event.code); // Physical key ('KeyA', 'Enter')
console.log(event.ctrlKey); // Ctrl held?
console.log(event.shiftKey); // Shift held?
console.log(event.altKey); // Alt held?
console.log(event.metaKey); // Cmd/Win held?
// Form events
console.log(event.target.value); // Input value
console.log(event.target.checked); // Checkbox state
});
// target vs currentTarget
document.querySelector('.container').addEventListener('click', e => {
console.log(e.target); // Element clicked (might be child)
console.log(e.currentTarget); // .container (listener attached here)
});
🎈 Event Bubbling and Capturing
Event Propagation
// HTML:
// Bubbling (default): event travels from target up to root
document.querySelector('button').addEventListener('click', () => {
console.log('Button clicked');
});
document.querySelector('.inner').addEventListener('click', () => {
console.log('Inner clicked');
});
document.querySelector('.outer').addEventListener('click', () => {
console.log('Outer clicked');
});
// Click button logs:
// "Button clicked"
// "Inner clicked"
// "Outer clicked"
// Capturing: event travels from root down to target
document.querySelector('.outer').addEventListener('click', () => {
console.log('Outer (capture)');
}, true); // true enables capture phase
document.querySelector('button').addEventListener('click', () => {
console.log('Button');
});
// Click button logs:
// "Outer (capture)"
// "Button"
// "Inner clicked"
// "Outer clicked"
Stopping Propagation
// stopPropagation - prevent bubbling/capturing
button.addEventListener('click', e => {
e.stopPropagation();
console.log('Button clicked');
// Parent listeners won't fire
});
// stopImmediatePropagation - also stops other listeners on same element
button.addEventListener('click', e => {
e.stopImmediatePropagation();
console.log('First listener');
});
button.addEventListener('click', () => {
console.log('Second listener'); // Won't fire
});
🛡️ preventDefault
// Prevent default behavior
let link = document.querySelector('a');
link.addEventListener('click', e => {
e.preventDefault(); // Don't navigate
console.log('Link clicked but not followed');
});
// Prevent form submission
form.addEventListener('submit', e => {
e.preventDefault();
console.log('Form not submitted');
// Validate and submit manually
if (isValid(form)) {
submitForm(form);
}
});
// Prevent context menu
document.addEventListener('contextmenu', e => {
e.preventDefault();
console.log('Right-click disabled');
});
// Prevent keyboard shortcuts
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('Save prevented');
}
});
// Check if preventDefault is possible
if (!event.defaultPrevented) {
// Default behavior hasn't been prevented yet
}
🎭 Event Delegation
// Instead of adding listener to each item
let items = document.querySelectorAll('.item');
items.forEach(item => {
item.addEventListener('click', handleClick);
});
// Use delegation: attach to parent
let container = document.querySelector('.container');
container.addEventListener('click', e => {
// Check if clicked element matches
if (e.target.matches('.item')) {
handleClick(e);
}
// Or use closest for nested elements
let item = e.target.closest('.item');
if (item) {
handleClick(e, item);
}
});
// Benefits:
// 1. Works with dynamically added elements
// 2. Better performance (one listener instead of many)
// 3. Less memory usage
// Practical example: dynamic list
let list = document.querySelector('.todo-list');
// Add listener once
list.addEventListener('click', e => {
if (e.target.matches('.delete-btn')) {
let item = e.target.closest('.todo-item');
item.remove();
}
if (e.target.matches('.checkbox')) {
let item = e.target.closest('.todo-item');
item.classList.toggle('completed');
}
});
// Add items dynamically (listeners work automatically)
function addTodo(text) {
let item = document.createElement('div');
item.className = 'todo-item';
item.innerHTML = `
${text}
`;
list.appendChild(item);
}
⚙️ Event Options
// addEventListener options (third parameter)
element.addEventListener('click', handler, {
capture: false, // Use capture phase (default: false)
once: true, // Remove after first trigger
passive: true // Never calls preventDefault (improves scroll performance)
});
// Passive listeners (for scroll/touch events)
element.addEventListener('touchstart', e => {
// Can't use preventDefault with passive: true
console.log('Touch started');
}, { passive: true });
// Once option (ES2015)
button.addEventListener('click', () => {
console.log('Clicked once');
// Automatically removed
}, { once: true });
// Signal for aborting listeners
let controller = new AbortController();
element.addEventListener('click', handler, {
signal: controller.signal
});
// Remove listener later
controller.abort(); // Removes all listeners with this signal
💡 Practical Examples
Toggle Dark Mode
let toggleBtn = document.querySelector('#darkModeToggle');
toggleBtn.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
let isDark = document.body.classList.contains('dark-mode');
toggleBtn.textContent = isDark ? '☀️ Light Mode' : '🌙 Dark Mode';
// Save preference
localStorage.setItem('darkMode', isDark);
});
// Load preference on page load
document.addEventListener('DOMContentLoaded', () => {
let isDark = localStorage.getItem('darkMode') === 'true';
if (isDark) {
document.body.classList.add('dark-mode');
}
});
Form Validation
let form = document.querySelector('#signupForm');
let email = form.querySelector('#email');
let password = form.querySelector('#password');
// Real-time validation
email.addEventListener('input', () => {
let isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
email.classList.toggle('invalid', !isValid);
});
password.addEventListener('input', () => {
let isValid = password.value.length >= 8;
password.classList.toggle('invalid', !isValid);
});
// Submit validation
form.addEventListener('submit', e => {
e.preventDefault();
let errors = [];
if (!email.value) {
errors.push('Email required');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
errors.push('Invalid email');
}
if (password.value.length < 8) {
errors.push('Password must be 8+ characters');
}
if (errors.length > 0) {
alert(errors.join('\n'));
} else {
console.log('Form valid, submitting...');
form.submit();
}
});
Debouncing Search Input
let searchInput = document.querySelector('#search');
let timeout;
searchInput.addEventListener('input', e => {
// Clear previous timeout
clearTimeout(timeout);
// Set new timeout
timeout = setTimeout(() => {
performSearch(e.target.value);
}, 500); // Wait 500ms after typing stops
});
function performSearch(query) {
console.log('Searching for:', query);
// Make API call
}
Keyboard Shortcuts
let shortcuts = {
's': () => save(),
'n': () => createNew(),
'o': () => open(),
'Escape': () => closeModal()
};
document.addEventListener('keydown', e => {
// Only if Ctrl/Cmd is held (except Escape)
if ((e.ctrlKey || e.metaKey) || e.key === 'Escape') {
if (shortcuts[e.key]) {
e.preventDefault();
shortcuts[e.key]();
}
}
});
function save() {
console.log('Saving...');
}
function createNew() {
console.log('Creating new...');
}
Drag and Drop
let draggable = document.querySelector('.draggable');
let dropZone = document.querySelector('.drop-zone');
// Make element draggable
draggable.draggable = true;
draggable.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', e.target.id);
e.target.classList.add('dragging');
});
draggable.addEventListener('dragend', e => {
e.target.classList.remove('dragging');
});
// Drop zone
dropZone.addEventListener('dragover', e => {
e.preventDefault(); // Allow drop
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('drag-over');
let id = e.dataTransfer.getData('text/plain');
let element = document.getElementById(id);
dropZone.appendChild(element);
});
Infinite Scroll
let loading = false;
let page = 1;
window.addEventListener('scroll', () => {
// Check if near bottom
let scrollPosition = window.scrollY + window.innerHeight;
let pageHeight = document.documentElement.scrollHeight;
if (scrollPosition > pageHeight - 100 && !loading) {
loadMoreItems();
}
});
async function loadMoreItems() {
loading = true;
console.log('Loading page', page);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Add items
let container = document.querySelector('.items');
for (let i = 0; i < 10; i++) {
let item = document.createElement('div');
item.className = 'item';
item.textContent = `Item ${(page - 1) * 10 + i + 1}`;
container.appendChild(item);
}
page++;
loading = false;
}
🎯 Key Takeaways
- addEventListener: Modern way to attach event handlers, allows multiple listeners
- Event Object: Contains information about the event (target, coordinates, keys)
- preventDefault: Stops default browser behavior (form submit, link navigation)
- stopPropagation: Prevents event from bubbling up to parent elements
- Event Delegation: Attach listener to parent, check target - works with dynamic content
- Event Bubbling: Events propagate from target to root by default
- Named Functions: Required to remove event listeners later
- Once Option: Automatically remove listener after first trigger