Module 02: DOM Manipulation
Learning Focus: Document Object Model (DOM), dynamic HTML manipulation, and programmatic UI updates
Table of Contents
- Module Overview
- Core Concepts
- Code Walkthrough
- Testing Strategy
- Bugs Fixed
- 2021 vs 2025 Comparison
- Key Takeaways
Module Overview
What We're Building
This module introduces DOM manipulationβthe ability to dynamically create, modify, and delete HTML elements using JavaScript. We build helper functions that:
- Create new DOM elements
- Insert elements before existing ones
- Insert elements after existing ones
- Append elements to containers
Why DOM Manipulation Matters
Every modern web application needs to update the UI dynamically:
- Social Media: New posts appear without page reload
- E-commerce: Shopping cart updates instantly
- Gmail: Emails load and display dynamically
- Google Maps: Map pins added/removed on-the-fly
Without DOM manipulation, we'd need full page reloads for every changeβslow and jarring UX.
Core Concepts
1. What is the DOM?
The Document Object Model is a tree representation of HTML:
<!-- HTML -->
<div id="container">
<h1>Title</h1>
<p>Content</p>
</div>
DOM Tree:
div#container
βββ h1 (Title)
βββ p (Content)
JavaScript can traverse and modify this tree:
const container = document.getElementById('container');
container.innerHTML = '<h2>New Title</h2>'; // Modified!
2. Creating Elements
// Create a new element
const newDiv = document.createElement('div');
// Set attributes
newDiv.id = 'myDiv';
newDiv.className = 'card';
// Set content
newDiv.textContent = 'Hello World';
newDiv.innerHTML = '<strong>Bold text</strong>';
// Result:
// <div id="myDiv" class="card"><strong>Bold text</strong></div>
3. Insertion Methods
const parent = document.getElementById('container');
const newElement = document.createElement('p');
// Method 1: Append to end
parent.appendChild(newElement);
// Method 2: Insert before specific child
const referenceElement = parent.children[0];
parent.insertBefore(newElement, referenceElement);
// Method 3: Insert after (no native method!)
// This is why we build our own helper functions
4. Selecting Elements
// By ID (returns single element)
const el = document.getElementById('myId');
// By class (returns collection)
const els = document.getElementsByClassName('myClass');
// By CSS selector (returns first match)
const el = document.querySelector('.myClass');
// By CSS selector (returns all matches)
const els = document.querySelectorAll('.myClass');
Code Walkthrough
File Structure
02-dom/
βββ src/
β βββ dom.js # DOM manipulation functions
β βββ dom.test.js # Tests using jsdom
β βββ index.html # Visual testing page
βββ jest.config.js # Configure jsdom environment
βββ package.json
dom.js - Core DOM Functions
const domFunctions = {
/**
* Get a DOM element by ID
* @param {string} id - The element ID
* @returns {HTMLElement} The DOM element
*/
getById: (id) => {
return document.getElementById(id);
},
/**
* Create a new DOM element
* @param {string} type - Element type (div, p, span, etc.)
* @returns {HTMLElement} New element
*/
createNewDomElement: (type) => {
return document.createElement(type);
},
/**
* Set text content of an element
* @param {HTMLElement} element - Target element
* @param {string} text - Text to set
* @returns {HTMLElement} The modified element
*/
changeText: (element, text) => {
element.textContent = text;
return element;
},
/**
* Set ID attribute of an element
* @param {HTMLElement} element - Target element
* @param {string} id - ID to set
* @returns {HTMLElement} The modified element
*/
changeId: (element, id) => {
element.id = id;
return element;
},
/**
* Add a class to an element
* @param {HTMLElement} element - Target element
* @param {string} className - Class to add
* @returns {HTMLElement} The modified element
*/
addClass: (element, className) => {
element.classList.add(className);
return element;
},
/**
* Insert element BEFORE a reference element
* @param {HTMLElement} newElement - Element to insert
* @param {HTMLElement} referenceElement - Existing element
*/
addBefore: (newElement, referenceElement) => {
// β οΈ BUG WAS HERE: Logic was swapped with addAfter
referenceElement.parentNode.insertBefore(
newElement,
referenceElement
);
},
/**
* Insert element AFTER a reference element
* @param {HTMLElement} newElement - Element to insert
* @param {HTMLElement} referenceElement - Existing element
*/
addAfter: (newElement, referenceElement) => {
// β οΈ BUG WAS HERE: Logic was swapped with addBefore
referenceElement.parentNode.insertBefore(
newElement,
referenceElement.nextSibling
);
},
/**
* Append element as last child of parent
* @param {HTMLElement} newElement - Element to add
* @param {HTMLElement} parentElement - Parent container
*/
addLast: (newElement, parentElement) => {
parentElement.appendChild(newElement);
}
};
export default domFunctions;
Key Design Decisions:
Why return the element?
// Allows method chaining domFunctions.createNewDomElement('div') .changeText(element, 'Hello') .addClass(element, 'card');Why
insertBeforeforaddAfter?- JavaScript has
insertBefore()but noinsertAfter() - We simulate
addAfterby inserting before thenextSibling
// Insert after means: insert before the next element parent.insertBefore(newEl, referenceEl.nextSibling);- JavaScript has
Why use
classList.add()?// β GOOD: Preserves existing classes element.classList.add('new-class'); // β BAD: Overwrites existing classes element.className = 'new-class';
Testing Strategy
jest.config.js - Setting Up jsdom
module.exports = {
testEnvironment: 'jsdom', // Simulate browser DOM
transform: {
'^.+\\.jsx?$': 'babel-jest',
}
};
Why jsdom?
- Jest runs in Node.js (no browser)
- jsdom creates a fake DOM in memory
- We can test DOM operations without opening a browser
dom.test.js - Test Suite
import domFunctions from "./dom.js";
test("Does the dom functions work?", () => {
// Create container element
const parent = domFunctions.createNewDomElement('div');
domFunctions.changeId(parent, 'parent');
// Create first child
const first = domFunctions.createNewDomElement('p');
domFunctions.changeText(first, 'First');
domFunctions.changeId(first, 'first');
// Append first child to parent
domFunctions.addLast(first, parent);
// Verify structure
expect(parent.id).toBe('parent');
expect(parent.children.length).toBe(1);
expect(parent.children[0].textContent).toBe('First');
});
test("Does the addBefore function work?", () => {
// Setup: parent with one child
const parent = domFunctions.createNewDomElement('div');
const existing = domFunctions.createNewDomElement('p');
domFunctions.changeId(existing, 'existing');
domFunctions.addLast(existing, parent);
// Add new element BEFORE existing
const newElement = domFunctions.createNewDomElement('span');
domFunctions.changeId(newElement, 'new');
domFunctions.addBefore(newElement, existing);
// Verify order
expect(parent.children.length).toBe(2);
expect(parent.children[0].id).toBe('new'); // New is first
expect(parent.children[1].id).toBe('existing'); // Existing is second
});
test("Does the after function work?", () => {
// Setup: parent with one child
const parent = domFunctions.createNewDomElement('div');
const existing = domFunctions.createNewDomElement('p');
domFunctions.changeId(existing, 'existing');
domFunctions.addLast(existing, parent);
// Add new element AFTER existing
const newElement = domFunctions.createNewDomElement('span');
domFunctions.changeId(newElement, 'new');
domFunctions.addAfter(newElement, existing);
// Verify order
expect(parent.children.length).toBe(2);
expect(parent.children[0].id).toBe('existing'); // Existing is first
expect(parent.children[1].id).toBe('new'); // New is second
});
Testing Philosophy:
Each test follows the Arrange-Act-Assert pattern:
- Arrange: Set up DOM structure
- Act: Perform DOM operation
- Assert: Verify expected outcome
Bugs Fixed
Bug #1: Import Path Typo
// β WRONG: Incorrect filename
import domFunctions from "./domfunc.js";
// β
CORRECT: Match actual filename
import domFunctions from "./dom.js";
Impact: Tests couldn't find the module, all tests failed to run.
How it happened: Likely a typo when creating the test file.
Lesson: Always verify import paths match actual file names.
Bug #2: addBefore/addAfter Logic Swapped
This was the most interesting bugβthe logic was completely backwards!
// β WRONG: Functions did the opposite of their names
addBefore: (newElement, referenceElement) => {
// This actually inserts AFTER!
referenceElement.parentNode.insertBefore(
newElement,
referenceElement.nextSibling
);
},
addAfter: (newElement, referenceElement) => {
// This actually inserts BEFORE!
referenceElement.parentNode.insertBefore(
newElement,
referenceElement
);
}
// β
CORRECT: Swap the implementations
addBefore: (newElement, referenceElement) => {
referenceElement.parentNode.insertBefore(
newElement,
referenceElement
);
},
addAfter: (newElement, referenceElement) => {
referenceElement.parentNode.insertBefore(
newElement,
referenceElement.nextSibling
);
}
Visual representation of the bug:
Before Bug Fix:
Called addBefore() β Element appears AFTER
Called addAfter() β Element appears BEFORE
After Bug Fix:
Called addBefore() β Element appears BEFORE β
Called addAfter() β Element appears AFTER β
How to avoid this:
- Write tests first (TDD)
- Use descriptive variable names
- Add comments explaining the logic
2021 vs 2025 Comparison
DOM Manipulation Approaches
2021: Direct DOM Manipulation
// Old way - direct HTML strings
const container = document.getElementById('app');
container.innerHTML = `
<div class="card">
<h2>${title}</h2>
<p>${description}</p>
</div>
`;
Problems with 2021 approach:
- β Security risk (XSS attacks)
- β Loses event listeners
- β Destroys existing elements
- β No type safety
2025: Programmatic DOM Creation
// Modern way - create elements programmatically
const card = document.createElement('div');
card.className = 'card';
const title = document.createElement('h2');
title.textContent = titleText; // Safe from XSS
const desc = document.createElement('p');
desc.textContent = descriptionText;
card.appendChild(title);
card.appendChild(desc);
container.appendChild(card);
Benefits of 2025 approach:
- β XSS-safe (textContent escapes HTML)
- β Preserves event listeners
- β Fine-grained control
- β Better for testing
Modern Frameworks (React/Vue)
Why we still learn vanilla DOM: Even though React exists, understanding the DOM is crucial:
// React abstracts DOM manipulation
function Card({ title, description }) {
return (
<div className="card">
<h2>{title}</h2>
<p>{description}</p>
</div>
);
}
But React compiles to:
// Similar to what we write manually!
React.createElement('div', { className: 'card' },
React.createElement('h2', null, title),
React.createElement('p', null, description)
);
Understanding vanilla DOM helps you:
- Debug React issues
- Optimize performance
- Work with web components
- Understand what frameworks do under the hood
Testing Evolution
2021: Manual Browser Testing
// Old way - open browser, look at page
function testAddBefore() {
const parent = document.getElementById('test-container');
const existing = parent.children[0];
const newEl = document.createElement('span');
addBefore(newEl, existing);
// Manually inspect page in browser
console.log('Check if span is before existing element');
}
2025: Automated jsdom Testing
// Modern way - automated tests
test("addBefore inserts element correctly", () => {
const parent = document.createElement('div');
const existing = document.createElement('p');
parent.appendChild(existing);
const newEl = document.createElement('span');
addBefore(newEl, existing);
// Automated assertion
expect(parent.children[0]).toBe(newEl);
expect(parent.children[1]).toBe(existing);
});
Key Takeaways
Core Skills Developed
DOM Tree Navigation
element.parentNode // Go up element.children // Go down element.nextSibling // Go right element.previousSibling // Go leftElement Creation Pipeline
Create β Configure β InsertSafe Content Updates
// β Safe from XSS element.textContent = userInput; // β Dangerous! element.innerHTML = userInput;Testing DOM Code
- Use jsdom for unit tests
- Create minimal DOM structures
- Test insertion order carefully
Common Patterns
Pattern 1: Builder Functions
function createCard(title, content) {
const card = document.createElement('div');
card.className = 'card';
const h2 = document.createElement('h2');
h2.textContent = title;
const p = document.createElement('p');
p.textContent = content;
card.appendChild(h2);
card.appendChild(p);
return card;
}
Pattern 2: Bulk Operations
// Create multiple elements efficiently
const items = ['Apple', 'Banana', 'Cherry'];
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
fragment.appendChild(li);
});
// Single reflow/repaint
list.appendChild(fragment);
Pattern 3: Template Cloning
// Clone template instead of creating from scratch
const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.title').textContent = 'New Title';
container.appendChild(clone);
Performance Considerations
Minimize Reflows:
// β BAD: Multiple reflows
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
list.appendChild(li); // Reflow every time!
}
// β
GOOD: Single reflow
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
fragment.appendChild(li);
}
list.appendChild(fragment); // One reflow
Batch Style Changes:
// β BAD: Multiple style recalculations
element.style.width = '100px'; // Recalc
element.style.height = '200px'; // Recalc
element.style.color = 'red'; // Recalc
// β
GOOD: Single recalculation
element.style.cssText = 'width: 100px; height: 200px; color: red;';
Further Learning
Practice Exercises
Dynamic List Builder:
- Create function that builds unordered list from array
- Add click handlers to each item
- Implement delete functionality
Table Generator:
- Create table from 2D array
- Add sorting by column
- Implement cell editing
Modal Creator:
- Build reusable modal component
- Add open/close animations
- Handle keyboard (ESC to close)
Real-World Applications
- Todo Lists: Add/remove/edit items dynamically
- Forms: Add form fields on demand
- Galleries: Load images dynamically
- Comments: Add new comments without page reload
- Notifications: Show toast messages
Recommended Resources
Next Steps
Now that we understand DOM manipulation, we're ready for object-oriented programming and API integration in Module 03: Objects & API Integration β
Module Status: β
Complete (3/3 tests passing)
Key Bugs Fixed: 2 (import path, swapped logic)
Time Investment: ~2 hours
Key Skill: Understanding the foundation that powers all
frontend frameworks