The Power of Dynamic Content Manipulation
Welcome to our exploration of DOM content manipulation! Now that we know how to select elements from the Document Object Model, it's time to learn how to change them, which is where the true magic of JavaScript and web interactivity begins.
This lecture belongs to Week 4: Web Fundamentals and is part of our Python Full Stack Developer course. You'll find this content in the folder structure: /04week/04week_4day_c.html
In this session, we'll cover the various ways to modify DOM elements, from changing text and HTML content to updating attributes, manipulating styles, and creating entirely new elements from scratch.
Why Manipulate the DOM?
Before diving into the technical aspects, let's understand the purpose and power of DOM manipulation:
- Dynamic User Experiences: Update content without page refreshes
- Interactivity: Respond to user actions in real-time
- Data Visualization: Represent changing data visually
- Form Validation: Provide immediate feedback on user inputs
- Content Personalization: Tailor the interface to user preferences
- Progressive Enhancement: Add advanced functionality for capable browsers
Orchestra Conductor Metaphor: Think of DOM manipulation as conducting an orchestra. The HTML document provides the sheet music (structure), CSS sets the tone and mood (presentation), but JavaScript is the conductor who brings it all to life, directing when instruments play, adjusting volume, and even improvising new sections in response to the audience's reactions.
Changing Text Content
Let's start with one of the most common operations: modifying the text inside an element.
textContent
The textContent property sets or returns the text content of an element and all its descendants.
// HTML: <p id="greeting">Hello, World!</p>
// Getting text content
const greeting = document.getElementById('greeting');
console.log(greeting.textContent); // "Hello, World!"
// Setting text content
greeting.textContent = "Hello, JavaScript!";
// Now the paragraph displays: Hello, JavaScript!
Key features of textContent:
- Gets/sets text content of element and all descendant elements
- Ignores HTML tags within the content (treats them as text)
- Returns hidden elements' text (even if display:none)
- Preserves whitespace, line breaks, etc.
innerText
The innerText property is similar to textContent but with some important differences in how it handles content:
// HTML: <div id="content"><p>Visible text</p><p style="display:none">Hidden text</p></div>
const content = document.getElementById('content');
console.log(content.innerText); // "Visible text" (ignores hidden text)
console.log(content.textContent); // "Visible text Hidden text" (includes hidden text)
// Setting innerText
content.innerText = "New visible content";
// This replaces all inner elements with just text
Key features of innerText:
- Aware of CSS styling (ignores hidden text)
- Tries to preserve visual formatting
- May trigger reflow (performance implication)
- Not part of DOM standard (though widely supported)
When to use textContent vs. innerText
Use textContent when:
- You need all text regardless of visibility
- Performance is critical (doesn't trigger reflow)
- You want to preserve exactly what's in the DOM
Use innerText when:
- You want to get text as visually rendered
- You need to respect CSS styling in text retrieval
- You're building something that mirrors user-visible text
Book Metaphor: Think of textContent as getting all the text from a manuscript, including editor's notes and crossed-out sections. innerText is like reading only what's printed in the final published book, respecting the formatting and layout decisions.
Practical Example: Dynamic Greeting
// HTML:
// <h2 id="time-greeting">Good day!</h2>
function updateGreeting() {
const greeting = document.getElementById('time-greeting');
const hour = new Date().getHours();
let greetingText = "Good day!";
if (hour < 12) {
greetingText = "Good morning!";
} else if (hour < 18) {
greetingText = "Good afternoon!";
} else {
greetingText = "Good evening!";
}
greeting.textContent = greetingText;
}
// Call immediately and then update every hour
updateGreeting();
setInterval(updateGreeting, 3600000); // 3600000 ms = 1 hour
Changing HTML Content
Sometimes we need to insert structured content with HTML tags, not just plain text.
innerHTML
The innerHTML property gets or sets the HTML content inside an element.
// HTML: <div id="user-profile"><p>Loading profile...</p></div>
const profile = document.getElementById('user-profile');
// Getting innerHTML
console.log(profile.innerHTML); // "<p>Loading profile...</p>"
// Setting innerHTML with HTML structure
profile.innerHTML = `
<h3>User Profile</h3>
<img src="avatar.jpg" alt="User Avatar">
<p>Name: Alex Johnson</p>
<p>Role: Developer</p>
<a href="/profile/edit">Edit Profile</a>
`;
Security Warning: innerHTML can lead to cross-site scripting (XSS) vulnerabilities if used with untrusted content. Never use it with user input without proper sanitization!
// DANGEROUS - don't do this:
const userComment = userInput; // potentially malicious input
commentBox.innerHTML = userComment; // could execute scripts!
// SAFER:
// Option 1: Use textContent instead
commentBox.textContent = userComment;
// Option 2: Use a sanitization library
const sanitizedContent = DOMPurify.sanitize(userComment);
commentBox.innerHTML = sanitizedContent;
insertAdjacentHTML
A more precise way to insert HTML at specific positions relative to an element.
// HTML: <div id="container"><h2>Main Title</h2></div>
const container = document.getElementById('container');
// Insert HTML before the container element's opening tag
container.insertAdjacentHTML('beforebegin', '<p>Before the container</p>');
// Insert HTML after the container element's opening tag
container.insertAdjacentHTML('afterbegin', '<p>At the start of the container</p>');
// Insert HTML before the container element's closing tag
container.insertAdjacentHTML('beforeend', '<p>At the end of the container</p>');
// Insert HTML after the container element's closing tag
container.insertAdjacentHTML('afterend', '<p>After the container</p>');
The resulting HTML would look like:
<p>Before the container</p>
<div id="container">
<p>At the start of the container</p>
<h2>Main Title</h2>
<p>At the end of the container</p>
</div>
<p>After the container</p>
House Renovation Metaphor: If innerHTML is like gutting a room and completely redoing it, insertAdjacentHTML is like carefully adding fixtures in specific locations without disturbing the existing structure. The first approach is faster for complete overhauls, but the second is more precise and doesn't require rebuilding everything.
Practical Example: Dynamic Content Loading
// HTML:
// <div id="article-container">
// <button id="load-more">Load More Articles</button>
// </div>
document.getElementById('load-more').addEventListener('click', function() {
// In a real app, this would come from an API
const newArticles = [
{ title: "New Feature Released", content: "Exciting new features available now!" },
{ title: "Industry Trends", content: "See what's happening in tech this month." }
];
const container = document.getElementById('article-container');
// Create HTML for each new article
newArticles.forEach(article => {
const articleHTML = `
<article class="news-item">
<h3>${article.title}</h3>
<p>${article.content}</p>
<a href="#">Read more</a>
</article>
`;
// Insert before the "Load More" button
container.insertAdjacentHTML('beforeend', articleHTML);
});
});
Manipulating Attributes
Manipulating element attributes allows us to control an element's behavior, appearance, and relationships.
Standard Attribute Methods
// HTML: <img id="profile-pic" src="default.jpg" alt="Profile Picture">
const profilePic = document.getElementById('profile-pic');
// Get attribute value
const currentSrc = profilePic.getAttribute('src');
console.log(currentSrc); // "default.jpg"
// Set attribute value
profilePic.setAttribute('src', 'new-profile.jpg');
profilePic.setAttribute('alt', 'User Profile Picture');
// Check if attribute exists
const hasAlt = profilePic.hasAttribute('alt'); // true
// Remove attribute
profilePic.removeAttribute('data-temp');
These methods work with any attribute, including custom data attributes.
Direct Attribute Properties
Many common attributes can be accessed and modified directly as properties of the element object:
// HTML: <a id="main-link" href="https://example.com">Visit Example</a>
const link = document.getElementById('main-link');
// Get attribute via property
console.log(link.href); // "https://example.com"
// Set attribute via property
link.href = "https://newsite.com";
link.target = "_blank"; // Makes the link open in a new tab
link.title = "Go to New Site";
Note: Using properties sometimes gives different values than getAttribute:
// HTML: <a id="home" href="/home">Home</a>
const homeLink = document.getElementById('home');
console.log(homeLink.getAttribute('href')); // "/home" (exactly as written in HTML)
console.log(homeLink.href); // "https://example.com/home" (absolute URL)
Data Attributes
Custom data-* attributes provide a clean way to store extra information in HTML elements:
// HTML: <button id="product-btn" data-product-id="1234" data-category="electronics">Add to Cart</button>
const productBtn = document.getElementById('product-btn');
// Using getAttribute
const productId = productBtn.getAttribute('data-product-id'); // "1234"
// Using the dataset property (cleaner)
console.log(productBtn.dataset.productId); // "1234"
console.log(productBtn.dataset.category); // "electronics"
// Modifying data attributes
productBtn.dataset.price = "99.99";
productBtn.dataset.inStock = "true";
// Resulting HTML would now include:
// data-product-id="1234" data-category="electronics" data-price="99.99" data-in-stock="true"
Note: When accessing data attributes via the dataset property, the attribute names are converted from kebab-case to camelCase (e.g., data-product-id becomes dataset.productId).
Practical Example: Image Gallery
// HTML:
// <div id="current-image-container">
// <img id="current-image" src="default.jpg" alt="Gallery Image">
// </div>
// <div id="thumbnails">
// <img class="thumb" data-full-src="image1.jpg" src="thumb1.jpg" alt="Thumbnail 1">
// <img class="thumb" data-full-src="image2.jpg" src="thumb2.jpg" alt="Thumbnail 2">
// <img class="thumb" data-full-src="image3.jpg" src="thumb3.jpg" alt="Thumbnail 3">
// </div>
const thumbs = document.querySelectorAll('.thumb');
const currentImage = document.getElementById('current-image');
thumbs.forEach(thumb => {
thumb.addEventListener('click', function() {
// Update the main image src from the thumbnail's data attribute
const fullSrc = this.dataset.fullSrc;
currentImage.src = fullSrc;
// Update the alt text
currentImage.alt = this.alt;
// Add active class to selected thumbnail and remove from others
thumbs.forEach(t => t.classList.remove('active'));
this.classList.add('active');
});
});
Manipulating CSS and Styles
JavaScript gives us several ways to change an element's appearance dynamically.
The style Property
The most direct way to change inline styles of an element:
// HTML: <div id="highlight-box">Important content</div>
const box = document.getElementById('highlight-box');
// Set individual style properties
box.style.backgroundColor = 'yellow';
box.style.color = '#333';
box.style.padding = '10px';
box.style.borderRadius = '5px';
box.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
// Note: CSS property names are written in camelCase in JavaScript
// For example: background-color becomes backgroundColor
Important limitations:
- Only affects inline styles (high specificity)
- Can't access styles from stylesheets or computed styles
- Setting individual properties can be verbose
cssText for Multiple Styles
Set multiple styles at once with a single property:
// Set multiple styles at once
box.style.cssText = 'background-color: yellow; color: #333; padding: 10px; border-radius: 5px;';
// Or append to existing styles
box.style.cssText += 'margin-top: 20px; font-weight: bold;';
Note: Using cssText replaces all existing inline styles unless you append to it.
Working with Classes
Often a better approach is to toggle predefined CSS classes instead of setting inline styles:
// HTML: <div id="notification" class="info">This is an information message</div>
const notification = document.getElementById('notification');
// Add a class
notification.classList.add('visible');
// Remove a class
notification.classList.remove('info');
// Toggle a class (add if not present, remove if present)
notification.classList.toggle('expanded');
// Replace one class with another
notification.classList.replace('info', 'warning');
// Check if element has a class
if (notification.classList.contains('warning')) {
console.log('This is a warning notification');
}
// Get all classes as a DOMTokenList
console.log(notification.classList);
// Get all classes as a string
console.log(notification.className); // "warning visible"
// Set all classes at once (replaces existing classes)
notification.className = 'error urgent';
Benefits of using classes:
- Keeps presentation logic in CSS where it belongs
- Easier to maintain and update styles
- Better performance than inline styles for frequent changes
- Enables animation and transition effects
Wardrobe Metaphor: Think of CSS classes as complete outfits hanging in your closet. Rather than choosing individual garments every time (inline styles), you can simply switch between pre-coordinated outfits (classes) with a single action. This is faster, more consistent, and easier to update later when fashion trends change.
Reading Computed Styles
To read the actual applied styles (from stylesheets, not just inline):
// Get computed style object
const computedStyle = window.getComputedStyle(element);
// Read specific properties
const bgColor = computedStyle.backgroundColor;
const fontSize = computedStyle.fontSize;
// For pseudo-elements like ::before and ::after
const beforeContent = window.getComputedStyle(element, '::before').content;
Note: Computed style values are read-only; you can't modify them directly.
Practical Example: Theme Switcher
// HTML:
// <button id="theme-toggle">Switch to Dark Mode</button>
// <div id="content" class="light-theme">... content here ...</div>
// CSS (would be in a separate file):
// .light-theme { background-color: white; color: #333; }
// .dark-theme { background-color: #222; color: #eee; }
const themeToggle = document.getElementById('theme-toggle');
const content = document.getElementById('content');
themeToggle.addEventListener('click', function() {
// Toggle between themes
if (content.classList.contains('light-theme')) {
content.classList.replace('light-theme', 'dark-theme');
themeToggle.textContent = 'Switch to Light Mode';
// Save preference in localStorage
localStorage.setItem('theme', 'dark');
} else {
content.classList.replace('dark-theme', 'light-theme');
themeToggle.textContent = 'Switch to Dark Mode';
// Save preference in localStorage
localStorage.setItem('theme', 'light');
}
});
// Apply saved theme preference on page load
document.addEventListener('DOMContentLoaded', function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
content.classList.replace('light-theme', 'dark-theme');
themeToggle.textContent = 'Switch to Light Mode';
}
});
Creating and Adding Elements
Beyond modifying existing elements, JavaScript allows us to create entirely new elements and insert them into the DOM.
Creating Elements from Scratch
// Create a new element
const newParagraph = document.createElement('p');
// Add content to it
newParagraph.textContent = 'This paragraph was created with JavaScript.';
// Add attributes
newParagraph.id = 'dynamic-paragraph';
newParagraph.className = 'highlight info';
newParagraph.setAttribute('data-created', Date.now());
// Add styles
newParagraph.style.color = '#0066cc';
newParagraph.style.fontSize = '18px';
// Create a nested element
const link = document.createElement('a');
link.href = 'https://example.com';
link.textContent = 'Learn more';
// Add the link to the paragraph
newParagraph.appendChild(link);
At this point, we've created a complete element structure in memory, but it's not yet part of the document.
Adding Elements to the DOM
Several methods for inserting elements into the document:
// Get container to add the new element to
const container = document.getElementById('content-container');
// Method 1: Append to the end of container
container.appendChild(newParagraph);
// Method 2: Insert before a specific element
const existingElement = document.getElementById('existing-element');
container.insertBefore(newParagraph, existingElement);
// Method 3: Replace an existing element
container.replaceChild(newParagraph, existingElement);
// Method 4: Modern append/prepend methods
container.append(newParagraph); // Add to the end
container.prepend(newParagraph); // Add to the beginning
// Method 5: Insert adjacent to an element
existingElement.after(newParagraph); // After the element
existingElement.before(newParagraph); // Before the element
Note: The newer methods (append, prepend, before, after) aren't supported in older browsers but are more intuitive.
Using Document Fragments
When adding multiple elements, using a DocumentFragment improves performance:
// Create a document fragment (a lightweight container)
const fragment = document.createDocumentFragment();
// Create and add multiple elements to the fragment
for (let i = 1; i <= 100; i++) {
const listItem = document.createElement('li');
listItem.textContent = `Item ${i}`;
fragment.appendChild(listItem);
}
// Add the entire fragment to the DOM in one operation
document.getElementById('long-list').appendChild(fragment);
// This causes only one reflow/repaint instead of 100!
Construction Metaphor: Think of DocumentFragment as an off-site construction area where you assemble a complex structure before moving it into place. Rather than building directly on a busy street (the live DOM) and causing traffic disruptions with each small addition, you build the entire structure in a workshop and then move it into place in one efficient operation.
Cloning Existing Elements
Sometimes it's easier to clone an existing element than to create one from scratch:
// HTML: <div id="template-card" class="card">...card content...</div>
// Get the template element
const templateCard = document.getElementById('template-card');
// Clone it (the true parameter clones all descendants too)
const newCard = templateCard.cloneNode(true);
// Modify the clone
newCard.id = 'card-' + Date.now();
newCard.querySelector('.card-title').textContent = 'New Card';
newCard.querySelector('.card-image').src = 'new-image.jpg';
// Add the clone to the container
document.getElementById('card-container').appendChild(newCard);
Practical Example: Dynamic Todo List
// HTML:
// <form id="todo-form">
// <input type="text" id="todo-input" placeholder="Add a new task">
// <button type="submit">Add</button>
// </form>
// <ul id="todo-list"></ul>
document.getElementById('todo-form').addEventListener('submit', function(event) {
event.preventDefault(); // Prevent form submission
const input = document.getElementById('todo-input');
const todoText = input.value.trim();
if (todoText === '') return; // Don't add empty tasks
// Create new list item
const listItem = document.createElement('li');
listItem.className = 'todo-item';
// Create text span
const textSpan = document.createElement('span');
textSpan.textContent = todoText;
textSpan.className = 'todo-text';
// Create delete button
const deleteButton = document.createElement('button');
deleteButton.textContent = 'Delete';
deleteButton.className = 'delete-btn';
deleteButton.addEventListener('click', function() {
listItem.remove(); // Modern method to remove an element
});
// Create complete checkbox
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'complete-checkbox';
checkbox.addEventListener('change', function() {
if (this.checked) {
textSpan.classList.add('completed');
} else {
textSpan.classList.remove('completed');
}
});
// Assemble the list item
listItem.appendChild(checkbox);
listItem.appendChild(textSpan);
listItem.appendChild(deleteButton);
// Add to the list
document.getElementById('todo-list').appendChild(listItem);
// Clear the input
input.value = '';
input.focus();
});
Removing Elements
There are several ways to remove elements from the DOM:
Removal Methods
// Method 1: Using removeChild (traditional)
const parent = document.getElementById('container');
const childToRemove = document.getElementById('old-element');
parent.removeChild(childToRemove);
// Method 2: Using remove (modern)
document.getElementById('old-element').remove();
// Method 3: Replace with nothing
parent.replaceChild(document.createTextNode(''), childToRemove);
// Method 4: Clear all children
while (parent.firstChild) {
parent.removeChild(parent.firstChild);
}
// Method 5: Clear all children (modern)
parent.innerHTML = '';
// Method 6: Hide instead of remove (if you might need it later)
document.getElementById('old-element').style.display = 'none';
Important note: Using innerHTML = '' is simple but can cause memory leaks if you had event listeners attached to the removed elements. The other methods are safer.
Practical Example: Filtered List
// HTML:
// <div id="filter-controls">
// <button data-filter="all" class="active">All</button>
// <button data-filter="active">Active</button>
// <button data-filter="completed">Completed</button>
// </div>
// <ul id="task-list">
// <li class="task-item" data-status="active">Task 1</li>
// <li class="task-item" data-status="completed">Task 2</li>
// <li class="task-item" data-status="active">Task 3</li>
// </ul>
const filterButtons = document.querySelectorAll('#filter-controls button');
const taskItems = document.querySelectorAll('.task-item');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
const filter = this.dataset.filter;
// Update active button
filterButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// Show/hide tasks based on filter
taskItems.forEach(item => {
if (filter === 'all' || item.dataset.status === filter) {
item.style.display = ''; // Reset to default display
} else {
item.style.display = 'none';
}
});
});
});
Traversing the DOM
Once you have a reference to an element, you can navigate to related elements without re-querying the DOM:
Parent, Child, and Sibling Relationships
// Start with a reference element
const listItem = document.querySelector('li.selected');
// Navigate to parent
const list = listItem.parentElement; // or parentNode
// Navigate to siblings
const previousItem = listItem.previousElementSibling; // or previousSibling
const nextItem = listItem.nextElementSibling; // or nextSibling
// Navigate to children
const firstChild = list.firstElementChild; // or firstChild
const lastChild = list.lastElementChild; // or lastChild
const allChildren = list.children; // HTMLCollection of all direct children
// Navigate to all descendants
const allDescendants = list.querySelectorAll('*');
Element vs. Node: Properties with "Element" in the name skip text nodes and only give element nodes. Without "Element" in the name, they include all node types (elements, text, comments).
Family Tree Metaphor: DOM traversal is like navigating a family tree. From any person (element), you can find their parent, their children, their siblings, their ancestors, and their descendants. Just as you might ask a family member about their relatives instead of consulting the full family records, traversing from a known element is often more efficient than querying the entire DOM.
Practical Example: Accordion Menu
// HTML:
// <div class="accordion">
// <div class="accordion-item">
// <button class="accordion-header">Section 1</button>
// <div class="accordion-content">Content for section 1...</div>
// </div>
// <div class="accordion-item">
// <button class="accordion-header">Section 2</button>
// <div class="accordion-content">Content for section 2...</div>
// </div>
// </div>
document.querySelectorAll('.accordion-header').forEach(header => {
header.addEventListener('click', function() {
// Toggle active class on the header
this.classList.toggle('active');
// Get the content panel using traversal (it's the next sibling)
const content = this.nextElementSibling;
// Toggle the content visibility
if (content.style.maxHeight) {
content.style.maxHeight = null;
} else {
content.style.maxHeight = content.scrollHeight + 'px';
}
// Optional: Close other sections
const allItems = this.parentElement.parentElement.children;
for (let item of allItems) {
// Skip the current item
if (item === this.parentElement) continue;
// Get header and content of other items
const otherHeader = item.querySelector('.accordion-header');
const otherContent = item.querySelector('.accordion-content');
// Close them
otherHeader.classList.remove('active');
otherContent.style.maxHeight = null;
}
});
});
Performance Considerations
DOM manipulation can be expensive in terms of performance. Here are some best practices:
- Minimize DOM Operations: Batch changes when possible
- Use Document Fragments: For adding multiple elements
- Cache DOM References: Store elements you use repeatedly in variables
- Avoid Frequent Layout Changes: Group read operations and write operations
- Use Classes Instead of Many Style Changes: Toggle predefined classes
- Be Careful with Expensive Properties: Accessing certain properties (like offsetWidth) can trigger layout recalculations
Performance Example: Bad vs. Good Approach
// BAD APPROACH (causes many reflows)
for (let i = 0; i < 100; i++) {
const newItem = document.createElement('div');
newItem.textContent = `Item ${i}`;
document.getElementById('container').appendChild(newItem); // Each append causes reflow
console.log(newItem.offsetHeight); // Forces layout calculation
}
// GOOD APPROACH (single reflow)
const fragment = document.createDocumentFragment();
const container = document.getElementById('container');
for (let i = 0; i < 100; i++) {
const newItem = document.createElement('div');
newItem.textContent = `Item ${i}`;
fragment.appendChild(newItem);
}
container.appendChild(fragment); // Single reflow
Traffic Metaphor: Think of DOM operations like road construction. Making many small changes during rush hour (while the page is rendering) creates traffic jams (poor performance). It's better to bundle all your changes into a single construction project, complete it quickly, and then let traffic flow normally again.
Real-World Applications
Form Validation and Feedback
// HTML:
// <form id="registration-form">
// <div class="form-group">
// <label for="email">Email:</label>
// <input type="email" id="email" name="email">
// <div class="error-message" id="email-error"></div>
// </div>
// <div class="form-group">
// <label for="password">Password:</label>
// <input type="password" id="password" name="password">
// <div class="error-message" id="password-error"></div>
// </div>
// <button type="submit">Register</button>
// </form>
document.getElementById('registration-form').addEventListener('submit', function(e) {
let isValid = true;
// Clear previous errors
document.querySelectorAll('.error-message').forEach(el => {
el.textContent = '';
el.style.display = 'none';
});
// Validate email
const emailInput = document.getElementById('email');
const emailError = document.getElementById('email-error');
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(emailInput.value)) {
emailError.textContent = 'Please enter a valid email address';
emailError.style.display = 'block';
emailInput.classList.add('invalid');
isValid = false;
} else {
emailInput.classList.remove('invalid');
emailInput.classList.add('valid');
}
// Validate password
const passwordInput = document.getElementById('password');
const passwordError = document.getElementById('password-error');
if (passwordInput.value.length < 8) {
passwordError.textContent = 'Password must be at least 8 characters';
passwordError.style.display = 'block';
passwordInput.classList.add('invalid');
isValid = false;
} else {
passwordInput.classList.remove('invalid');
passwordInput.classList.add('valid');
}
// Prevent form submission if invalid
if (!isValid) {
e.preventDefault();
}
});
Infinite Scroll Implementation
// HTML:
// <div id="posts-container"></div>
// <div id="loading-indicator" style="display: none;">Loading...</div>
let page = 1;
let isLoading = false;
// Function to load more content
function loadMorePosts() {
if (isLoading) return;
isLoading = true;
document.getElementById('loading-indicator').style.display = 'block';
// In a real app, this would be an API call
fetch(`/api/posts?page=${page}`)
.then(response => response.json())
.then(data => {
const container = document.getElementById('posts-container');
const fragment = document.createDocumentFragment();
data.posts.forEach(post => {
const postElement = document.createElement('article');
postElement.className = 'post';
postElement.innerHTML = `
<h2>${post.title}</h2>
<p class="meta">By ${post.author} on ${post.date}</p>
<div class="content">${post.excerpt}</div>
<a href="/post/${post.id}" class="read-more">Read more</a>
`;
fragment.appendChild(postElement);
});
container.appendChild(fragment);
page++;
isLoading = false;
document.getElementById('loading-indicator').style.display = 'none';
})
.catch(error => {
console.error('Error loading posts:', error);
isLoading = false;
document.getElementById('loading-indicator').style.display = 'none';
});
}
// Check if scrolled near bottom
window.addEventListener('scroll', function() {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) {
loadMorePosts();
}
});
// Initial load
loadMorePosts();
Modern Alternatives to Direct DOM Manipulation
While direct DOM manipulation is a fundamental skill, modern web development often uses higher-level abstractions:
Template Literals and Interpolation
// Create HTML with template literals
function createUserCard(user) {
return `
<div class="user-card" id="user-${user.id}">
<img src="${user.avatar}" alt="${user.name}">
<h3>${user.name}</h3>
<p>${user.email}</p>
<button data-user-id="${user.id}" class="contact-btn">Contact</button>
</div>
`;
}
// Use it to add content
document.getElementById('users-container').innerHTML =
users.map(user => createUserCard(user)).join('');
Virtual DOM Approaches
Libraries like React use a virtual DOM to optimize actual DOM manipulations:
// React example
function UserCard({ user }) {
return (
<div className="user-card" id={`user-${user.id}`}>
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => contactUser(user.id)}>Contact</button>
</div>
);
}
// React handles the DOM updates efficiently
function UserList({ users }) {
return (
<div className="users-container">
{users.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
}
Blueprint Metaphor: If direct DOM manipulation is like manually constructing a building brick by brick, the virtual DOM approach is like working with a detailed blueprint. You make all your changes to the blueprint (virtual DOM), then the framework (like React) figures out the most efficient way to update the actual building (real DOM) to match your new design, minimizing construction disruption.
Practice Exercises
Exercise 1: Dynamic Content Tabs
Create a tabbed interface where clicking on different tabs displays different content panels. This exercise will practice:
- Modifying content
- Changing classes
- Event handling
- DOM traversal
Exercise 2: Interactive Form Builder
Create a drag-and-drop form builder that allows users to add different types of input fields to a form. This exercise will practice:
- Creating elements
- Setting attributes
- Appending to the DOM
- Working with event listeners
Exercise 3: DOM Manipulation Utility Library
Build a small utility library with functions that simplify common DOM manipulation tasks, such as:
- A function to toggle multiple classes
- A function to create and append elements with attributes in one step
- A function to easily replace elements
- A function to animate element changes (opacity, height, etc.)
Lecture Summary
Today we've explored the world of DOM content manipulation, the core of dynamic web interactivity. We've covered:
- Changing text content with
textContentandinnerText - Modifying HTML structure with
innerHTMLandinsertAdjacentHTML - Working with element attributes through methods and properties
- Manipulating CSS and classes for styling changes
- Creating, adding, and removing elements
- Traversing the DOM to access related elements
- Performance considerations for efficient DOM manipulation
- Real-world applications and modern alternatives
With these skills, you can create dynamic, interactive web experiences that respond to user actions in real-time without requiring page refreshes. In our next session, we'll build on this foundation to explore event handling in depth, allowing us to respond to user interactions like clicks, keypresses, and form submissions.