UC Berkeley School of Information
October 6, 2025
Review of DOM and Events concepts from your videos
↓ Navigate down for details
window
└── document
└── html
├── head
│ └── title (text node)
└── body
├── div#main
│ ├── h1.title (text node)
│ └── p (text node)
└── script
// Single element selectors
const element = document.getElementById('myId');
const firstMatch = document.querySelector('.myClass');
// Multiple element selectors
const allDivs = document.getElementsByTagName('div');
const allCards = document.querySelectorAll('.card');
// Adding an event listener
button.addEventListener('click', function() {
console.log('Button clicked!');
});
// The event object
button.addEventListener('click', function(e) {
console.log(e.target); // The element that was clicked
console.log(e.type); // 'click'
});
// Creating elements
const newDiv = document.createElement('div');
newDiv.textContent = 'Hello World';
newDiv.className = 'my-class';
// Adding to DOM
parentElement.appendChild(newDiv);
// Modifying existing elements
element.innerHTML = '<strong>Bold text</strong>';
element.style.backgroundColor = 'blue';
Professional techniques for DOM manipulation
↓ Navigate down for advanced patterns
// Complex selectors
const submitBtn = document.querySelector('form#userForm button[type="submit"]');
const activeItems = document.querySelectorAll('li.item:not(.disabled)');
// Relative selection
const card = document.querySelector('.card');
const cardTitle = card.querySelector('.title'); // Searches within card
const nextCard = card.nextElementSibling;
const parentContainer = card.closest('.container');
// Navigate the DOM tree efficiently
const element = document.querySelector('.current');
// Parent navigation
const parent = element.parentElement;
const grandparent = element.parentElement.parentElement;
// Sibling navigation
const prevSibling = element.previousElementSibling;
const nextSibling = element.nextElementSibling;
const allSiblings = [...element.parentElement.children]
.filter(child => child !== element);
// Children navigation
const firstChild = element.firstElementChild;
const lastChild = element.lastElementChild;
const allChildren = element.children; // HTMLCollection
const childArray = [...element.children]; // Convert to array
// Build a complete component
function createCard(title, content, imageUrl) {
const card = document.createElement('article');
card.className = 'card';
// Use template literal for structure
card.innerHTML = `
<img src="${imageUrl}" alt="${title}" class="card-image">
<div class="card-body">
<h3 class="card-title">${title}</h3>
<p class="card-content">${content}</p>
<button class="card-btn" data-action="delete">Delete</button>
</div>
`;
// Add event listener to button
card.querySelector('.card-btn').addEventListener('click', handleDelete);
return card;
}
// Add to page
const container = document.querySelector('.container');
container.appendChild(createCard('Title', 'Content', 'image.jpg'));
// Batch DOM updates for performance
const fragment = document.createDocumentFragment();
const list = document.querySelector('#myList');
// Build in memory
data.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
fragment.appendChild(li); // Add to fragment, not DOM
});
// Single DOM update
list.appendChild(fragment);
// Update multiple properties efficiently
const element = document.querySelector('.target');
element.style.cssText = `
background-color: blue;
color: white;
padding: 10px;
border-radius: 5px;
`;
// Modern class manipulation
const element = document.querySelector('.box');
// Add/remove/toggle classes
element.classList.add('active', 'highlighted');
element.classList.remove('disabled');
element.classList.toggle('visible'); // Add if missing, remove if present
// Conditional class
element.classList.toggle('error', hasError); // Second param is condition
// Check for class
if (element.classList.contains('active')) {
// Do something
}
// Attribute manipulation
element.setAttribute('data-id', '123');
element.getAttribute('data-id'); // '123'
element.removeAttribute('disabled');
element.hasAttribute('required'); // true/false
// Data attributes shortcut
element.dataset.id = '456'; // Sets data-id="456"
const id = element.dataset.id; // Gets data-id value
Making web pages interactive
↓ Navigate down for event expertise
// The event object contains everything you need
button.addEventListener('click', function(e) {
// Event properties
console.log(e.type); // 'click'
console.log(e.target); // Element that triggered event
console.log(e.currentTarget); // Element with listener (this)
// Mouse event properties
console.log(e.clientX, e.clientY); // Mouse position
// Keyboard event properties (for keydown/keyup)
console.log(e.key); // 'Enter', 'a', 'ArrowUp', etc.
console.log(e.ctrlKey, e.shiftKey, e.altKey); // Modifier keys
// Prevent default behavior
e.preventDefault(); // Stop normal action
e.stopPropagation(); // Stop bubbling
});
// Mouse events
element.addEventListener('click', handleClick);
element.addEventListener('dblclick', handleDoubleClick);
element.addEventListener('mouseenter', handleMouseEnter); // Doesn't bubble
element.addEventListener('mouseleave', handleMouseLeave); // Doesn't bubble
element.addEventListener('mouseover', handleMouseOver); // Bubbles
element.addEventListener('mouseout', handleMouseOut); // Bubbles
// Keyboard events
input.addEventListener('keydown', handleKeyDown); // Key pressed down
input.addEventListener('keyup', handleKeyUp); // Key released
input.addEventListener('keypress', handleKeyPress); // Deprecated!
// Form events
form.addEventListener('submit', handleSubmit);
input.addEventListener('input', handleInput); // Every change
input.addEventListener('change', handleChange); // On blur after change
input.addEventListener('focus', handleFocus);
input.addEventListener('blur', handleBlur);
// Document/Window events
window.addEventListener('load', handleLoad); // Everything loaded
document.addEventListener('DOMContentLoaded', handleReady); // DOM ready
window.addEventListener('resize', handleResize);
window.addEventListener('scroll', handleScroll);
// Pattern 1: Named functions (recommended)
function handleClick(e) {
console.log('Clicked:', e.target);
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick); // Can remove!
// Pattern 2: Arrow functions (careful with 'this')
button.addEventListener('click', (e) => {
console.log(this); // Window, not button!
});
// Pattern 3: Event handler object
const handler = {
handleEvent(e) {
switch(e.type) {
case 'click':
this.onClick(e);
break;
case 'mouseover':
this.onHover(e);
break;
}
},
onClick(e) { /* ... */ },
onHover(e) { /* ... */ }
};
button.addEventListener('click', handler);
button.addEventListener('mouseover', handler);
// Professional form handling
const form = document.querySelector('#userForm');
form.addEventListener('submit', async (e) => {
e.preventDefault(); // Stop page reload
// Get form data efficiently
const formData = new FormData(form);
const data = Object.fromEntries(formData);
// Validation
if (!data.email || !data.email.includes('@')) {
showError('Invalid email');
return;
}
// Disable form during submission
const submitBtn = form.querySelector('[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
try {
// Submit data
await submitToServer(data);
form.reset(); // Clear form
showSuccess('Form submitted!');
} catch (error) {
showError('Submission failed');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Submit';
}
});
// Create and dispatch custom events
const customEvent = new CustomEvent('taskComplete', {
detail: {
taskId: 123,
completedAt: Date.now()
},
bubbles: true,
cancelable: true
});
// Dispatch the event
element.dispatchEvent(customEvent);
// Listen for custom events
document.addEventListener('taskComplete', (e) => {
console.log('Task completed:', e.detail.taskId);
updateTaskCount();
});
// Real-world pattern: Component communication
class TodoItem {
complete() {
this.element.dispatchEvent(new CustomEvent('todo:complete', {
detail: { id: this.id },
bubbles: true
}));
}
}
// Parent listens for all todo events
todoList.addEventListener('todo:complete', (e) => {
updateStats();
saveToLocalStorage();
});
Advanced event patterns for scalable applications
↓ Navigate down for professional patterns
// Event propagation demonstration
const outer = document.querySelector('.outer');
const inner = document.querySelector('.inner');
const button = document.querySelector('.button');
// Capture phase (rarely used)
outer.addEventListener('click', (e) => {
console.log('Outer capture');
}, true); // Third parameter true = capture
// Bubble phase (default)
outer.addEventListener('click', (e) => {
console.log('Outer bubble');
}); // No third parameter = bubble
inner.addEventListener('click', (e) => {
console.log('Inner bubble');
e.stopPropagation(); // Stops here
});
button.addEventListener('click', (e) => {
console.log('Button clicked');
});
// Click button logs: "Outer capture" → "Button clicked" → "Inner bubble"
// Outer bubble never fires due to stopPropagation
// Without delegation: Bad for dynamic content
const buttons = document.querySelectorAll('.delete-btn');
buttons.forEach(btn => {
btn.addEventListener('click', handleDelete);
}); // Doesn't work for new buttons!
// With delegation: Perfect for dynamic content
const todoList = document.querySelector('#todoList');
todoList.addEventListener('click', (e) => {
// Check what was clicked
if (e.target.classList.contains('delete-btn')) {
const todoItem = e.target.closest('.todo-item');
todoItem.remove();
}
if (e.target.classList.contains('edit-btn')) {
const todoItem = e.target.closest('.todo-item');
editTodo(todoItem);
}
if (e.target.type === 'checkbox') {
const todoItem = e.target.closest('.todo-item');
toggleComplete(todoItem);
}
});
// Add new items without adding listeners
function addTodo(text) {
const html = `
<li class="todo-item">
<input type="checkbox">
<span>${text}</span>
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
</li>
`;
todoList.insertAdjacentHTML('beforeend', html);
// No need to add event listeners!
}
// Delegation with data attributes
document.addEventListener('click', (e) => {
const action = e.target.dataset.action;
if (!action) return;
const actions = {
delete: () => {
const id = e.target.dataset.id;
deleteItem(id);
},
edit: () => {
const id = e.target.dataset.id;
openEditModal(id);
},
toggle: () => {
e.target.closest('.panel').classList.toggle('expanded');
}
};
if (actions[action]) {
e.preventDefault();
actions[action]();
}
});
// HTML uses data attributes
// <button data-action="delete" data-id="123">Delete</button>
// <button data-action="edit" data-id="123">Edit</button>
// <button data-action="toggle">Toggle Panel</button>
// Throttle scroll events
let scrollTimeout;
window.addEventListener('scroll', () => {
if (scrollTimeout) return;
scrollTimeout = setTimeout(() => {
scrollTimeout = null;
handleScroll();
}, 100); // Max once per 100ms
});
// Debounce input events
let inputTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(inputTimeout);
inputTimeout = setTimeout(() => {
performSearch(e.target.value);
}, 300); // Wait 300ms after typing stops
});
// Passive listeners for better scroll performance
document.addEventListener('touchstart', handleTouch, { passive: true });
// Once option for one-time events
button.addEventListener('click', handleClick, { once: true });
Making data survive page refreshes
↓ Navigate down for storage patterns
// Basic LocalStorage operations
// Set item (always strings!)
localStorage.setItem('username', 'john');
localStorage.setItem('theme', 'dark');
// Get item
const username = localStorage.getItem('username'); // 'john'
const missing = localStorage.getItem('nonexistent'); // null
// Remove item
localStorage.removeItem('username');
// Clear everything
localStorage.clear();
// Working with objects/arrays
const user = { name: 'John', age: 30 };
localStorage.setItem('user', JSON.stringify(user));
const savedUser = JSON.parse(localStorage.getItem('user'));
console.log(savedUser.name); // 'John'
// Check if LocalStorage is available
if (typeof(Storage) !== "undefined") {
// LocalStorage is supported
} else {
// No web storage support
}
// Storage wrapper with error handling
class Storage {
static get(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
console.error('Storage get error:', e);
return defaultValue;
}
}
static set(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
console.error('Storage set error:', e);
return false;
}
}
static remove(key) {
localStorage.removeItem(key);
}
}
// Usage
const todos = Storage.get('todos', []); // Default to empty array
Storage.set('todos', [...todos, newTodo]);
// Auto-save form drafts
const form = document.querySelector('#myForm');
const DRAFT_KEY = 'formDraft';
// Save on input
form.addEventListener('input', () => {
const formData = new FormData(form);
const draft = Object.fromEntries(formData);
Storage.set(DRAFT_KEY, draft);
});
// Restore on load
window.addEventListener('DOMContentLoaded', () => {
const draft = Storage.get(DRAFT_KEY);
if (draft) {
Object.entries(draft).forEach(([name, value]) => {
const field = form.elements[name];
if (field) field.value = value;
});
}
});
// Full todo app with localStorage
class TodoApp {
constructor() {
this.todos = Storage.get('todos', []);
this.init();
}
init() {
this.renderTodos();
this.attachListeners();
}
attachListeners() {
// Single delegated listener
document.querySelector('#todoList').addEventListener('click', (e) => {
const id = e.target.dataset.id;
if (e.target.classList.contains('delete-btn')) {
this.deleteTodo(id);
} else if (e.target.type === 'checkbox') {
this.toggleTodo(id);
}
});
// Form submission
document.querySelector('#todoForm').addEventListener('submit', (e) => {
e.preventDefault();
const input = e.target.elements.todoText;
this.addTodo(input.value);
input.value = '';
});
}
addTodo(text) {
const todo = {
id: Date.now().toString(),
text,
completed: false
};
this.todos.push(todo);
this.save();
this.renderTodos();
}
deleteTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.save();
this.renderTodos();
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.save();
this.renderTodos();
}
}
save() {
Storage.set('todos', this.todos);
}
renderTodos() {
const html = this.todos.map(todo => `
<li class="todo-item ${todo.completed ? 'completed' : ''}">
<input type="checkbox" data-id="${todo.id}"
${todo.completed ? 'checked' : ''}>
<span>${todo.text}</span>
<button class="delete-btn" data-id="${todo.id}">×</button>
</li>
`).join('');
document.querySelector('#todoList').innerHTML = html;
}
}
// Initialize app
new TodoApp();