DOM Manipulation: Creating & Modifying Elements
Introduction to DOM Manipulation
In the previous lesson, you learned how to find and select elements in the DOM. Now it is time to learn how to create, modify, and remove elements. DOM manipulation is what transforms a static HTML page into a dynamic, interactive web application. Every time you see a new comment appear without a page reload, a notification pop up, a list item get added or removed, or a loading spinner appear -- that is DOM manipulation at work.
JavaScript gives you complete control over the DOM. You can create entirely new elements, change their text and HTML content, modify their attributes and styles, move them around in the document tree, clone them, and remove them entirely. Mastering these techniques is essential for building modern web interfaces.
createElement -- Building New Elements
The document.createElement() method creates a new HTML element in memory. The element is not yet part of the visible page -- it exists only in JavaScript's memory until you explicitly add it to the DOM tree. This two-step process (create, then attach) gives you the opportunity to configure the element fully before the browser needs to render it, which is better for performance.
Example: Creating Elements with createElement
// Create a new paragraph element
const paragraph = document.createElement('p');
console.log(paragraph); // <p></p> (empty, not yet in the DOM)
// Create a new div
const div = document.createElement('div');
// Create a new button
const button = document.createElement('button');
// Create a new image
const img = document.createElement('img');
// Create a new anchor link
const link = document.createElement('a');
// Create a new list item
const listItem = document.createElement('li');
// The element exists in memory but is NOT visible on the page yet
// You must add it to the DOM tree to make it appear
createTextNode -- Creating Text Content
The document.createTextNode() method creates a text node that you can append to an element. While you can often use textContent to set text directly, createTextNode() is useful when you need to insert text alongside other nodes or when building complex element structures programmatically.
Example: Creating and Appending Text Nodes
// Create a text node
const text = document.createTextNode('Hello, World!');
// Create a paragraph and add the text to it
const paragraph = document.createElement('p');
paragraph.appendChild(text);
// paragraph is now: <p>Hello, World!</p>
// You can create multiple text nodes in one element
const greeting = document.createElement('p');
const part1 = document.createTextNode('Welcome, ');
const bold = document.createElement('strong');
bold.textContent = 'John';
const part2 = document.createTextNode('! Glad to see you.');
greeting.appendChild(part1);
greeting.appendChild(bold);
greeting.appendChild(part2);
// Result: <p>Welcome, <strong>John</strong>! Glad to see you.</p>
appendChild -- Adding Elements to the DOM
The appendChild() method adds a node as the last child of a parent element. This is the classic way to insert elements into the DOM. It takes one argument -- the node to append -- and returns the appended node. If the node you are appending already exists in the DOM, appendChild() will move it from its current position to the new location rather than cloning it.
Example: Using appendChild to Build a List
// Get the existing unordered list
const list = document.getElementById('todo-list');
// Create a new list item
const newItem = document.createElement('li');
newItem.textContent = 'Buy groceries';
newItem.classList.add('todo-item');
// Append it as the last child of the list
list.appendChild(newItem);
// Create and append multiple items
const tasks = ['Clean house', 'Walk the dog', 'Read a book'];
tasks.forEach(function(task) {
const item = document.createElement('li');
item.textContent = task;
item.classList.add('todo-item');
list.appendChild(item);
});
// Moving an existing element (not cloning!)
const firstItem = list.children[0];
list.appendChild(firstItem);
// The first item is now the LAST item -- it was moved, not copied
insertBefore -- Inserting at a Specific Position
The insertBefore() method inserts a node before a specified reference node within a parent. It takes two arguments: the new node to insert and the reference node that should come after it. If the reference node is null, insertBefore() behaves like appendChild() and adds the element at the end.
Example: Using insertBefore
const list = document.getElementById('todo-list');
// Create a new priority item
const urgentItem = document.createElement('li');
urgentItem.textContent = 'URGENT: Fix production bug';
urgentItem.classList.add('todo-item', 'urgent');
// Insert it before the first item in the list
const firstItem = list.children[0];
list.insertBefore(urgentItem, firstItem);
// The urgent item is now the FIRST item in the list
// Insert before a specific item (the third child)
const newItem = document.createElement('li');
newItem.textContent = 'Review pull requests';
const thirdItem = list.children[2];
list.insertBefore(newItem, thirdItem);
// newItem is now at position 2 (before the old third item)
// When reference is null, it appends at the end
const lastItem = document.createElement('li');
lastItem.textContent = 'End of list item';
list.insertBefore(lastItem, null);
// Same as list.appendChild(lastItem)
Modern Insertion: append, prepend, after, before
Modern JavaScript provides four convenient methods that are easier to use than appendChild() and insertBefore(). These methods accept multiple arguments and can take both elements and strings (which are automatically converted to text nodes). They do not return the inserted node, unlike appendChild().
Example: Modern Insertion Methods
const container = document.getElementById('container');
// append() -- adds to the END of an element (like appendChild but better)
const p1 = document.createElement('p');
p1.textContent = 'First paragraph';
container.append(p1);
// append() can take multiple arguments at once!
const p2 = document.createElement('p');
p2.textContent = 'Second paragraph';
const p3 = document.createElement('p');
p3.textContent = 'Third paragraph';
container.append(p2, p3); // Both added at the end
// append() can also take plain strings (auto-converted to text nodes)
container.append('Some trailing text');
// prepend() -- adds to the BEGINNING of an element
const heading = document.createElement('h1');
heading.textContent = 'Welcome';
container.prepend(heading);
// heading is now the FIRST child of container
// before() -- inserts BEFORE the element (as a sibling)
const notice = document.createElement('div');
notice.textContent = 'Important notice';
container.before(notice);
// notice is now a sibling that appears right before container
// after() -- inserts AFTER the element (as a sibling)
const footer = document.createElement('footer');
footer.textContent = 'Page footer';
container.after(footer);
// footer is now a sibling that appears right after container
// All four methods accept multiple arguments
const divA = document.createElement('div');
const divB = document.createElement('div');
container.prepend(divA, divB, 'Some text');
append() and appendChild() is that append() accepts strings and multiple arguments, while appendChild() only accepts a single Node. Also, appendChild() returns the appended node while append() returns undefined. For new code, append() and prepend() are generally preferred for their flexibility.replaceChild and replaceWith
Sometimes you need to replace one element with another. The older replaceChild() method is called on the parent and takes two arguments: the new node and the old node to replace. The modern replaceWith() method is called directly on the element you want to replace, which is more intuitive.
Example: Replacing Elements
// Using replaceChild (older approach -- called on parent)
const list = document.getElementById('todo-list');
const oldItem = list.children[1]; // Second item
const newItem = document.createElement('li');
newItem.textContent = 'Updated task';
newItem.classList.add('todo-item', 'updated');
list.replaceChild(newItem, oldItem);
// oldItem is removed, newItem takes its place
// Using replaceWith (modern approach -- called on the element itself)
const heading = document.querySelector('h1');
const newHeading = document.createElement('h2');
newHeading.textContent = 'New Heading';
newHeading.id = 'main-title';
heading.replaceWith(newHeading);
// The h1 is gone, replaced by the h2
// replaceWith accepts multiple nodes and strings
const oldParagraph = document.querySelector('.old-content');
const span1 = document.createElement('span');
span1.textContent = 'Part 1';
const span2 = document.createElement('span');
span2.textContent = 'Part 2';
oldParagraph.replaceWith(span1, ' and ', span2);
// The paragraph is replaced with: <span>Part 1</span> and <span>Part 2</span>
removeChild and remove
To remove elements from the DOM, you have two options. The older removeChild() method is called on the parent element and takes the child to remove as an argument. The modern remove() method is called directly on the element you want to remove -- no need to reference the parent.
Example: Removing Elements
// Using removeChild (older approach)
const list = document.getElementById('todo-list');
const itemToRemove = list.children[0]; // First item
list.removeChild(itemToRemove);
// Using remove (modern approach -- much simpler!)
const notification = document.querySelector('.notification');
notification.remove();
// The notification is gone from the DOM
// Remove all children of an element
const container = document.getElementById('container');
// Method 1: Loop with removeChild
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// Method 2: Set innerHTML to empty string (faster but has caveats)
container.innerHTML = '';
// Method 3: replaceChildren with no arguments (modern and clean)
container.replaceChildren();
// Conditionally remove elements
const errors = document.querySelectorAll('.error-message');
errors.forEach(function(error) {
if (error.textContent.trim() === '') {
error.remove(); // Remove empty error containers
}
});
remove() on an element, the JavaScript variable still holds a reference to the removed node. The element is gone from the page, but it still exists in memory. You can even re-insert it into the DOM later using appendChild() or append(). If you truly want to discard the element, set the variable to null to allow garbage collection.cloneNode -- Duplicating Elements
The cloneNode() method creates a copy of an element. It takes one boolean argument: true for a deep clone (copies the element and all its descendants) or false for a shallow clone (copies only the element itself without its children). Cloning is useful when you want to duplicate a template element multiple times.
Example: Cloning Elements
// Original element:
// <div class="card" id="template-card">
// <h3 class="card-title">Title</h3>
// <p class="card-body">Body text</p>
// </div>
const originalCard = document.getElementById('template-card');
// Shallow clone -- only the outer div, no children
const shallowCopy = originalCard.cloneNode(false);
console.log(shallowCopy.innerHTML); // "" (empty, no children)
console.log(shallowCopy.className); // "card" (attributes are copied)
// Deep clone -- the div AND all its children
const deepCopy = originalCard.cloneNode(true);
console.log(deepCopy.innerHTML);
// <h3 class="card-title">Title</h3><p class="card-body">Body text</p>
// Important: Remove the id to avoid duplicates!
deepCopy.removeAttribute('id');
// Customize the clone
deepCopy.querySelector('.card-title').textContent = 'New Card';
deepCopy.querySelector('.card-body').textContent = 'New content here.';
// Add the clone to the page
document.getElementById('card-container').appendChild(deepCopy);
// Create multiple cards from a template
const cardData = [
{ title: 'Card 1', body: 'First card content' },
{ title: 'Card 2', body: 'Second card content' },
{ title: 'Card 3', body: 'Third card content' },
];
cardData.forEach(function(data) {
const card = originalCard.cloneNode(true);
card.removeAttribute('id');
card.querySelector('.card-title').textContent = data.title;
card.querySelector('.card-body').textContent = data.body;
document.getElementById('card-container').appendChild(card);
});
cloneNode() copies the element's attributes (including id) but does not copy event listeners added with addEventListener(). Inline event handlers in HTML attributes (like onclick="...") are copied because they are attributes. Always remove or change the id on cloned elements to avoid having duplicate IDs in your document.innerHTML vs textContent vs innerText
These three properties all deal with the content of an element, but they behave very differently. Understanding when to use each one is critical for both functionality and security.
innerHTML gets or sets the HTML markup inside an element. It parses any HTML tags in the string and creates the corresponding DOM elements. This is powerful but carries security risks if you insert user-provided data without sanitization.
textContent gets or sets the raw text content of an element and all its descendants. It does not parse HTML -- any tags in the string are treated as literal text. It returns the text of hidden elements too, and is the safest option for inserting user data.
innerText is similar to textContent but is aware of CSS styling. It only returns text that is visually rendered (it skips hidden elements), and it respects CSS properties like text-transform. Accessing innerText triggers a reflow, making it slower than textContent.
Example: Comparing innerHTML, textContent, and innerText
// Given this HTML:
// <div id="example">
// <p>Hello <strong>World</strong></p>
// <p style="display: none;">Hidden text</p>
// </div>
const el = document.getElementById('example');
// innerHTML -- returns the raw HTML markup
console.log(el.innerHTML);
// '<p>Hello <strong>World</strong></p><p style="display: none;">Hidden text</p>'
// textContent -- returns ALL text, including hidden elements
console.log(el.textContent);
// 'Hello World\nHidden text'
// innerText -- returns only VISIBLE text
console.log(el.innerText);
// 'Hello World' (hidden paragraph is excluded)
// Setting content with innerHTML (parses HTML)
el.innerHTML = '<h2>New Title</h2><p>New paragraph</p>';
// The div now contains an h2 and a p element
// Setting content with textContent (escapes HTML -- SAFE)
el.textContent = '<h2>This is NOT a heading</h2>';
// The div now contains the literal text "<h2>This is NOT a heading</h2>"
// The tags are visible as text, not parsed as HTML
// SECURITY: Never use innerHTML with user input!
const userInput = '<script>alert("hacked!")</script>';
el.innerHTML = userInput; // DANGEROUS -- script could execute!
el.textContent = userInput; // SAFE -- displayed as plain text
innerHTML to insert user-provided content directly into the page. This creates a Cross-Site Scripting (XSS) vulnerability where attackers can inject malicious scripts. Always use textContent for user-generated text, or sanitize HTML content before inserting it with innerHTML.Setting Attributes: setAttribute and dataset
Elements have attributes like id, class, href, src, and custom data-* attributes. JavaScript provides several ways to read and modify these attributes. The setAttribute() and getAttribute() methods work with any attribute, while the dataset property provides a convenient interface specifically for data-* attributes.
Example: Working with Attributes
const link = document.createElement('a');
// setAttribute -- sets any attribute
link.setAttribute('href', 'https://example.com');
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
link.setAttribute('class', 'external-link');
link.setAttribute('id', 'my-link');
// getAttribute -- reads an attribute value
console.log(link.getAttribute('href')); // "https://example.com"
console.log(link.getAttribute('target')); // "_blank"
// hasAttribute -- checks if an attribute exists
console.log(link.hasAttribute('target')); // true
console.log(link.hasAttribute('download')); // false
// removeAttribute -- removes an attribute
link.removeAttribute('target');
console.log(link.hasAttribute('target')); // false
// Direct property access (works for standard attributes)
link.href = 'https://newsite.com';
link.id = 'updated-link';
console.log(link.href); // "https://newsite.com"
// dataset -- the easy way to work with data-* attributes
const card = document.createElement('div');
card.setAttribute('data-user-id', '42');
card.setAttribute('data-role', 'admin');
card.setAttribute('data-is-active', 'true');
// Reading with dataset (note: camelCase conversion)
console.log(card.dataset.userId); // "42" (data-user-id becomes userId)
console.log(card.dataset.role); // "admin"
console.log(card.dataset.isActive); // "true"
// Setting with dataset
card.dataset.score = '100'; // Creates data-score="100"
card.dataset.lastLogin = '2024-01-15'; // Creates data-last-login="2024-01-15"
// Deleting a data attribute
delete card.dataset.isActive; // Removes data-is-active
dataset property automatically converts between the HTML format (data-user-id with hyphens) and the JavaScript format (userId in camelCase). When you set card.dataset.firstName = 'John', the HTML attribute becomes data-first-name="John". All dataset values are strings, so if you store numbers or booleans, you will need to convert them when reading.Modifying Styles: The style Property
Every DOM element has a style property that lets you set inline CSS styles directly from JavaScript. CSS property names are converted to camelCase in JavaScript: background-color becomes backgroundColor, font-size becomes fontSize, and so on. Setting a style this way adds it as an inline style on the element, which has high specificity and overrides most stylesheet rules.
Example: Modifying Inline Styles
const box = document.getElementById('my-box');
// Set individual styles
box.style.backgroundColor = '#3498db';
box.style.color = 'white';
box.style.padding = '20px';
box.style.borderRadius = '8px';
box.style.fontSize = '18px';
box.style.fontWeight = 'bold';
box.style.marginTop = '10px';
box.style.display = 'flex';
box.style.justifyContent = 'center';
box.style.alignItems = 'center';
// Read a computed style (what the browser actually renders)
const computedStyles = window.getComputedStyle(box);
console.log(computedStyles.backgroundColor); // "rgb(52, 152, 219)"
console.log(computedStyles.fontSize); // "18px"
// Remove an inline style by setting it to empty string
box.style.backgroundColor = '';
// The element reverts to whatever the stylesheet specifies
// Set multiple styles at once using cssText
box.style.cssText = 'background: red; color: white; padding: 10px;';
// WARNING: cssText replaces ALL existing inline styles!
// Better approach: use Object.assign for multiple styles
Object.assign(box.style, {
background: '#2ecc71',
color: 'white',
padding: '15px',
borderRadius: '5px'
});
classList -- Managing CSS Classes
Instead of manipulating inline styles directly, the professional approach is to define styles in CSS classes and toggle those classes with JavaScript. The classList property provides methods to add, remove, toggle, and check for CSS classes on an element. This keeps your styling in CSS where it belongs and makes your JavaScript cleaner and more maintainable.
Example: Using classList Methods
const button = document.querySelector('.action-btn');
// add() -- adds one or more classes
button.classList.add('active');
button.classList.add('primary', 'large'); // Multiple classes at once
// remove() -- removes one or more classes
button.classList.remove('inactive');
button.classList.remove('small', 'outlined'); // Multiple at once
// toggle() -- adds if missing, removes if present
button.classList.toggle('active');
// If button had "active", it is removed. If it did not, it is added.
// toggle() with force parameter
button.classList.toggle('active', true); // Always adds (like add)
button.classList.toggle('active', false); // Always removes (like remove)
// contains() -- checks if a class exists (returns true/false)
if (button.classList.contains('active')) {
console.log('Button is active');
} else {
console.log('Button is inactive');
}
// replace() -- replaces one class with another
button.classList.replace('primary', 'secondary');
// Removes "primary" and adds "secondary" in one step
// Practical: Toggle dark mode
const body = document.body;
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', function() {
body.classList.toggle('dark-mode');
// Update the button text based on current state
if (body.classList.contains('dark-mode')) {
themeToggle.textContent = 'Switch to Light Mode';
} else {
themeToggle.textContent = 'Switch to Dark Mode';
}
});
// Practical: Active navigation highlighting
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(function(link) {
link.addEventListener('click', function() {
// Remove active from all links
navLinks.forEach(function(l) {
l.classList.remove('active');
});
// Add active to the clicked link
this.classList.add('active');
});
});
classList over directly modifying the className property. Setting element.className = 'newClass' replaces all existing classes, which can accidentally remove classes added by other parts of your code. With classList.add() and classList.remove(), you only affect the specific classes you intend to change.insertAdjacentHTML -- Inserting HTML Strings
The insertAdjacentHTML() method parses a string of HTML and inserts the resulting nodes at a specified position relative to the element. It is faster than innerHTML for adding content because it does not reparse the existing content of the element. It takes two arguments: a position string and the HTML string to insert.
The four position values are:
- 'beforebegin' -- Before the element itself (as a previous sibling)
- 'afterbegin' -- Inside the element, before its first child
- 'beforeend' -- Inside the element, after its last child
- 'afterend' -- After the element itself (as a next sibling)
Example: Using insertAdjacentHTML
const container = document.getElementById('content');
// Insert before the element (as a sibling)
container.insertAdjacentHTML('beforebegin',
'<div class="alert">Notice: Content updated!</div>'
);
// Insert at the beginning of the element's content
container.insertAdjacentHTML('afterbegin',
'<h2>Latest Updates</h2>'
);
// Insert at the end of the element's content
container.insertAdjacentHTML('beforeend',
'<p>Last updated: January 2024</p>'
);
// Insert after the element (as a sibling)
container.insertAdjacentHTML('afterend',
'<footer>End of content</footer>'
);
// Practical: Adding a new row to a table
const tableBody = document.querySelector('#data-table tbody');
tableBody.insertAdjacentHTML('beforeend',
'<tr>' +
'<td>John Doe</td>' +
'<td>john@example.com</td>' +
'<td>Admin</td>' +
'</tr>'
);
// Building a notification system
function showNotification(message, type) {
const html = '<div class="notification ' + type + '">' +
'<span class="notification-text">' + message + '</span>' +
'<button class="notification-close">X</button>' +
'</div>';
document.getElementById('notification-area')
.insertAdjacentHTML('beforeend', html);
}
Document Fragments -- Batch DOM Operations
A DocumentFragment is a lightweight, minimal document object that can hold DOM nodes without being part of the active document tree. When you need to add many elements to the DOM, appending them one by one causes the browser to recalculate layout and repaint after each insertion. By building your elements inside a DocumentFragment first and then appending the fragment, the browser only needs to recalculate once. This dramatically improves performance for large batch insertions.
Example: Using DocumentFragment for Batch Insertion
// BAD: Appending items one by one (causes reflow on each append)
const list = document.getElementById('user-list');
const users = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve',
'Frank', 'Grace', 'Henry', 'Irene', 'Jack'];
// This triggers 10 separate reflows!
users.forEach(function(user) {
const li = document.createElement('li');
li.textContent = user;
list.appendChild(li); // Reflow happens here each time
});
// GOOD: Use a DocumentFragment (only 1 reflow)
const fragment = document.createDocumentFragment();
users.forEach(function(user) {
const li = document.createElement('li');
li.textContent = user;
li.classList.add('user-item');
fragment.appendChild(li); // No reflow -- fragment is not in the DOM
});
// Single append triggers only ONE reflow
list.appendChild(fragment);
// The fragment itself disappears; only its children are added
// Practical: Building a data table from an array
function buildTable(data) {
const fragment = document.createDocumentFragment();
data.forEach(function(row) {
const tr = document.createElement('tr');
const tdName = document.createElement('td');
tdName.textContent = row.name;
tr.appendChild(tdName);
const tdEmail = document.createElement('td');
tdEmail.textContent = row.email;
tr.appendChild(tdEmail);
const tdRole = document.createElement('td');
tdRole.textContent = row.role;
tr.appendChild(tdRole);
fragment.appendChild(tr);
});
document.querySelector('#data-table tbody').appendChild(fragment);
}
append() with multiple arguments to batch operations similarly, but DocumentFragment remains the standard pattern for large-scale insertions.Real-World Example: Dynamic Todo List Application
Let us combine everything we have learned into a complete, functional todo list application. This example uses createElement, classList, dataset, insertAdjacentHTML, event delegation, and element removal -- all the core DOM manipulation techniques working together.
Example: Complete Todo List with DOM Manipulation
// HTML Structure:
// <div id="todo-app">
// <h2>My Todo List</h2>
// <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>
// <p id="todo-count"></p>
// </div>
// Cache DOM references
const todoForm = document.getElementById('todo-form');
const todoInput = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');
const todoCount = document.getElementById('todo-count');
let taskId = 0;
// Function to create a new todo item
function createTodoItem(text) {
taskId++;
const li = document.createElement('li');
li.classList.add('todo-item');
li.dataset.id = taskId;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.classList.add('todo-checkbox');
const span = document.createElement('span');
span.classList.add('todo-text');
span.textContent = text;
const deleteBtn = document.createElement('button');
deleteBtn.classList.add('todo-delete');
deleteBtn.textContent = 'Delete';
deleteBtn.setAttribute('aria-label', 'Delete task: ' + text);
li.append(checkbox, span, deleteBtn);
return li;
}
// Function to update the task count
function updateCount() {
const total = todoList.children.length;
const completed = todoList.querySelectorAll('.completed').length;
todoCount.textContent = completed + ' of ' + total + ' tasks completed';
}
// Handle form submission
todoForm.addEventListener('submit', function(event) {
event.preventDefault();
const text = todoInput.value.trim();
if (text === '') return;
const item = createTodoItem(text);
todoList.appendChild(item);
todoInput.value = '';
todoInput.focus();
updateCount();
});
// Handle clicks using event delegation on the list
todoList.addEventListener('click', function(event) {
const todoItem = event.target.closest('.todo-item');
if (!todoItem) return;
// Handle checkbox toggle
if (event.target.classList.contains('todo-checkbox')) {
todoItem.classList.toggle('completed');
updateCount();
}
// Handle delete button
if (event.target.classList.contains('todo-delete')) {
todoItem.remove();
updateCount();
}
});
Real-World Example: Dynamic Card Grid
Here is another practical example that demonstrates creating a dynamic card layout from data. This pattern is common in e-commerce sites, dashboards, and content management systems where you need to render a collection of items from data fetched from an API.
Example: Rendering a Card Grid from Data
// Sample product data (simulating API response)
const products = [
{ id: 1, name: 'Wireless Headphones', price: 79.99,
category: 'electronics', inStock: true },
{ id: 2, name: 'Running Shoes', price: 129.99,
category: 'sports', inStock: true },
{ id: 3, name: 'Coffee Maker', price: 49.99,
category: 'kitchen', inStock: false },
{ id: 4, name: 'Yoga Mat', price: 29.99,
category: 'sports', inStock: true },
];
function createProductCard(product) {
const card = document.createElement('div');
card.classList.add('product-card');
card.dataset.productId = product.id;
card.dataset.category = product.category;
if (!product.inStock) {
card.classList.add('out-of-stock');
}
const title = document.createElement('h3');
title.classList.add('product-title');
title.textContent = product.name;
const price = document.createElement('p');
price.classList.add('product-price');
price.textContent = '$' + product.price.toFixed(2);
const badge = document.createElement('span');
badge.classList.add('stock-badge');
badge.textContent = product.inStock ? 'In Stock' : 'Out of Stock';
badge.classList.add(product.inStock ? 'badge-success' : 'badge-danger');
const button = document.createElement('button');
button.classList.add('add-to-cart');
button.textContent = 'Add to Cart';
if (!product.inStock) {
button.disabled = true;
button.textContent = 'Unavailable';
}
card.append(title, price, badge, button);
return card;
}
// Render all products using DocumentFragment
function renderProducts(productList) {
const container = document.getElementById('product-grid');
container.replaceChildren(); // Clear existing content
const fragment = document.createDocumentFragment();
productList.forEach(function(product) {
fragment.appendChild(createProductCard(product));
});
container.appendChild(fragment);
}
// Initial render
renderProducts(products);
// Filter by category
function filterByCategory(category) {
if (category === 'all') {
renderProducts(products);
} else {
const filtered = products.filter(function(p) {
return p.category === category;
});
renderProducts(filtered);
}
}
Summary of DOM Manipulation Methods
Here is a comprehensive reference of all DOM manipulation methods covered in this lesson:
- document.createElement(tag) -- Creates a new element in memory.
- document.createTextNode(text) -- Creates a new text node.
- parent.appendChild(node) -- Appends a node as the last child.
- parent.insertBefore(newNode, refNode) -- Inserts a node before a reference node.
- element.append(...nodes) -- Appends multiple nodes or strings at the end.
- element.prepend(...nodes) -- Inserts multiple nodes or strings at the beginning.
- element.after(...nodes) -- Inserts siblings after the element.
- element.before(...nodes) -- Inserts siblings before the element.
- parent.replaceChild(newNode, oldNode) -- Replaces one child with another.
- element.replaceWith(...nodes) -- Replaces the element with new nodes.
- parent.removeChild(node) -- Removes a child node from the parent.
- element.remove() -- Removes the element from the DOM.
- element.cloneNode(deep) -- Creates a shallow or deep copy of an element.
- element.innerHTML -- Gets or sets the HTML content (use with caution).
- element.textContent -- Gets or sets the text content (safe for user data).
- element.setAttribute(name, value) -- Sets an attribute on the element.
- element.dataset -- Access and modify
data-*attributes. - element.style -- Read and set inline CSS styles.
- element.classList -- Add, remove, toggle, and check CSS classes.
- element.insertAdjacentHTML(position, html) -- Insert parsed HTML at a specified position.
- document.createDocumentFragment() -- Create a lightweight container for batch DOM operations.
Practice Exercise
Build a dynamic contact list application using only JavaScript DOM manipulation. Create an HTML page with a form containing inputs for name, email, and phone number, plus an Add Contact button. Below the form, add an empty table with headers (Name, Email, Phone, Actions). Write JavaScript that accomplishes the following: (1) When the form is submitted, create a new table row using createElement and populate each cell with the form data using textContent. (2) Add a Delete button in the Actions column that removes the row when clicked using remove(). (3) Add an Edit button that replaces the text cells with input fields pre-filled with the current values, and changes the Edit button to a Save button. When Save is clicked, update the cells with the new values. (4) Use classList to add a highlight class when hovering over rows. (5) Use DocumentFragment to add five sample contacts when the page loads. (6) Add a search input above the table that filters visible rows by setting style.display based on whether the name contains the search text. (7) Use dataset attributes to store a unique ID on each row. Test all features and verify that adding, editing, deleting, and searching all work correctly.