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