02 DOM Manipulation
On this page (15 sections)
1. Selecting elements in the document#
Before you change anything on a page, you need a reference to the element. The three methods below cover almost every case in this practice set.
document.getElementById("someId"); // returns one element, or null
document.querySelector(".someClass"); // returns the first match, or null
document.querySelectorAll("a"); // returns a NodeList (array-like, not a real array)
querySelectorAll accepts any CSS selector, so document.querySelectorAll("h1, h2, h3") selects all three heading levels at once. A NodeList supports .forEach, but not methods like .map directly unless you convert it first with Array.from(nodeList).
Give elements an id in the HTML when you plan to target them individually (a specific <div> for output, a specific input field). Use a shared class or tag name when you plan to loop over several similar elements.
2. Writing content into the document#
There are two main ways to put text or HTML into an element.
element.textContent = "Hello world"; // treats the value as plain text
element.innerHTML = "<b>Hello</b>"; // parses the value as HTML
Use textContent whenever you are inserting plain text, since it is safer and does not risk the string being interpreted as markup. Use innerHTML only when you actually need to insert HTML structure (for example, a generated <img> tag or a table row).
To add a brand new element instead of overwriting existing content:
const p = document.createElement("p");
p.textContent = "Hello world";
document.body.appendChild(p);
appendChild adds the new element as the last child of the container. This pattern (create, set content, append) is the building block for generating repeated content: list items, table rows, headings, and so on.
To clear a container before filling it again (important for anything that refreshes, like a search-as-you-type list):
container.innerHTML = "";
3. Responding to events#
An event handler is a function that runs when something happens (a click, a page load, a key press). Attach handlers with addEventListener.
button.addEventListener("click", function () {
// code to run when the button is clicked
});
Common events used in this practice set:
| Event | Fires when |
|---|---|
click | An element (usually a button, link, or radio button) is clicked |
load | The page and its resources finish loading (attach to window) |
input | A text field’s value changes, on every keystroke |
change | A form control’s value changes and loses focus (or, for radio/checkbox, when selection changes) |
For code that must run once the page has loaded:
window.addEventListener("load", function () {
// setup code here
});
Alternatively, place your <script> tag at the end of the <body>, after the HTML elements it references, so the elements already exist when the script runs.
For text fields where you want to react to every keystroke (not just when the field loses focus), use input, not change:
searchBox.addEventListener("input", function () {
const typedText = searchBox.value;
// refresh results using typedText
});
4. Reading values from form controls#
const n = numberInput.value; // always a string, even for type="number"
const n = Number(numberInput.value); // convert to a number before doing arithmetic
Forgetting to convert .value to a number is a common bug: "3" + "4" gives "34", not 7. Always wrap numeric inputs with Number(...) or parseFloat(...) before calculating with them.
For radio buttons, several inputs share the same name but have different value attributes. Check which one is selected with .checked:
maleRadio.checked; // true or false
You can attach a click listener to each radio button individually, or loop over the group. Either approach lets you react whenever the selection changes.
5. Showing and hiding elements#
Every HTML element has a hidden property that maps to the hidden attribute.
element.hidden = true; // element disappears from the page
element.hidden = false; // element reappears
This is the simplest way to toggle visibility, and it applies to any element, not just inputs: a <div>, a <label>, a whole form section.
The general pattern for “show element B depending on the state of control A” is:
- Read the current state of A (
.checked,.value, and so on). - Set
.hiddenon B based on that state. - Do step 1 and 2 both on page load (so the initial state is correct) and inside the event handler (so it updates when the user interacts).
6. Styling elements from JavaScript#
You can set inline CSS directly through the style property.
element.style.fontSize = "20px";
element.style.color = "red";
CSS properties with hyphens (font-size) become camelCase in JavaScript (fontSize). When generating a sequence of elements with a changing style (for example, growing font size), compute the value inside a loop and assign it per element:
for (let i = 1; i <= n; i++) {
const p = document.createElement("p");
p.textContent = "Hello World!";
p.style.fontSize = (10 + i * 4) + "px";
document.body.appendChild(p);
}
7. Loops for repeated structure#
Most “generate N things” or “generate a table” tasks come down to nested loops that build DOM elements.
A single loop produces a flat sequence (N lines of increasing font size, N rows of a report). A nested loop (loop inside a loop) produces a grid: rows and columns.
for (let row = 1; row <= n; row++) {
for (let col = 1; col <= n; col++) {
// compute a value using row and col, e.g. row * col
}
}
The outer loop typically corresponds to table rows; the inner loop corresponds to the cells within one row. Build a <tr> in the outer loop, then append <td> elements to it in the inner loop, then append the finished <tr> to the <table>.
8. Building tables#
A table is a nested structure: <table> contains <tr> elements, each <tr> contains <td> (or <th> for headers) elements.
const table = document.getElementById("myTable");
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.textContent = "value";
row.appendChild(cell);
table.appendChild(row);
Any task that says “show a table where every row represents…” follows this pattern: one loop iteration produces one <tr>, and within that iteration you create and append one <td> per column of data.
9. Math and simple formulas#
Some tasks require a formula rather than DOM logic. Read the input, convert to a number, compute, then display the result using the techniques from section 2.
- Circumference of a circle:
2 * Math.PI * radius.Math.PIis a built-in constant. - Compound interest for year
i:principal * Math.pow(1 + rate / 100, i), assuming rate is given as a whole-number percentage. Compute this inside the table-building loop from section 8, one row per year.
10. Iterating over existing elements#
To list something about elements already in the HTML (their URLs, their tag names, their text), select them all with querySelectorAll, then loop and read a property from each.
const links = document.querySelectorAll("a");
links.forEach(function (link) {
console.log(link.href); // resolved absolute URL
console.log(link.textContent); // the visible link text
});
For each link, create a new element to hold that information (see section 2 and 8) and append it to an output container, such as a <ul> you fill with <li> elements.
The same pattern applies to building a table of contents: select "h1, h2, h3" together with querySelectorAll, and for each heading found, create a list entry using its .tagName (to know the level) and .textContent (for the heading text).
11. Copying values between fields#
To copy the content of one input into another, read .value from the source and assign it to .value on the target, inside a click handler.
button.addEventListener("click", function () {
targetInput.value = sourceInput.value;
});
12. Stateful components: counters#
Some components need to remember a value across multiple clicks. Store that value in a variable outside the event handler (so it persists between clicks), and update both the variable and the displayed field together.
let count = 0;
const min = 0;
const max = 10;
function refresh() {
countInput.value = count;
increaseButton.disabled = (count >= max);
decreaseButton.disabled = (count <= min);
}
increaseButton.addEventListener("click", function () {
count++;
refresh();
});
decreaseButton.addEventListener("click", function () {
count--;
refresh();
});
refresh(); // set the correct initial state
The disabled property (not hidden) is the right tool when a button should become unclickable but stay visible. Centralizing the “update the display” logic in one refresh function, called after every state change, keeps the input field and the button states from drifting out of sync.
13. Guess-the-number logic#
This task needs a hidden target value and a comparison each time the user guesses.
const target = Math.floor(Math.random() * 100) + 1; // random number, chosen once
guessButton.addEventListener("click", function () {
const guess = Number(guessInput.value);
if (guess < target) {
// display "too low"
} else if (guess > target) {
// display "too high"
} else {
// display "correct"
}
});
target must be set once, outside the click handler, so it stays the same across repeated guesses. Math.random() returns a value between 0 (inclusive) and 1 (exclusive); scaling and flooring it produces a random whole number in a chosen range.
14. Working with an array of objects (the book catalogue)#
Represent structured data as an array of objects, one object per record.
const books = [
{ author: "...", title: "...", year: 2001, publisher: "..." },
{ author: "...", title: "...", year: 2010, publisher: "..." },
// ...
];
Filtering, the core operation behind every catalogue task, means looping through the array and keeping only the entries that match a condition:
const matches = books.filter(function (book) {
return book.year === targetYear;
});
.filter returns a new array containing only the elements for which the given function returns true. After filtering, display the results by clearing the output container (section 2) and looping over matches to build rows or list items (sections 7 to 8).
For the dropdown of publishers, first collect the distinct publisher names, then create one <option> per name:
const publishers = [...new Set(books.map(function (book) { return book.publisher; }))];
publishers.forEach(function (name) {
const option = document.createElement("option");
option.value = name;
option.textContent = name;
dropdown.appendChild(option);
});
books.map(...) produces an array of publisher names (with repeats). new Set(...) removes duplicates. The spread [...set] converts the Set back into a plain array so .forEach can iterate over it.
Filtering by dropdown selection uses the change event on the <select> element and compares book.publisher to dropdown.value.
Filtering by typed substring (refreshing on every keystroke, from section 3’s input event) uses String.includes:
const matches = books.filter(function (book) {
return book.title.includes(searchBox.value);
});
This same .filter and rebuild pattern covers all three catalogue sub-tasks; only the condition inside .filter and the triggering event change.
Summary: how the pieces combine#
Every exercise in this set is some combination of:
- Select element(s) — section 1
- Read input, converting to a number if needed — section 4
- Compute something — sections 6, 9, 13, or a
.filter— section 14 - Build or update DOM content — sections 2, 5, 6, 8
- Attach the whole thing to an event — sections 3, 12
Read a task, identify which of these five steps it needs, and assemble them in that order.