Chapter 6: JavaScript DOM Manipulation - Complete Guide to Browser APIs
DOM (Document Object Model) manipulation is the foundation of interactive web development. It allows JavaScript to dynamically modify HTML content, handle user interactions, and create responsive web applications. Mastering DOM manipulation is essential for building modern, interactive websites.
Why DOM Manipulation Matters in JavaScript
DOM manipulation in JavaScript is essential because it:
- Creates Interactive Websites: Enables dynamic content updates and user interactions
- Handles User Events: Responds to clicks, form submissions, and keyboard input
- Manages Application State: Updates the UI based on data changes
- Enables Single Page Applications: Creates smooth, app-like experiences
- Supports Modern Web Development: Essential for frameworks like React, Vue, and Angular
Learning Objectives
Through this chapter, you will master:
- DOM element selection and traversal
- Event handling and delegation
- Dynamic content creation and modification
- Form handling and validation
- Browser APIs and storage
- Performance optimization techniques
DOM Element Selection
Basic Selection Methods
// getElementById - select by unique ID
const header = document.getElementById('main-header');
console.log(header); // Returns the element or null
// getElementsByClassName - select by class name
const buttons = document.getElementsByClassName('btn');
console.log(buttons); // Returns HTMLCollection (live collection)
// getElementsByTagName - select by tag name
const paragraphs = document.getElementsByTagName('p');
console.log(paragraphs); // Returns HTMLCollection
// querySelector - select first matching element
const firstButton = document.querySelector('.btn');
const firstParagraph = document.querySelector('p');
const elementById = document.querySelector('#main-header');
// querySelectorAll - select all matching elements
const allButtons = document.querySelectorAll('.btn');
const allParagraphs = document.querySelectorAll('p');
console.log(allButtons); // Returns NodeList (static collection)
// CSS selector examples
const complexSelectors = {
descendant: document.querySelectorAll('div p'), // p inside div
child: document.querySelectorAll('ul > li'), // direct li children of ul
adjacent: document.querySelectorAll('h1 + p'), // p immediately after h1
sibling: document.querySelectorAll('h1 ~ p'), // all p siblings after h1
attribute: document.querySelectorAll('[data-id]'), // elements with data-id
attributeValue: document.querySelectorAll('[data-id="123"]'), // specific value
pseudo: document.querySelectorAll('p:first-child'), // first child paragraphs
not: document.querySelectorAll('p:not(.special)') // paragraphs without .special
};
Advanced Selection Techniques
// Selecting within a specific container
const container = document.getElementById('content');
const buttonsInContainer = container.querySelectorAll('.btn');
// Selecting by data attributes
const dataElements = document.querySelectorAll('[data-role="button"]');
const customData = document.querySelectorAll('[data-user-id]');
// Selecting by multiple classes
const multiClass = document.querySelectorAll('.btn.primary.large');
// Selecting by text content (using XPath-like approach)
function selectByText(text) {
const elements = document.querySelectorAll('*');
return Array.from(elements).filter(el =>
el.textContent.trim() === text
);
}
// Selecting by partial text match
function selectByPartialText(partialText) {
const elements = document.querySelectorAll('*');
return Array.from(elements).filter(el =>
el.textContent.includes(partialText)
);
}
// Example usage
const elementsWithText = selectByText('Click me');
const elementsWithPartial = selectByPartialText('Hello');
DOM Traversal
Parent and Child Navigation
const element = document.querySelector('.target');
// Parent navigation
const parent = element.parentElement;
const parentNode = element.parentNode;
const closestParent = element.closest('.container'); // Find closest ancestor
// Child navigation
const firstChild = element.firstElementChild;
const lastChild = element.lastElementChild;
const allChildren = element.children; // HTMLCollection
const childNodes = element.childNodes; // NodeList (includes text nodes)
// Sibling navigation
const nextSibling = element.nextElementSibling;
const previousSibling = element.previousElementSibling;
const nextSiblingNode = element.nextSibling; // Includes text nodes
const previousSiblingNode = element.previousSibling;
// Traversal examples
function findAncestorWithClass(element, className) {
let current = element;
while (current && !current.classList.contains(className)) {
current = current.parentElement;
}
return current;
}
function getAllSiblings(element) {
const siblings = [];
let current = element.parentElement.firstElementChild;
while (current) {
if (current !== element) {
siblings.push(current);
}
current = current.nextElementSibling;
}
return siblings;
}
// Example usage
const container = findAncestorWithClass(element, 'container');
const siblings = getAllSiblings(element);
Element Creation and Modification
Creating Elements
// Create new elements
const newDiv = document.createElement('div');
const newParagraph = document.createElement('p');
const newButton = document.createElement('button');
// Set element properties
newDiv.id = 'new-container';
newDiv.className = 'container primary';
newDiv.classList.add('active', 'highlighted');
newDiv.classList.remove('inactive');
newDiv.classList.toggle('visible');
// Set attributes
newButton.setAttribute('type', 'button');
newButton.setAttribute('data-id', '123');
newButton.setAttribute('aria-label', 'Submit form');
// Set content
newParagraph.textContent = 'This is a new paragraph';
newParagraph.innerHTML = '<strong>Bold text</strong> and <em>italic text</em>';
// Create elements with content
function createElementWithContent(tag, content, attributes = {}) {
const element = document.createElement(tag);
element.textContent = content;
Object.entries(attributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
return element;
}
// Example usage
const newListItem = createElementWithContent('li', 'New item', {
'class': 'list-item',
'data-id': '456'
});
Modifying Elements
const element = document.querySelector('.target');
// Content modification
element.textContent = 'New text content';
element.innerHTML = '<span>HTML content</span>';
element.outerHTML = '<div class="new-element">Replaced content</div>';
// Style modification
element.style.color = 'red';
element.style.backgroundColor = '#f0f0f0';
element.style.fontSize = '16px';
element.style.display = 'none';
element.style.display = 'block';
// CSS class manipulation
element.classList.add('new-class');
element.classList.remove('old-class');
element.classList.toggle('active');
element.classList.replace('old-class', 'new-class');
// Attribute modification
element.setAttribute('data-value', '123');
element.removeAttribute('data-old');
const hasAttribute = element.hasAttribute('data-value');
const attributeValue = element.getAttribute('data-value');
// Dataset API (for data-* attributes)
element.dataset.userId = '123';
element.dataset.userName = 'john';
console.log(element.dataset); // DOMStringMap { userId: '123', userName: 'john' }
Adding and Removing Elements
const container = document.querySelector('.container');
const newElement = document.createElement('div');
// Adding elements
container.appendChild(newElement); // Add to end
container.insertBefore(newElement, container.firstChild); // Add to beginning
container.insertAdjacentElement('beforeend', newElement); // Add to end
container.insertAdjacentElement('afterbegin', newElement); // Add to beginning
container.insertAdjacentElement('beforebegin', newElement); // Add before container
container.insertAdjacentElement('afterend', newElement); // Add after container
// insertAdjacentHTML - insert HTML string
container.insertAdjacentHTML('beforeend', '<p>New paragraph</p>');
// insertAdjacentText - insert text
container.insertAdjacentText('beforeend', 'New text content');
// Removing elements
container.removeChild(newElement); // Remove specific child
newElement.remove(); // Remove self (modern method)
// Replacing elements
const oldElement = document.querySelector('.old');
const newElement = document.createElement('div');
container.replaceChild(newElement, oldElement); // Replace child
oldElement.replaceWith(newElement); // Replace self (modern method)
// Cloning elements
const clonedElement = newElement.cloneNode(true); // Deep clone
const shallowClone = newElement.cloneNode(false); // Shallow clone
Event Handling
Basic Event Listeners
const button = document.querySelector('.btn');
// addEventListener - modern approach
button.addEventListener('click', function(event) {
console.log('Button clicked!', event);
});
// Arrow function
button.addEventListener('click', (event) => {
console.log('Button clicked with arrow function!');
});
// Named function
function handleClick(event) {
console.log('Button clicked with named function!');
event.preventDefault(); // Prevent default behavior
event.stopPropagation(); // Stop event bubbling
}
button.addEventListener('click', handleClick);
// Multiple event types
const input = document.querySelector('input');
input.addEventListener('input', handleInput);
input.addEventListener('focus', handleFocus);
input.addEventListener('blur', handleBlur);
function handleInput(event) {
console.log('Input value:', event.target.value);
}
function handleFocus(event) {
event.target.style.borderColor = 'blue';
}
function handleBlur(event) {
event.target.style.borderColor = 'gray';
}
// Remove event listeners
button.removeEventListener('click', handleClick);
Event Object and Properties
function handleEvent(event) {
// Event properties
console.log('Event type:', event.type);
console.log('Target element:', event.target);
console.log('Current target:', event.currentTarget);
console.log('Event phase:', event.eventPhase);
// Mouse events
if (event.type.includes('mouse')) {
console.log('Mouse position:', event.clientX, event.clientY);
console.log('Page position:', event.pageX, event.pageY);
console.log('Button pressed:', event.button);
console.log('Modifier keys:', {
ctrl: event.ctrlKey,
shift: event.shiftKey,
alt: event.altKey,
meta: event.metaKey
});
}
// Keyboard events
if (event.type.includes('key')) {
console.log('Key pressed:', event.key);
console.log('Key code:', event.code);
console.log('Char code:', event.charCode);
}
// Form events
if (event.target.tagName === 'INPUT') {
console.log('Input value:', event.target.value);
console.log('Input type:', event.target.type);
}
// Prevent default behavior
if (event.defaultPrevented) {
console.log('Default behavior was prevented');
}
// Stop event propagation
event.stopPropagation();
event.stopImmediatePropagation();
}
// Add to multiple elements
document.querySelectorAll('.interactive').forEach(element => {
element.addEventListener('click', handleEvent);
});
Event Delegation
// Event delegation - handle events on parent for dynamic children
const list = document.querySelector('.dynamic-list');
// Instead of adding listeners to each item
list.addEventListener('click', function(event) {
// Check if clicked element is a list item
if (event.target.matches('li')) {
console.log('List item clicked:', event.target.textContent);
event.target.classList.toggle('selected');
}
// Check for specific buttons within items
if (event.target.matches('.delete-btn')) {
event.target.closest('li').remove();
}
// Check for elements with specific data attributes
if (event.target.matches('[data-action="edit"]')) {
const itemId = event.target.dataset.itemId;
editItem(itemId);
}
});
// Dynamic content example
function addListItem(text) {
const li = document.createElement('li');
li.innerHTML = `
<span>${text}</span>
<button class="delete-btn" data-action="delete">Delete</button>
<button class="edit-btn" data-action="edit" data-item-id="${Date.now()}">Edit</button>
`;
list.appendChild(li);
}
// Add some items
addListItem('Item 1');
addListItem('Item 2');
addListItem('Item 3');
Custom Events
// Create and dispatch custom events
const customEvent = new CustomEvent('myCustomEvent', {
detail: { message: 'Hello from custom event!' },
bubbles: true,
cancelable: true
});
// Listen for custom event
document.addEventListener('myCustomEvent', function(event) {
console.log('Custom event received:', event.detail.message);
});
// Dispatch custom event
document.dispatchEvent(customEvent);
// Custom event with more complex data
function createUserEvent(userId, action) {
return new CustomEvent('userAction', {
detail: {
userId: userId,
action: action,
timestamp: Date.now()
},
bubbles: true
});
}
// Listen for user events
document.addEventListener('userAction', function(event) {
const { userId, action, timestamp } = event.detail;
console.log(`User ${userId} performed ${action} at ${new Date(timestamp)}`);
});
// Dispatch user event
const userEvent = createUserEvent(123, 'login');
document.dispatchEvent(userEvent);
Form Handling
Form Validation
const form = document.querySelector('#user-form');
const inputs = form.querySelectorAll('input, select, textarea');
// Real-time validation
inputs.forEach(input => {
input.addEventListener('blur', validateField);
input.addEventListener('input', clearErrors);
});
function validateField(event) {
const field = event.target;
const value = field.value.trim();
const fieldName = field.name;
// Clear previous errors
clearFieldError(field);
// Validation rules
const rules = {
required: value !== '',
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
minLength: value.length >= field.minLength,
maxLength: value.length <= field.maxLength,
pattern: field.pattern ? new RegExp(field.pattern).test(value) : true
};
// Check validation
const isValid = Object.values(rules).every(rule => rule);
if (!isValid) {
showFieldError(field, getErrorMessage(fieldName, rules));
}
return isValid;
}
function showFieldError(field, message) {
field.classList.add('error');
let errorElement = field.parentNode.querySelector('.error-message');
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.className = 'error-message';
field.parentNode.appendChild(errorElement);
}
errorElement.textContent = message;
}
function clearFieldError(field) {
field.classList.remove('error');
const errorElement = field.parentNode.querySelector('.error-message');
if (errorElement) {
errorElement.remove();
}
}
function clearErrors(event) {
clearFieldError(event.target);
}
function getErrorMessage(fieldName, rules) {
if (!rules.required) return `${fieldName} is required`;
if (!rules.email) return 'Please enter a valid email address';
if (!rules.minLength) return `Minimum length is ${fieldName.minLength} characters`;
if (!rules.maxLength) return `Maximum length is ${fieldName.maxLength} characters`;
if (!rules.pattern) return 'Invalid format';
return 'Invalid input';
}
Form Submission
// Handle form submission
form.addEventListener('submit', async function(event) {
event.preventDefault();
// Validate all fields
const isValid = validateForm();
if (!isValid) {
showFormError('Please fix the errors above');
return;
}
// Collect form data
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// Show loading state
showLoadingState();
try {
// Submit data
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
showSuccessMessage('Form submitted successfully!');
form.reset();
} catch (error) {
showFormError('Failed to submit form. Please try again.');
console.error('Form submission error:', error);
} finally {
hideLoadingState();
}
});
function validateForm() {
let isValid = true;
inputs.forEach(input => {
if (!validateField({ target: input })) {
isValid = false;
}
});
return isValid;
}
function showLoadingState() {
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
}
function hideLoadingState() {
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.disabled = false;
submitBtn.textContent = 'Submit';
}
function showSuccessMessage(message) {
const messageElement = document.createElement('div');
messageElement.className = 'success-message';
messageElement.textContent = message;
form.insertBefore(messageElement, form.firstChild);
setTimeout(() => {
messageElement.remove();
}, 3000);
}
function showFormError(message) {
const errorElement = document.createElement('div');
errorElement.className = 'form-error';
errorElement.textContent = message;
form.insertBefore(errorElement, form.firstChild);
setTimeout(() => {
errorElement.remove();
}, 5000);
}
Browser APIs
Local Storage
// Local Storage API
class StorageManager {
static setItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}
static getItem(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('Error reading from localStorage:', error);
return defaultValue;
}
}
static removeItem(key) {
localStorage.removeItem(key);
}
static clear() {
localStorage.clear();
}
static getAllKeys() {
return Object.keys(localStorage);
}
static getStorageSize() {
let total = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += localStorage[key].length + key.length;
}
}
return total;
}
}
// Usage examples
StorageManager.setItem('user', { name: 'John', email: '[email protected]' });
const user = StorageManager.getItem('user');
StorageManager.setItem('settings', { theme: 'dark', language: 'en' });
// Session Storage (similar API, but data is cleared when tab closes)
sessionStorage.setItem('tempData', 'This will be cleared when tab closes');
Geolocation API
// Geolocation API
function getCurrentLocation() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocation is not supported by this browser'));
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
timestamp: position.timestamp
});
},
(error) => {
reject(error);
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000 // 5 minutes
}
);
});
}
// Watch position changes
function watchLocation(callback) {
if (!navigator.geolocation) {
console.error('Geolocation not supported');
return null;
}
return navigator.geolocation.watchPosition(
(position) => {
callback({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
});
},
(error) => {
console.error('Geolocation error:', error);
}
);
}
// Usage
getCurrentLocation()
.then(location => {
console.log('Current location:', location);
})
.catch(error => {
console.error('Error getting location:', error);
});
// Stop watching
const watchId = watchLocation((location) => {
console.log('Location updated:', location);
});
// navigator.geolocation.clearWatch(watchId);
Intersection Observer
// Intersection Observer for lazy loading and animations
const observerOptions = {
root: null, // Use viewport as root
rootMargin: '0px',
threshold: 0.1 // Trigger when 10% visible
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Element is visible
entry.target.classList.add('visible');
// Lazy load images
if (entry.target.tagName === 'IMG' && entry.target.dataset.src) {
entry.target.src = entry.target.dataset.src;
entry.target.removeAttribute('data-src');
}
// Load more content
if (entry.target.classList.contains('load-more-trigger')) {
loadMoreContent();
}
} else {
// Element is not visible
entry.target.classList.remove('visible');
}
});
}, observerOptions);
// Observe elements
document.querySelectorAll('.lazy-image, .load-more-trigger, .animate-on-scroll').forEach(el => {
observer.observe(el);
});
function loadMoreContent() {
// Simulate loading more content
console.log('Loading more content...');
}
Performance Optimization
Efficient DOM Operations
// Batch DOM operations
function inefficientDOM() {
const container = document.querySelector('.container');
// Inefficient - causes multiple reflows
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
container.appendChild(div); // Reflow on each append
}
}
function efficientDOM() {
const container = document.querySelector('.container');
const fragment = document.createDocumentFragment();
// Efficient - batch operations
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div); // No reflow
}
container.appendChild(fragment); // Single reflow
}
// Debouncing for performance
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Throttling for performance
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Usage examples
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
}, 300);
const throttledScroll = throttle(() => {
console.log('Scroll event');
}, 100);
// Event listeners with debouncing/throttling
document.querySelector('#search-input').addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
window.addEventListener('scroll', throttledScroll);
Best Practices
1. Use Modern DOM Methods
// Good: Use modern methods
element.classList.add('active');
element.dataset.userId = '123';
element.remove();
// Avoid: Old methods
element.className += ' active';
element.setAttribute('data-user-id', '123');
element.parentNode.removeChild(element);
2. Cache DOM Queries
// Good: Cache frequently used elements
const elements = {
container: document.querySelector('.container'),
button: document.querySelector('.btn'),
input: document.querySelector('input')
};
// Avoid: Repeated queries
function badExample() {
document.querySelector('.container').appendChild(element);
document.querySelector('.container').classList.add('active');
document.querySelector('.container').style.display = 'block';
}
3. Use Event Delegation
// Good: Event delegation for dynamic content
document.addEventListener('click', (e) => {
if (e.target.matches('.delete-btn')) {
e.target.closest('li').remove();
}
});
// Avoid: Adding listeners to each element
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', () => {
btn.closest('li').remove();
});
});
Summary
DOM manipulation is essential for interactive web development:
- Element Selection: Use modern querySelector methods for efficient selection
- Event Handling: Implement event delegation and proper event management
- Dynamic Content: Create and modify elements efficiently
- Form Handling: Validate and process form data
- Browser APIs: Leverage storage, geolocation, and other APIs
- Performance: Use batching, debouncing, and throttling for optimization
Mastering these concepts enables you to build responsive, interactive web applications that provide excellent user experiences.
This tutorial is part of the JavaScript Mastery series by syscook.dev