What is the DOM?
The Document Object Model (DOM) is a programming interface for web documents. It represents the page structure as a tree of objects that JavaScript can manipulate. When a web page is loaded, the browser creates a DOM representation of the page.
Key Concepts:
- Document: The entire HTML document
- Element: HTML tags like
<div>,<p>,<h1> - Node: Every part of the document (elements, text, comments)
- Tree Structure: Hierarchical representation with parent-child relationships
DOM Tree Example:
document
└── html
├── head
│ ├── title
│ └── meta
└── body
├── header
│ └── h1
└── main
├── p
└── div
DOM Structure and Tree
Every HTML document follows a tree structure where:
documentis the root- Elements have parent-child relationships
- Text content is also considered nodes
<!DOCTYPE html>
<html>
<head>
<title>DOM Example</title>
</head>
<body>
<div id="container">
<p>Hello World</p>
<span>This is a span</span>
</div>
</body>
</html>
Selecting Elements
1. getElementById()
// Select element by ID
const element = document.getElementById('myId');
console.log(element);
// Example
const header = document.getElementById('main-header');
if (header) {
console.log('Header found:', header.textContent);
}
2. getElementsByClassName()
// Returns HTMLCollection (array-like)
const elements = document.getElementsByClassName('my-class');
console.log(elements.length);
// Loop through elements
for (let i = 0; i < elements.length; i++) {
console.log(elements[i].textContent);
}
// Convert to array for array methods
const elementsArray = Array.from(elements);
elementsArray.forEach(el => console.log(el));
3. getElementsByTagName()
// Select all paragraphs
const paragraphs = document.getElementsByTagName('p');
// Select all divs
const divs = document.getElementsByTagName('div');
// Loop through
Array.from(paragraphs).forEach((p, index) => {
console.log(`Paragraph ${index + 1}: ${p.textContent}`);
});
4. querySelector() - Modern Approach
// Select first matching element
const firstDiv = document.querySelector('div');
const elementById = document.querySelector('#myId');
const elementByClass = document.querySelector('.my-class');
const complexSelector = document.querySelector('div.container p.highlight');
// Advanced selectors
const nestedElement = document.querySelector('nav ul li:first-child');
const attributeSelector = document.querySelector('input[type="email"]');
5. querySelectorAll() - Most Powerful
// Returns NodeList (array-like with forEach)
const allDivs = document.querySelectorAll('div');
const multipleClasses = document.querySelectorAll('.class1, .class2');
// NodeList has forEach method
allDivs.forEach((div, index) => {
console.log(`Div ${index + 1}:`, div.textContent);
});
// Convert to array if needed
const divsArray = [...allDivs];
Practical Selection Examples
// Real-world examples
const navigationLinks = document.querySelectorAll('nav a');
const formInputs = document.querySelectorAll('form input, form textarea');
const activeButtons = document.querySelectorAll('button.active');
const dataAttributes = document.querySelectorAll('[data-toggle="modal"]');
// Checking if elements exist
const sidebar = document.querySelector('#sidebar');
if (sidebar) {
// Safe to manipulate
sidebar.style.display = 'block';
}
Manipulating Elements
1. Changing Text Content
const element = document.getElementById('myElement');
// textContent - plain text only
element.textContent = 'New text content';
// innerHTML - can include HTML
element.innerHTML = '<strong>Bold text</strong>';
// innerText - respects styling (hidden elements)
element.innerText = 'Visible text only';
// Practical example
const counter = document.getElementById('counter');
let count = 0;
setInterval(() => {
counter.textContent = `Count: ${++count}`;
}, 1000);
2. Changing Styles
const element = document.querySelector('.my-element');
// Individual style properties
element.style.color = 'red';
element.style.backgroundColor = 'yellow';
element.style.fontSize = '20px';
element.style.margin = '10px';
// CSS properties with hyphens become camelCase
element.style.borderRadius = '5px';
element.style.fontWeight = 'bold';
// Setting multiple styles
Object.assign(element.style, {
width: '200px',
height: '100px',
border: '2px solid blue',
textAlign: 'center'
});
// Getting computed styles
const computedStyle = window.getComputedStyle(element);
console.log('Current color:', computedStyle.color);
3. Working with CSS Classes
const element = document.querySelector('.box');
// Add class
element.classList.add('active');
element.classList.add('highlight', 'bordered'); // Multiple classes
// Remove class
element.classList.remove('inactive');
// Toggle class
element.classList.toggle('hidden'); // Adds if not present, removes if present
// Check if class exists
if (element.classList.contains('active')) {
console.log('Element is active');
}
// Replace class
element.classList.replace('old-class', 'new-class');
// Practical example: Tab switching
function switchTab(tabId) {
// Remove active from all tabs
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
// Add active to selected tab
document.getElementById(tabId).classList.add('active');
}
Working with Attributes
1. Getting and Setting Attributes
const image = document.querySelector('img');
// Get attribute
const src = image.getAttribute('src');
const alt = image.getAttribute('alt');
// Set attribute
image.setAttribute('src', 'new-image.jpg');
image.setAttribute('alt', 'New image description');
// Check if attribute exists
if (image.hasAttribute('data-lazy')) {
console.log('Image has lazy loading');
}
// Remove attribute
image.removeAttribute('title');
// Direct property access (for standard attributes)
image.src = 'another-image.jpg';
image.alt = 'Another description';
2. Data Attributes
const element = document.querySelector('[data-user-id]');
// Get data attribute (kebab-case becomes camelCase)
const userId = element.dataset.userId;
const userName = element.dataset.userName;
// Set data attribute
element.dataset.status = 'active';
element.dataset.lastLogin = new Date().toISOString();
// HTML: <div data-user-id="123" data-user-name="john"></div>
// JavaScript access:
console.log(element.dataset.userId); // "123"
console.log(element.dataset.userName); // "john"
// Practical example: Store configuration
function setupWidget(element) {
const config = {
autoPlay: element.dataset.autoPlay === 'true',
speed: parseInt(element.dataset.speed) || 1000,
direction: element.dataset.direction || 'forward'
};
console.log('Widget config:', config);
}
Event Handling
1. addEventListener() - Recommended
const button = document.getElementById('myButton');
// Basic event listener
button.addEventListener('click', function() {
console.log('Button clicked!');
});
// Arrow function
button.addEventListener('click', () => {
console.log('Button clicked with arrow function!');
});
// Event with parameters
button.addEventListener('click', function(event) {
console.log('Event type:', event.type);
console.log('Target element:', event.target);
console.log('Mouse coordinates:', event.clientX, event.clientY);
});
// Multiple event listeners
button.addEventListener('mouseenter', () => console.log('Mouse entered'));
button.addEventListener('mouseleave', () => console.log('Mouse left'));
2. Common Events
const input = document.querySelector('#email');
const form = document.querySelector('#myForm');
// Input events
input.addEventListener('focus', () => console.log('Input focused'));
input.addEventListener('blur', () => console.log('Input lost focus'));
input.addEventListener('input', (e) => console.log('Input value:', e.target.value));
input.addEventListener('change', (e) => console.log('Input changed:', e.target.value));
// Form events
form.addEventListener('submit', function(e) {
e.preventDefault(); // Prevent default form submission
console.log('Form submitted');
// Process form data
const formData = new FormData(form);
for (let [key, value] of formData.entries()) {
console.log(key, value);
}
});
// Window events
window.addEventListener('load', () => console.log('Page loaded'));
window.addEventListener('resize', () => console.log('Window resized'));
window.addEventListener('scroll', () => console.log('Page scrolled'));
3. Event Delegation
// Instead of adding listeners to each item
const list = document.getElementById('myList');
list.addEventListener('click', function(e) {
// Check if clicked element is a list item
if (e.target.tagName === 'LI') {
console.log('List item clicked:', e.target.textContent);
e.target.classList.toggle('selected');
}
// Check for specific class
if (e.target.classList.contains('delete-btn')) {
e.target.parentElement.remove();
}
});
// Practical example: Dynamic content
function addListItem(text) {
const li = document.createElement('li');
li.innerHTML = `
${text}
<button class="delete-btn">Delete</button>
`;
list.appendChild(li);
}
4. Removing Event Listeners
function handleClick() {
console.log('Clicked!');
}
const button = document.querySelector('#myButton');
// Add listener
button.addEventListener('click', handleClick);
// Remove listener (must use same function reference)
button.removeEventListener('click', handleClick);
// One-time event listener
button.addEventListener('click', function handler() {
console.log('This will only run once');
button.removeEventListener('click', handler);
});
// Or use once option
button.addEventListener('click', () => {
console.log('This will only run once');
}, { once: true });
Creating and Removing Elements
1. Creating Elements
// Create element
const div = document.createElement('div');
const paragraph = document.createElement('p');
const image = document.createElement('img');
// Set properties
div.className = 'new-container';
div.id = 'dynamic-container';
paragraph.textContent = 'This is a new paragraph';
image.src = 'image.jpg';
image.alt = 'Dynamic image';
// Create with content
const heading = document.createElement('h2');
heading.innerHTML = 'Dynamic <em>Heading</em>';
// Create text node
const textNode = document.createTextNode('Plain text content');
2. Adding Elements to DOM
const container = document.getElementById('container');
const newDiv = document.createElement('div');
newDiv.textContent = 'New content';
// Append to end
container.appendChild(newDiv);
// Insert at beginning
container.insertBefore(newDiv, container.firstChild);
// Insert adjacent (modern approach)
container.insertAdjacentElement('beforebegin', newDiv); // Before container
container.insertAdjacentElement('afterbegin', newDiv); // Start of container
container.insertAdjacentElement('beforeend', newDiv); // End of container
container.insertAdjacentElement('afterend', newDiv); // After container
// Insert HTML string
container.insertAdjacentHTML('beforeend', '<p>New paragraph</p>');
3. Complex Element Creation
function createUserCard(user) {
// Create main container
const card = document.createElement('div');
card.className = 'user-card';
card.dataset.userId = user.id;
// Create and append avatar
const avatar = document.createElement('img');
avatar.src = user.avatar;
avatar.alt = `${user.name}'s avatar`;
avatar.className = 'avatar';
// Create info container
const info = document.createElement('div');
info.className = 'user-info';
// Create name
const name = document.createElement('h3');
name.textContent = user.name;
// Create email
const email = document.createElement('p');
email.textContent = user.email;
email.className = 'email';
// Create actions
const actions = document.createElement('div');
actions.className = 'actions';
const editBtn = document.createElement('button');
editBtn.textContent = 'Edit';
editBtn.className = 'btn btn-primary';
editBtn.onclick = () => editUser(user.id);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.className = 'btn btn-danger';
deleteBtn.onclick = () => deleteUser(user.id);
// Assemble the card
info.appendChild(name);
info.appendChild(email);
actions.appendChild(editBtn);
actions.appendChild(deleteBtn);
card.appendChild(avatar);
card.appendChild(info);
card.appendChild(actions);
return card;
}
// Usage
const user = { id: 1, name: 'John Doe', email: 'john@example.com', avatar: 'avatar.jpg' };
const userCard = createUserCard(user);
document.getElementById('users-container').appendChild(userCard);
4. Removing Elements
const element = document.getElementById('elementToRemove');
// Modern approach
element.remove();
// Traditional approach
element.parentNode.removeChild(element);
// Remove all children
const container = document.getElementById('container');
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// Or modern approach
container.innerHTML = '';
// Remove specific children
container.querySelectorAll('.temporary').forEach(el => el.remove());
Traversing the DOM
1. Parent-Child Relationships
const element = document.getElementById('myElement');
// Parent traversal
const parent = element.parentNode;
const parentElement = element.parentElement; // Only element nodes
const closestDiv = element.closest('div'); // First ancestor matching selector
// Child traversal
const children = element.children; // HTMLCollection of element children
const firstChild = element.firstElementChild;
const lastChild = element.lastElementChild;
const childCount = element.childElementCount;
// All child nodes (including text nodes)
const allChildren = element.childNodes;
const firstNode = element.firstChild;
const lastNode = element.lastChild;
2. Sibling Relationships
const element = document.getElementById('myElement');
// Next/Previous siblings (elements only)
const nextSibling = element.nextElementSibling;
const prevSibling = element.previousElementSibling;
// Next/Previous siblings (all nodes)
const nextNode = element.nextSibling;
const prevNode = element.previousSibling;
// Get all siblings
function getAllSiblings(element) {
const siblings = [];
let sibling = element.parentNode.firstElementChild;
while (sibling) {
if (sibling !== element) {
siblings.push(sibling);
}
sibling = sibling.nextElementSibling;
}
return siblings;
}
3. Practical Traversal Examples
// Find all nested lists
function findNestedLists(container) {
const lists = [];
function traverse(node) {
if (node.tagName === 'UL' || node.tagName === 'OL') {
lists.push(node);
}
for (let child of node.children) {
traverse(child);
}
}
traverse(container);
return lists;
}
// Navigate breadcrumb trail
function getBreadcrumbs(element) {
const breadcrumbs = [];
let current = element;
while (current && current !== document.body) {
const info = {
tagName: current.tagName,
id: current.id,
className: current.className
};
breadcrumbs.unshift(info);
current = current.parentElement;
}
return breadcrumbs;
}
// Find table cell coordinates
function getTableCellPosition(cell) {
const row = cell.parentElement;
const table = row.closest('table');
return {
row: Array.from(table.rows).indexOf(row),
col: Array.from(row.cells).indexOf(cell)
};
}
Forms and Input Handling
1. Form Element Access
const form = document.getElementById('myForm');
const inputs = form.elements; // All form controls
// Access by name
const emailInput = form.elements.email;
const passwordInput = form.elements.password;
// Access by index
const firstInput = form.elements[0];
// Get form data
function getFormData(form) {
const data = {};
for (let element of form.elements) {
if (element.name && element.type !== 'submit') {
if (element.type === 'checkbox') {
data[element.name] = element.checked;
} else if (element.type === 'radio') {
if (element.checked) {
data[element.name] = element.value;
}
} else {
data[element.name] = element.value;
}
}
}
return data;
}
2. Input Validation
const emailInput = document.getElementById('email');
const errorDiv = document.getElementById('email-error');
emailInput.addEventListener('input', function() {
const value = this.value;
const isValid = validateEmail(value);
if (isValid) {
this.classList.remove('invalid');
this.classList.add('valid');
errorDiv.textContent = '';
} else {
this.classList.remove('valid');
this.classList.add('invalid');
errorDiv.textContent = 'Please enter a valid email address';
}
});
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Real-time password strength
const passwordInput = document.getElementById('password');
const strengthMeter = document.getElementById('strength-meter');
passwordInput.addEventListener('input', function() {
const strength = calculatePasswordStrength(this.value);
updateStrengthMeter(strength);
});
function calculatePasswordStrength(password) {
let score = 0;
if (password.length >= 8) score++;
if (/[a-z]/.test(password)) score++;
if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
return score;
}
function updateStrengthMeter(strength) {
const levels = ['weak', 'fair', 'good', 'strong', 'very-strong'];
strengthMeter.className = `strength-meter ${levels[strength - 1] || 'weak'}`;
strengthMeter.textContent = levels[strength - 1] || 'weak';
}
3. Dynamic Form Generation
function createForm(fields) {
const form = document.createElement('form');
form.className = 'dynamic-form';
fields.forEach(field => {
const fieldContainer = document.createElement('div');
fieldContainer.className = 'field-container';
const label = document.createElement('label');
label.textContent = field.label;
label.htmlFor = field.name;
let input;
if (field.type === 'select') {
input = document.createElement('select');
field.options.forEach(option => {
const optionEl = document.createElement('option');
optionEl.value = option.value;
optionEl.textContent = option.text;
input.appendChild(optionEl);
});
} else if (field.type === 'textarea') {
input = document.createElement('textarea');
} else {
input = document.createElement('input');
input.type = field.type || 'text';
}
input.name = field.name;
input.id = field.name;
if (field.required) input.required = true;
if (field.placeholder) input.placeholder = field.placeholder;
fieldContainer.appendChild(label);
fieldContainer.appendChild(input);
form.appendChild(fieldContainer);
});
const submitBtn = document.createElement('button');
submitBtn.type = 'submit';
submitBtn.textContent = 'Submit';
form.appendChild(submitBtn);
return form;
}
// Usage
const formFields = [
{ name: 'firstName', label: 'First Name', type: 'text', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'age', label: 'Age', type: 'number' },
{ name: 'country', label: 'Country', type: 'select', options: [
{ value: 'us', text: 'United States' },
{ value: 'ca', text: 'Canada' },
{ value: 'uk', text: 'United Kingdom' }
]},
{ name: 'comments', label: 'Comments', type: 'textarea' }
];
const dynamicForm = createForm(formFields);
document.body.appendChild(dynamicForm);
Advanced DOM Techniques
1. Document Fragments for Performance
// Instead of multiple DOM manipulations
const container = document.getElementById('container');
// Inefficient: Multiple reflows
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
container.appendChild(div); // Each append triggers reflow
}
// Efficient: Single reflow
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
container.appendChild(fragment); // Single reflow
2. Template Elements
// HTML template
// <template id="user-template">
// <div class="user-card">
// <img class="avatar" src="" alt="">
// <h3 class="name"></h3>
// <p class="email"></p>
// </div>
// </template>
function createUserFromTemplate(user) {
const template = document.getElementById('user-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.avatar').src = user.avatar;
clone.querySelector('.avatar').alt = `${user.name}'s avatar`;
clone.querySelector('.name').textContent = user.name;
clone.querySelector('.email').textContent = user.email;
return clone;
}
// Usage
const users = [
{ name: 'John Doe', email: 'john@example.com', avatar: 'john.jpg' },
{ name: 'Jane Smith', email: 'jane@example.com', avatar: 'jane.jpg' }
];
const container = document.getElementById('users');
const fragment = document.createDocumentFragment();
users.forEach(user => {
fragment.appendChild(createUserFromTemplate(user));
});
container.appendChild(fragment);
3. Intersection Observer for Performance
// Lazy loading images
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
// Observe all lazy images
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
// Infinite scroll
const scrollObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMoreContent();
}
});
});
const loadTrigger = document.getElementById('load-trigger');
scrollObserver.observe(loadTrigger);
4. Custom Elements (Web Components)
class UserCard extends HTMLElement {
constructor() {
super();
this.innerHTML = `
<style>
.user-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin: 8px;
}
.avatar { width: 64px; height: 64px; border-radius: 50%; }
</style>
<div class="user-card">
<img class="avatar" src="" alt="">
<h3 class="name"></h3>
<p class="email"></p>
</div>
`;
}
connectedCallback() {
const name = this.getAttribute('name');
const email = this.getAttribute('email');
const avatar = this.getAttribute('avatar');
this.querySelector('.name').textContent = name;
this.querySelector('.email').textContent = email;
this.querySelector('.avatar').src = avatar;
this.querySelector('.avatar').alt = `${name}'s avatar`;
}
static get observedAttributes() {
return ['name', 'email', 'avatar'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'name') {
this.querySelector('.name').textContent = newValue;
} else if (name === 'email') {
this.querySelector('.email').textContent = newValue;
} else if (name === 'avatar') {
this.querySelector('.avatar').src = newValue;
}
}
}
customElements.define('user-card', UserCard);
// Usage in HTML: <user-card name="John Doe" email="john@example.com" avatar="john.jpg"></user-card>
Performance Optimization
1. Batching DOM Operations
// Bad: Multiple style changes trigger multiple reflows
const element = document.getElementById('myElement');
element.style.width = '200px';
element.style.height = '200px';
element.style.backgroundColor = 'red';
// Good: Batch style changes
element.style.cssText = 'width: 200px; height: 200px; background-color: red;';
// Or use CSS classes
element.className = 'styled-element';
// Good: Use requestAnimationFrame for animations
function animateElement() {
requestAnimationFrame(() => {
element.style.transform = `translateX(${position}px)`;
if (position < targetPosition) {
position += speed;
animateElement();
}
});
}
2. Virtual Scrolling for Large Lists
class VirtualList {
constructor(container, itemHeight, itemCount, renderItem) {
this.container = container;
this.itemHeight = itemHeight;
this.itemCount = itemCount;
this.renderItem = renderItem;
this.scrollTop = 0;
this.containerHeight = container.clientHeight;
this.init();
}
init() {
// Create scrollable area
this.scrollArea = document.createElement('div');
this.scrollArea.style.height = `${this.itemCount * this.itemHeight}px`;
this.scrollArea.style.position = 'relative';
// Create visible items container
this.itemsContainer = document.createElement('div');
this.itemsContainer.style.position = 'absolute';
this.itemsContainer.style.top = '0';
this.itemsContainer.style.left = '0';
this.itemsContainer.style.right = '0';
this.scrollArea.appendChild(this.itemsContainer);
this.container.appendChild(this.scrollArea);
// Handle scroll
this.container.addEventListener('scroll', () => {
this.scrollTop = this.container.scrollTop;
this.updateVisibleItems();
});
this.updateVisibleItems();
}
updateVisibleItems() {
const startIndex = Math.floor(this.scrollTop / this.itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(this.containerHeight / this.itemHeight) + 1,
this.itemCount
);
// Clear existing items
this.itemsContainer.innerHTML = '';
// Render visible items
for (let i = startIndex; i < endIndex; i++) {
const item = this.renderItem(i);
item.style.position = 'absolute';
item.style.top = `${i * this.itemHeight}px`;
item.style.height = `${this.itemHeight}px`;
this.itemsContainer.appendChild(item);
}
}
}
// Usage
const container = document.getElementById('list-container');
const virtualList = new VirtualList(container, 50, 10000, (index) => {
const item = document.createElement('div');
item.textContent = `Item ${index}`;
item.className = 'list-item';
return item;
});
3. Debouncing and Throttling
// Debouncing and Throttling
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Throttle function
function throttle(func, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}
// Usage examples
const searchInput = document.getElementById('search');
const scrollContainer = document.getElementById('scroll-container');
// Debounced search - waits for user to stop typing
const debouncedSearch = debounce(function(query) {
console.log('Searching for:', query);
// Perform search API call
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// Throttled scroll - limits frequency of scroll handling
const throttledScroll = throttle(function() {
console.log('Scroll position:', window.scrollY);
// Update scroll-based UI elements
}, 100);
window.addEventListener('scroll', throttledScroll);
// Resize handler with debouncing
const debouncedResize = debounce(function() {
console.log('Window resized to:', window.innerWidth, window.innerHeight);
// Recalculate layouts
}, 250);
### 4. Mutation Observer for Dynamic Content
```javascript
// Watch for changes in the DOM
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
console.log('Element added:', node);
// Initialize new elements
initializeNewElement(node);
}
});
mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
console.log('Element removed:', node);
// Cleanup removed elements
cleanupElement(node);
}
});
}
if (mutation.type === 'attributes') {
console.log('Attribute changed:', mutation.attributeName);
console.log('Old value:', mutation.oldValue);
console.log('New value:', mutation.target.getAttribute(mutation.attributeName));
}
});
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true,
attributeFilter: ['class', 'data-status'] // Only watch specific attributes
});
// Stop observing when needed
// observer.disconnect();
function initializeNewElement(element) {
// Add event listeners to new elements
if (element.classList.contains('interactive')) {
element.addEventListener('click', handleInteractiveClick);
}
// Initialize plugins or components
if (element.classList.contains('slider')) {
initializeSlider(element);
}
}
function cleanupElement(element) {
// Remove event listeners and cleanup resources
if (element.classList.contains('interactive')) {
element.removeEventListener('click', handleInteractiveClick);
}
}
Best Practices
1. Error Handling and Defensive Programming
// Always check if elements exist
function safeElementOperation(elementId) {
const element = document.getElementById(elementId);
if (!element) {
console.warn(`Element with ID '${elementId}' not found`);
return null;
}
try {
// Perform operations
element.style.display = 'block';
return element;
} catch (error) {
console.error('Error manipulating element:', error);
return null;
}
}
// Safe event listener addition
function addSafeEventListener(selector, event, handler) {
const elements = document.querySelectorAll(selector);
if (elements.length === 0) {
console.warn(`No elements found for selector: ${selector}`);
return;
}
elements.forEach(element => {
try {
element.addEventListener(event, handler);
} catch (error) {
console.error(`Failed to add ${event} listener:`, error);
}
});
}
// Graceful degradation
function enhanceElement(element) {
if (!element || !element.classList) {
return; // Gracefully handle older browsers
}
// Modern browser enhancements
if ('IntersectionObserver' in window) {
// Use Intersection Observer if available
const observer = new IntersectionObserver(handleIntersection);
observer.observe(element);
} else {
// Fallback for older browsers
window.addEventListener('scroll', throttledScrollHandler);
}
}
2. Memory Management
class ComponentManager {
constructor() {
this.components = new Map();
this.eventListeners = new WeakMap();
}
createComponent(id, element) {
// Create component
const component = {
element,
cleanup: () => {
// Cleanup function
this.removeEventListeners(element);
if (element.parentNode) {
element.parentNode.removeChild(element);
}
}
};
this.components.set(id, component);
this.addEventListeners(element);
return component;
}
addEventListeners(element) {
const listeners = [];
const clickHandler = (e) => this.handleClick(e);
const mouseoverHandler = (e) => this.handleMouseover(e);
element.addEventListener('click', clickHandler);
element.addEventListener('mouseover', mouseoverHandler);
// Store listeners for cleanup
listeners.push(
{ event: 'click', handler: clickHandler },
{ event: 'mouseover', handler: mouseoverHandler }
);
this.eventListeners.set(element, listeners);
}
removeEventListeners(element) {
const listeners = this.eventListeners.get(element);
if (listeners) {
listeners.forEach(({ event, handler }) => {
element.removeEventListener(event, handler);
});
this.eventListeners.delete(element);
}
}
destroyComponent(id) {
const component = this.components.get(id);
if (component) {
component.cleanup();
this.components.delete(id);
}
}
destroyAll() {
this.components.forEach((component, id) => {
component.cleanup();
});
this.components.clear();
}
handleClick(e) {
console.log('Component clicked:', e.target);
}
handleMouseover(e) {
console.log('Component hovered:', e.target);
}
}
// Usage
const manager = new ComponentManager();
// Create components
const comp1 = manager.createComponent('comp1', document.getElementById('element1'));
const comp2 = manager.createComponent('comp2', document.getElementById('element2'));
// Cleanup when page unloads
window.addEventListener('beforeunload', () => {
manager.destroyAll();
});
3. Cross-Browser Compatibility
// Feature detection and polyfills
const DOMUtils = {
// Cross-browser event listener
addEvent: function(element, event, handler) {
if (element.addEventListener) {
element.addEventListener(event, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + event, handler);
} else {
element['on' + event] = handler;
}
},
// Cross-browser event removal
removeEvent: function(element, event, handler) {
if (element.removeEventListener) {
element.removeEventListener(event, handler, false);
} else if (element.detachEvent) {
element.detachEvent('on' + event, handler);
} else {
element['on' + event] = null;
}
},
// Cross-browser class manipulation
addClass: function(element, className) {
if (element.classList) {
element.classList.add(className);
} else {
const classes = element.className.split(' ');
if (classes.indexOf(className) === -1) {
classes.push(className);
element.className = classes.join(' ');
}
}
},
removeClass: function(element, className) {
if (element.classList) {
element.classList.remove(className);
} else {
const classes = element.className.split(' ');
const index = classes.indexOf(className);
if (index > -1) {
classes.splice(index, 1);
element.className = classes.join(' ');
}
}
},
hasClass: function(element, className) {
if (element.classList) {
return element.classList.contains(className);
} else {
return element.className.split(' ').indexOf(className) > -1;
}
},
// Cross-browser query selector
querySelector: function(selector) {
if (document.querySelector) {
return document.querySelector(selector);
} else {
// Fallback for older browsers
if (selector.startsWith('#')) {
return document.getElementById(selector.substring(1));
} else if (selector.startsWith('.')) {
return document.getElementsByClassName(selector.substring(1))[0];
} else {
return document.getElementsByTagName(selector)[0];
}
}
}
};
4. Performance Best Practices
// Efficient DOM manipulation patterns
const PerformanceUtils = {
// Batch DOM reads and writes
batchDOMOperations: function(elements, operations) {
// Batch all reads first
const measurements = elements.map(el => ({
element: el,
rect: el.getBoundingClientRect(),
computedStyle: window.getComputedStyle(el)
}));
// Then batch all writes
measurements.forEach(({ element, rect, computedStyle }, index) => {
const operation = operations[index];
if (operation) {
operation(element, rect, computedStyle);
}
});
},
// Efficient list rendering
renderLargeList: function(container, items, renderItem, chunkSize = 100) {
let index = 0;
function renderChunk() {
const fragment = document.createDocumentFragment();
let count = 0;
while (count < chunkSize && index < items.length) {
fragment.appendChild(renderItem(items[index], index));
index++;
count++;
}
container.appendChild(fragment);
if (index < items.length) {
setTimeout(renderChunk, 0); // Allow UI to update
}
}
renderChunk();
},
// Optimized scroll handler
optimizedScrollHandler: function(callback) {
let ticking = false;
return function() {
if (!ticking) {
requestAnimationFrame(() => {
callback();
ticking = false;
});
ticking = true;
}
};
},
// Element visibility checker
isElementVisible: function(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= windowHeight &&
rect.right <= windowWidth
);
}
};
5. Modern DOM APIs and Techniques
// Modern DOM manipulation techniques
class ModernDOMUtils {
// Efficient element creation with template literals
static createElement(template, data = {}) {
const div = document.createElement('div');
div.innerHTML = template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return data[key] || '';
});
return div.firstElementChild;
}
// Batch style updates
static batchStyles(element, styles) {
const cssText = Object.entries(styles)
.map(([prop, value]) => `${prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${value}`)
.join('; ');
element.style.cssText += cssText;
}
// Async DOM operations
static async waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
return;
}
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}, timeout);
});
}
// Modern event delegation
static delegate(container, selector, event, handler) {
container.addEventListener(event, (e) => {
const target = e.target.closest(selector);
if (target && container.contains(target)) {
handler.call(target, e);
}
});
}
// Smooth animations
static animate(element, keyframes, options = {}) {
if (element.animate) {
return element.animate(keyframes, {
duration: 300,
easing: 'ease-in-out',
...options
});
} else {
// Fallback for older browsers
return this.fallbackAnimate(element, keyframes, options);
}
}
static fallbackAnimate(element, keyframes, options) {
const duration = options.duration || 300;
const startTime = Date.now();
const startStyles = {};
const endStyles = keyframes[keyframes.length - 1];
// Get starting styles
Object.keys(endStyles).forEach(prop => {
startStyles[prop] = window.getComputedStyle(element)[prop];
});
function step() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
Object.keys(endStyles).forEach(prop => {
const startValue = parseFloat(startStyles[prop]);
const endValue = parseFloat(endStyles[prop]);
const currentValue = startValue + (endValue - startValue) * progress;
element.style[prop] = currentValue + (endStyles[prop].replace(/[\d.-]/g, '') || 'px');
});
if (progress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
}
// Usage examples
ModernDOMUtils.waitForElement('#dynamic-content')
.then(element => {
console.log('Element loaded:', element);
ModernDOMUtils.animate(element, [
{ opacity: 0, transform: 'translateY(20px)' },
{ opacity: 1, transform: 'translateY(0)' }
], { duration: 500 });
})
.catch(error => console.error(error));
// Delegate click events to all buttons
ModernDOMUtils.delegate(document.body, 'button', 'click', function(e) {
console.log('Button clicked:', this.textContent);
});
Common Patterns and Solutions
1. Modal Dialog Implementation
class Modal {
constructor(modalId) {
this.modal = document.getElementById(modalId);
this.overlay = this.modal.querySelector('.modal-overlay');
this.closeButtons = this.modal.querySelectorAll('[data-close]');
this.isOpen = false;
this.init();
}
init() {
// Close button handlers
this.closeButtons.forEach(btn => {
btn.addEventListener('click', () => this.close());
});
// Overlay click to close
this.overlay.addEventListener('click', (e) => {
if (e.target === this.overlay) {
this.close();
}
});
// Escape key to close
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
});
}
open() {
this.modal.classList.add('active');
document.body.classList.add('modal-open');
this.isOpen = true;
// Focus management
this.previouslyFocused = document.activeElement;
this.modal.focus();
// Trap focus within modal
this.trapFocus();
}
close() {
this.modal.classList.remove('active');
document.body.classList.remove('modal-open');
this.isOpen = false;
// Restore focus
if (this.previouslyFocused) {
this.previouslyFocused.focus();
}
}
trapFocus() {
const focusableElements = this.modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
this.modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
lastFocusable.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastFocusable) {
firstFocusable.focus();
e.preventDefault();
}
}
}
});
}
}
// Usage
const confirmModal = new Modal('confirm-modal');
document.getElementById('open-modal').addEventListener('click', () => {
confirmModal.open();
});
2. Drag and Drop Implementation
class DragDrop {
constructor(container) {
this.container = container;
this.draggedElement = null;
this.placeholder = null;
this.init();
}
init() {
this.container.addEventListener('dragstart', this.handleDragStart.bind(this));
this.container.addEventListener('dragover', this.handleDragOver.bind(this));
this.container.addEventListener('drop', this.handleDrop.bind(this));
this.container.addEventListener('dragend', this.handleDragEnd.bind(this));
// Make items draggable
this.container.querySelectorAll('.draggable').forEach(item => {
item.draggable = true;
});
}
handleDragStart(e) {
if (!e.target.classList.contains('draggable')) return;
this.draggedElement = e.target;
e.target.classList.add('dragging');
// Create placeholder
this.placeholder = document.createElement('div');
this.placeholder.className = 'drag-placeholder';
this.placeholder.style.height = e.target.offsetHeight + 'px';
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.outerHTML);
}
handleDragOver(e) {
if (!this.draggedElement) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const afterElement = this.getDragAfterElement(this.container, e.clientY);
if (afterElement == null) {
this.container.appendChild(this.placeholder);
} else {
this.container.insertBefore(this.placeholder, afterElement);
}
}
handleDrop(e) {
if (!this.draggedElement) return;
e.preventDefault();
// Replace placeholder with dragged element
this.container.insertBefore(this.draggedElement, this.placeholder);
this.placeholder.remove();
// Trigger custom event
this.container.dispatchEvent(new CustomEvent('itemMoved', {
detail: {
element: this.draggedElement,
newIndex: Array.from(this.container.children).indexOf(this.draggedElement)
}
}));
}
handleDragEnd(e) {
if (this.draggedElement) {
this.draggedElement.classList.remove('dragging');
this.draggedElement = null;
}
if (this.placeholder && this.placeholder.parentNode) {
this.placeholder.remove();
}
}
getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.draggable:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
}
// Usage
const sortableList = new DragDrop(document.getElementById('sortable-list'));
sortableList.container.addEventListener('itemMoved', (e) => {
console.log('Item moved:', e.detail.element.textContent, 'to position', e.detail.newIndex);
});
Conclusion
This comprehensive guide covers the JavaScript DOM from basic concepts to advanced techniques. The DOM is a powerful interface that allows you to create dynamic, interactive web applications. Key takeaways:
- Start with the basics: Understanding element selection and manipulation
- Learn event handling: The foundation of interactive web applications
- Master traversal: Navigate the DOM tree efficiently
- Optimize performance: Use techniques like batching, throttling, and virtual scrolling
- Handle errors gracefully: Always check for element existence and handle edge cases
- Stay modern: Use newer APIs while maintaining backward compatibility
- Think about accessibility: Ensure your DOM manipulations don't break screen readers
- Memory management: Clean up event listeners and references to prevent memory leaks
Remember that the DOM is just one part of web development. Combine these techniques with CSS for styling and modern frameworks for complex applications. Practice these examples and experiment with your own implementations to master DOM manipulation.
The examples provided are production-ready and follow modern best practices. Use them as building blocks for your own projects, and always consider the user experience when manipulating the DOM.
