ποΈ Creating and Removing Elements
So far you've found and changed elements that already existed in the HTML. Now you become the architect: building brand-new nodes in JavaScript, slotting them precisely into the page, and tearing them down again. This is how a search box fills with results, a cart gains items, and a feed grows as you scroll.
Week 2 · Day 2 (Tuesday: DOM Manipulation) · Lecture 3
π― Learning Objectives
By the end of this lesson, you will be able to:
- Create elements and text nodes with
createElementandcreateTextNode - Assemble a multi-part component and insert it into the page
- Choose the right insertion method:
append,prepend,before,after, andinsertAdjacentHTML - Batch many insertions efficiently with a
DocumentFragment - Clone reusable markup from a
<template>withcloneNode - Remove elements cleanly with
remove()and understand why event delegation matters
Estimated Time: 65 minutes
Practice: Build a small list that adds and removes items dynamically.
In This Lesson
The Build-Insert-Remove Cycle
Dynamic UI follows a simple rhythm. You create a node in memory, configure it (text, classes, attributes), insert it into the live page where the user can see it, and eventually remove it when it's no longer needed. A node you've created but not yet inserted exists only in JavaScript β it isn't on screen until you attach it to the document.
Understanding that "created but not attached" limbo is the key insight of this lesson: it's exactly what makes DocumentFragment and templates fast.
Creating Elements
document.createElement(tag) makes a fresh, empty element in memory. You then set its content and properties before inserting it.
// Create and configure a single element:
const div = document.createElement('div');
div.textContent = 'Hello, World!';
div.className = 'message';
div.id = 'welcome-message';
// Nothing is on screen yet β div lives only in memory
// until you attach it to the document (next section).
// Build a small nested structure by assembling children:
const card = document.createElement('div');
card.className = 'card';
const header = document.createElement('div');
header.className = 'card-header';
header.textContent = 'Card Title';
const body = document.createElement('div');
body.className = 'card-body';
body.textContent = 'Card content goes here';
card.append(header, body); // card now contains both children
// Pure text (no element) via createTextNode:
const p = document.createElement('p');
p.append(document.createTextNode('This is plain text'));
A reusable component factory
Wrapping element creation in a function gives you a repeatable "component." Note we use textContent and properties β never innerHTML with data β so the card is safe even if product.name came from a user.
function createProductCard(product) {
const card = document.createElement('div');
card.className = 'product-card';
card.dataset.productId = product.id;
const title = document.createElement('h3');
title.className = 'product-title';
title.textContent = product.name; // safe
const price = document.createElement('div');
price.className = 'product-price';
price.textContent = `$${product.price.toFixed(2)}`;
const button = document.createElement('button');
button.type = 'button';
button.className = 'add-to-cart';
button.textContent = 'Add to Cart';
button.addEventListener('click', () => addToCart(product.id));
card.append(title, price, button); // one call, many children
return card;
}
// Usage:
const card = createProductCard({ id: 1, name: 'Headphones', price: 99.99 });
document.querySelector('.products-container').append(card);
π‘ append() beats appendChild()
The modern append() accepts multiple nodes and plain strings in one call: card.append(title, price, 'text'). The older appendChild() takes exactly one node and returns it. Prefer append() unless you specifically need that return value.
Inserting into the DOM
Creating an element is inert until you place it somewhere. Modern DOM gives you an intuitive set of positioning methods.
const container = document.querySelector('.container');
const el = document.createElement('div');
el.textContent = 'New Element';
// The four modern positioners:
container.append(el); // as the LAST child of container
container.prepend(el); // as the FIRST child of container
target.before(el); // as a sibling, just BEFORE target
target.after(el); // as a sibling, just AFTER target
// Replace an element entirely:
oldElement.replaceWith(el);
Precise placement with insertAdjacentβ¦
When you need fine control relative to one element, the insertAdjacent* family takes a position keyword. The four keywords map onto the element like this:
<!-- beforebegin -->
<div class="target">
<!-- afterbegin -->
β¦ existing content β¦
<!-- beforeend -->
</div>
<!-- afterend -->
const target = document.querySelector('.target');
target.insertAdjacentElement('beforeend', el); // an element node
target.insertAdjacentText('afterbegin', 'Hi'); // plain text
// insertAdjacentHTML parses a string β SAFE only with trusted markup:
target.insertAdjacentHTML('beforeend', '<span class="badge">New</span>');
β οΈ insertAdjacentHTML carries the same XSS risk as innerHTML
It parses its string as HTML, so never build it from untrusted input. For user data, create elements and set textContent instead.
Batching with DocumentFragment
Every time you insert into the live DOM, the browser may recalculate layout β a "reflow." Insert a thousand items one at a time and you trigger up to a thousand reflows. A DocumentFragment is a lightweight, off-screen container: you fill it cheaply in memory, then attach it once, causing a single reflow.
// β Slow β inserts inside the loop, reflow after reflow:
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
document.querySelector('.list').append(div); // touches live DOM 1000Γ
}
// β
Fast β build off-screen, attach once:
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.append(div); // in memory, no reflow
}
document.querySelector('.list').append(fragment); // ONE reflow
Cloning Templates
When you need the same structure over and over, building it element-by-element each time is tedious. The <template> element lets you write the markup once in HTML β inert, never rendered β then stamp out copies with cloneNode(true).
<!-- In your HTML β this never renders on its own: -->
<template id="item-template">
<li class="list-item">
<span class="item-name" data-field="name"></span>
<button class="delete-btn">Delete</button>
</li>
</template>
const template = document.getElementById('item-template');
function createItem(data) {
// cloneNode(true) = deep copy, including all descendants:
const clone = template.content.cloneNode(true);
clone.querySelector('[data-field="name"]').textContent = data.name;
clone.querySelector('.delete-btn')
.addEventListener('click', () => console.log('delete', data.id));
return clone; // a fragment ready to append
}
document.querySelector('.list').append(createItem({ id: 1, name: 'Milk' }));
β Templates keep structure in HTML, logic in JS
Instead of hiding markup inside long JavaScript strings, the shape lives in your HTML where it's easy to read and edit, and your script only fills in the data. It's cleaner and safer than assembling HTML strings by hand.
Removing Elements
Taking a node out of the page is refreshingly simple with the modern API.
// Remove an element directly β no parent reference needed:
document.querySelector('.removable').remove();
// Remove every child of a container:
function clearChildren(parent) {
while (parent.firstChild) {
parent.firstChild.remove();
}
}
// (Setting parent.innerHTML = '' also clears it, but discards any
// event listeners and is less explicit about intent.)
// Remove all elements matching a condition:
function removeWhere(selector, predicate) {
document.querySelectorAll(selector).forEach(el => {
if (predicate(el)) el.remove();
});
}
removeWhere('.item', el => el.dataset.expired === 'true');
Removing with a fade-out
function removeWithFade(element, duration = 300) {
element.style.transition = `opacity ${duration}ms`;
element.style.opacity = '0';
// Wait for the transition to finish, THEN remove:
element.addEventListener('transitionend', () => element.remove(), { once: true });
}
β οΈ Listeners on removed nodes, and why delegation wins
When you remove an element, any event listeners attached directly to it are discarded with it β good. But if you constantly add and remove items, attaching a fresh listener to every one is wasteful and easy to leak. The robust pattern is event delegation: attach a single listener to a stable parent and inspect event.target. You'll go deep on this in the next lesson.
// One listener handles clicks for ALL current AND future .delete-btn:
document.querySelector('.list').addEventListener('click', (e) => {
if (e.target.matches('.delete-btn')) {
e.target.closest('.list-item').remove();
}
});
Practice & Quiz
ποΈ Exercise 1: Render a list efficiently
Goal: Write renderList(container, items) that turns an array of strings into <li> elements inside container, using a DocumentFragment so the whole list inserts in one reflow.
function renderList(container, items) {
// TODO: build all <li> in a fragment, then append once
}
renderList(document.querySelector('ul'), ['Alpha', 'Beta', 'Gamma']);
π‘ Hint
Create document.createDocumentFragment(). Loop the items, make an li, set textContent, append it to the fragment. After the loop, container.append(fragment) once.
β Solution
function renderList(container, items) {
const fragment = document.createDocumentFragment();
for (const text of items) {
const li = document.createElement('li');
li.textContent = text;
fragment.append(li);
}
container.append(fragment); // single reflow
}
ποΈ Exercise 2: An add/remove todo strip
Goal: Write addTodo(listEl, text) that appends an <li> containing the text and a Delete button which removes that <li> when clicked.
β Solution
function addTodo(listEl, text) {
const li = document.createElement('li');
li.className = 'todo';
const label = document.createElement('span');
label.textContent = text;
const del = document.createElement('button');
del.type = 'button';
del.textContent = 'Delete';
del.addEventListener('click', () => li.remove());
li.append(label, del);
listEl.append(li);
return li;
}
π― Quick Quiz
Question 1: After const d = document.createElement('div'), is d visible on the page?
Question 2: Why insert 1,000 elements through a DocumentFragment instead of one at a time?
Question 3: You add and remove list items constantly. What's the most robust way to handle their click events?
Best Practices & Pitfalls
β Do
- Use a
DocumentFragment(or one big string built safely) when inserting many nodes at once - Clone a
<template>for repeated structures instead of rebuilding them by hand - Set data with
textContentand properties, keeping user input out of parsed HTML - Prefer
element.remove()β no parent reference needed - Use event delegation for collections that grow and shrink
β Don't
- Insert inside a tight loop directly into the live DOM β batch instead
- Build HTML strings from untrusted input for
innerHTML/insertAdjacentHTML - Forget that a created element does nothing until it's inserted
- Attach a brand-new listener to every dynamically created element when delegation would do
β Clone from a template instead of hand-building
// β Rebuild every element, every time:
function makeCard(data) {
const card = document.createElement('div');
const title = document.createElement('h3');
// β¦many more linesβ¦
}
// β
Stamp a copy of markup written once in HTML:
const template = document.getElementById('card-template');
function makeCard(data) {
const clone = template.content.cloneNode(true);
clone.querySelector('.title').textContent = data.title;
return clone;
}
Summary
π Key Takeaways
createElementbuilds a node in memory; it's invisible until you insert it- Insert with the modern quartet:
append,prepend,before,afterβ plusinsertAdjacent*for fine control - A
DocumentFragmentbatches many insertions into a single reflow - Clone a
<template>withcloneNode(true)for repeated structures element.remove()takes a node out cleanly, no parent needed- Use event delegation so one listener serves an ever-changing list
π Additional Resources
π What's Next?
You can now build, place, and remove any part of the page. The final piece of interactivity is making it all respond to the user: Event Listeners and Handlers β clicks, input, keyboard, the event object, and the delegation pattern we just previewed.
π You're a DOM architect now!
Select, modify, create, remove β you hold every tool to reshape a page. Next, you'll wire it up to the user.