DOM, Events and Forms · Lesson 34 of 40
DOM Manipulation in JavaScript
When a browser loads HTML, it builds a tree of objects called the DOM (Document Object Model). JavaScript can read and change that tree, and the browser immediately updates the screen. Showing a notification, adding a row to a table, opening a menu – all of it is DOM manipulation. Frameworks like React do this for you, but they're built on exactly these APIs, and every front-end interview expects you to know them.
In this lesson
- The DOM tree
- Selecting elements with
querySelector - Changing text, HTML, classes, attributes and styles
- Creating, inserting and removing elements efficiently
The DOM tree
<body>
<header>
<h1 id="title">My Courses</h1>
</header>
<ul class="course-list">
<li class="course" data-id="1">JavaScript</li>
<li class="course" data-id="2">SQL</li>
</ul>
</body>
The same page as a DOM tree
document → html → body
- header→ h1#title → text "My Courses"
- ul.course-list→ li.course (data-id=1) "JavaScript" and li.course (data-id=2) "SQL"
Every tag becomes an element node; text inside it becomes a text node. Each element object has properties (textContent, className, children…) and methods (append, remove, addEventListener…).
Selecting elements
const title = document.querySelector("#title"); // first match, or null
const firstCourse = document.querySelector(".course");
const allCourses = document.querySelectorAll(".course"); // static NodeList
const sqlItem = document.querySelector('[data-id="2"]'); // any CSS selector works
const list = title.closest("body").querySelector(".course-list");
console.log(allCourses.length); // 2
allCourses.forEach((li) => console.log(li.textContent));
const names = [...allCourses].map((li) => li.textContent); // NodeList → array
| Method | Returns |
|---|---|
querySelector(css) | first matching element or null |
querySelectorAll(css) | a static NodeList of all matches (has forEach) |
getElementById(id) | element or null (fast, older style) |
el.closest(css) | nearest ancestor (or itself) that matches |
el.matches(css) | true/false |
Common mistake: null elements
If the selector matches nothing (a typo, or the script ran before the HTML loaded), querySelector returns null and the next line throws "Cannot read properties of null". Load scripts with defer or type="module", and check optional elements: document.querySelector(".banner")?.remove().
Changing content
const title = document.querySelector("#title");
title.textContent = "My Learning Path"; // plain text – safe
const box = document.querySelector("#welcome");
box.innerHTML = "<strong>Welcome back!</strong> You have <em>3</em> lessons left.";
const userName = '<img src=x onerror="alert(1)">'; // malicious input
box.textContent = `Hi ${userName}`; // shown as text, never executed
textContent vs innerHTML
textContent sets plain text – any < is displayed, not parsed. innerHTML parses HTML, so inserting user input with it opens an XSS hole. Rule: use textContent for data, and only use innerHTML with trusted, fixed markup.
Classes, attributes, dataset and styles
const card = document.querySelector(".course");
card.classList.add("active");
card.classList.remove("hidden");
card.classList.toggle("expanded"); // add if missing, remove if present
card.classList.toggle("done", progress === 100); // force on/off with a condition
console.log(card.classList.contains("active")); // true
const link = document.querySelector("a.docs");
link.setAttribute("href", "/learning/javascript");
link.setAttribute("aria-current", "page");
console.log(link.getAttribute("href"));
// data-* attributes ⇄ dataset (data-course-id → dataset.courseId)
console.log(card.dataset.id); // "1" – always a string
card.dataset.status = "completed"; // adds data-status="completed"
card.style.setProperty("--progress", "75%"); // set a CSS variable
card.hidden = true; // the hidden attribute
Prefer toggling classes to setting style directly: keep the look in CSS and let JavaScript switch states. That keeps styles in one place and makes dark mode, animations and responsive design easier.
Creating and inserting elements
function createCourseCard({ id, title, lessons }) {
const li = document.createElement("li");
li.className = "course";
li.dataset.id = id;
const name = document.createElement("strong");
name.textContent = title; // safe for any title
const meta = document.createElement("span");
meta.textContent = ` · ${lessons} lessons`;
li.append(name, meta);
return li;
}
const list = document.querySelector(".course-list");
list.append(createCourseCard({ id: 3, title: "React", lessons: 28 })); // at the end
list.prepend(createCourseCard({ id: 0, title: "HTML", lessons: 12 })); // at the start
list.querySelector('[data-id="2"]').before(createCourseCard({ id: 9, title: "Git", lessons: 8 }));
list.querySelector('[data-id="9"]').remove();
Result: the list now reads HTML, JavaScript, SQL, React.
Rendering many items efficiently
const courses = await (await fetch("/api/courses")).json();
const fragment = document.createDocumentFragment(); // an off-screen container
for (const course of courses) fragment.append(createCourseCard(course));
list.replaceChildren(fragment); // one update to the page
Why batch DOM updates
- Each insertion into the live pagecan trigger style and layout work
- Build in a fragment insteadno layout while you add items
- Insert oncereplaceChildren / append the fragment
- Browser lays out oncesmooth, even with hundreds of items
Templates
<template id="course-tpl">
<li class="course"><strong class="name"></strong> <span class="meta"></span></li>
</template>
const tpl = document.querySelector("#course-tpl");
function renderCourse(course) {
const node = tpl.content.cloneNode(true); // copy the template's markup
node.querySelector(".name").textContent = course.title;
node.querySelector(".meta").textContent = `${course.lessons} lessons`;
return node;
}
Traversing
const item = document.querySelector('[data-id="1"]');
item.parentElement; // the <ul>
item.children; // child elements (HTMLCollection)
item.nextElementSibling; // the SQL <li>
item.previousElementSibling; // null – it's the first
item.closest(".course-list") // nearest matching ancestor
Practice
Try it yourself
Open your browser's console on any page and run:
document.querySelectorAll("a").length, then change the page heading withdocument.querySelector("h1").textContent = "Edited!".Write
renderList(items)that turns an array of strings into<li>elements inside#todo-list, safely.Show solution
function renderList(items) { const ul = document.querySelector("#todo-list"); ul.replaceChildren(...items.map((text) => { const li = document.createElement("li"); li.textContent = text; return li; })); } renderList(["Revise closures", "Practise SQL joins"]);
Quick check
What does querySelector return when nothing matches?
null.
Why prefer textContent for user data?
It never parses HTML, so it can't run injected scripts (XSS).
How do you read data-course-id?
el.dataset.courseId – a string.
Summary
- The DOM is a live tree of objects; select with querySelector/querySelectorAll.
- Use textContent for data, classList for state, dataset for data-* attributes.
- Create with createElement, insert with append/prepend/before, batch with fragments.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.