Events, Forms & Browser Storage
The event system
Events are how JavaScript responds to user actions — clicks, key presses, form submissions, scrolling, and more.
Adding event listeners
JavaScript
const button = document.querySelector('#submit');
button.addEventListener('click', (event) => {
console.log('Button clicked!');
console.log('Click coordinates:', event.clientX, event.clientY);
});Always use
addEventListener — not onclick. It lets you attach multiple handlers and remove them later.Common events
| Event | Triggers when... |
|---|---|
click | Element is clicked |
dblclick | Element is double-clicked |
mouseover / mouseout | Mouse enters/leaves element |
keydown / keyup | Key is pressed/released |
input | Input value changes |
change | Input value changes AND loses focus |
submit | Form is submitted |
focus / blur | Element gains/loses focus |
scroll | Element or page is scrolled |
DOMContentLoaded | HTML is fully parsed |
load | Page + all resources are loaded |
The event object
Every event handler receives an event object with useful properties:
JavaScript
document.addEventListener('keydown', (event) => {
console.log(event.key); // "Enter", "Escape", "a", etc.
console.log(event.ctrlKey); // true if Ctrl was held
console.log(event.target); // the element that fired the event
});Preventing default behavior
Some elements have default behaviors (links navigate, forms reload the page). Stop them with
preventDefault():JavaScript
const form = document.querySelector('form');
form.addEventListener('submit', (event) => {
event.preventDefault(); // stop page reload
const formData = new FormData(form);
console.log(formData.get('email'));
});Event delegation
Instead of adding a listener to every list item, add one to the parent:
JavaScript
document.querySelector('.todo-list').addEventListener('click', (event) => {
if (event.target.matches('.delete-btn')) {
const item = event.target.closest('.todo-item');
item.remove();
}
});This works because events bubble up — a click on a button inside a list item also fires on the list item, then the list, then the body, all the way to
document.Event delegation is:
- More efficient (one listener instead of hundreds)
- Works for dynamically added elements
- Used by every major framework internally
Working with forms
Getting form values
JavaScript
const form = document.querySelector('#signup');
form.addEventListener('submit', (event) => {
event.preventDefault();
// Option 1: FormData
const data = new FormData(form);
const email = data.get('email');
const password = data.get('password');
// Option 2: Object from FormData
const values = Object.fromEntries(new FormData(form));
// { email: '...', password: '...' }
});Real-time validation
JavaScript
const emailInput = document.querySelector('#email');
const emailError = document.querySelector('#email-error');
emailInput.addEventListener('input', () => {
if (!emailInput.value.includes('@')) {
emailError.textContent = 'Please enter a valid email';
emailInput.setAttribute('aria-invalid', 'true');
} else {
emailError.textContent = '';
emailInput.removeAttribute('aria-invalid');
}
});localStorage
Persist data across page reloads and browser restarts:
JavaScript
// Save
localStorage.setItem('theme', 'dark');
// Read
const theme = localStorage.getItem('theme'); // "dark"
// Remove
localStorage.removeItem('theme');
// Clear everything
localStorage.clear();Storing objects
localStorage only stores strings. Use JSON for complex data:
JavaScript
const user = { name: 'Alice', preferences: { theme: 'dark' } };
// Save
localStorage.setItem('user', JSON.stringify(user));
// Read
const stored = JSON.parse(localStorage.getItem('user'));
console.log(stored.name); // "Alice"A helper function
JavaScript
function storage(key, value) {
if (value === undefined) {
const item = localStorage.getItem(key);
try { return JSON.parse(item); } catch { return item; }
}
if (value === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(value));
}
}
storage('user', { name: 'Alice' }); // save
storage('user'); // read → { name: 'Alice' }
storage('user', null); // deletesessionStorage
Same API as localStorage, but data is cleared when the tab closes. Use it for temporary state like form drafts:
JavaScript
const form = document.querySelector('#editor');
form.addEventListener('input', () => {
sessionStorage.setItem('draft', form.querySelector('textarea').value);
});
window.addEventListener('load', () => {
const draft = sessionStorage.getItem('draft');
if (draft) {
form.querySelector('textarea').value = draft;
}
});Cookies vs localStorage vs sessionStorage
| Feature | Cookies | localStorage | sessionStorage |
|---|---|---|---|
| Capacity | ~4KB | ~5-10MB | ~5-10MB |
| Sent to server | Yes (every request) | No | No |
| Expiration | Configurable | Never | Tab close |
| Access | Server + client | Client only | Client only |
Use cookies for auth tokens (httpOnly, secure). Use localStorage for user preferences. Use sessionStorage for temporary data.
Event phases: capture, target, and bubble
Events travel in two directions. During the capture phase, the event moves from
document down to the target element. During the bubble phase, it travels back up. Most handlers run on bubble (the default). Pass `{
capture: true }
as the third argument to addEventListener to run during capture — useful for intercepting events before children handle them.
event.stopPropagation() prevents the event from reaching other elements. Use it sparingly; it can break delegation patterns. event.stopImmediatePropagation() also blocks other listeners on the same element.
For scroll and touch listeners, {
passive: true }
tells the browser you will not call preventDefault(), enabling smoother scrolling. Omit passive only when you genuinely need to block default behavior.
Storage limits and security
localStorage is synchronous and blocks the main thread on large writes — keep payloads small. Never store secrets (API keys, passwords, session tokens) in localStorage; any script on the page can read it. Prefer httpOnly cookies for session tokens set by the server.
Data in localStorage persists until cleared. Version your stored objects so you can migrate schema changes: {
version: 2, tasks:
[
...] }
. On read, if version is outdated, transform or reset the data instead of crashing on missing fields.
Private browsing modes may throw QuotaExceededError or silently fail — always wrap writes in try/catch and degrade gracefully when persistence is unavailable.
Key takeaway
Events drive interactivity — use addEventListener, understand event delegation, and always preventDefault` on forms. For persistence, use localStorage for long-term data and sessionStorage for temporary state. Wrap JSON parsing in try/catch since stored data might be corrupted.