A reading shelf can contain a title, a button and a short status message. When a reader pins an item, three things need to agree: the program's Boolean state, the card's appearance and the button's accessible state. Changing only one of them produces a page that tells different stories to different observers.
The DOM supplies objects representing the current document. Events tell our program that an action occurred. Ordinary JavaScript objects hold the state on which the next document update depends. We will connect these ideas in a small browser-only example.
The DOM is a tree of objects
Definition
Document Object Model
The Document Object Model (DOM) represents a document as a tree of nodes. Element nodes represent elements such as headings and buttons; text nodes contain their text. A node can have children, and its position determines its parent and siblings.
The browser constructs the DOM while parsing HTML. JavaScript can subsequently change that same tree: adding a node, changing text or moving an existing element changes the current document. The original source text and the current DOM are therefore different representations.
In a browser, window represents the browsing window's global environment and document gives access to its document. The DOM is an API exposed to JavaScript; it is not the JavaScript language itself. A JavaScript program running outside a browser does not acquire a page's DOM merely because it uses the same language syntax.
<section id="shelf">
<h2>Reading shelf</h2>
<p>One item is available.</p>
</section>
The section contains an h2 element and a p element. It can also contain whitespace text nodes from the line breaks and indentation. This explains the distinction between children, which contains element children, and childNodes, which includes other node types such as text nodes.
const shelf = document.querySelector("#shelf");
const heading = shelf.firstElementChild;
const paragraph = heading.nextElementSibling;
console.log(heading.tagName); // H2
console.log(paragraph.parentNode === shelf); // true
console.log(shelf.children.length); // 2
These expressions assume the preceding document has been parsed and still has the shown structure. firstElementChild, lastElementChild, nextElementSibling and previousElementSibling navigate between elements. A boundary has no further element: for example, the last element's nextElementSibling is null.
A selector identifies nodes; a collection records matches
document.getElementById("shelf") searches by ID. document.querySelector("#shelf") uses CSS selector syntax and returns the first matching element. A query with no matching element returns null. IDs should be unique within a document; a class can describe several elements.
querySelectorAll returns a static NodeList. Its membership is fixed by that query, although the nodes inside it remain ordinary, changeable DOM nodes. By contrast, getElementsByTagName and getElementsByClassName return live HTMLCollections whose membership follows changes in the document.
Worked example
Predict a static and a live collection
Suppose #reading-list initially contains two li elements.
const list = document.querySelector("#reading-list");
const snapshot = list.querySelectorAll("li");
const liveItems = list.getElementsByTagName("li");
const item = document.createElement("li");
item.textContent = "Browser rendering";
list.appendChild(item);
console.log(snapshot.length); // 2
console.log(liveItems.length); // 3
snapshot[0].textContent = "Revised first title";
The new element is absent from snapshot because the query is not rerun. It appears in liveItems because that collection follows the tree. The final instruction still edits the first element in the document: a static list freezes membership, not the state of each listed object.
Common mistake
Removing elements changes a live collection during iteration
If a forward loop removes liveItems[0], the old second item becomes the new first item. Incrementing the loop index can then skip it. To remove every item from a live collection, one direct approach is to keep removing its first element while its length is positive. Alternatively, obtain a static querySelectorAll result and visit those recorded nodes.
Checkpoint
What remains fixed in a static NodeList?
After the example, a script changes the first item's text and appends a fourth item. Does the original snapshot display old text? Does its length become four?
Solution · Membership and node state are separate
The first entry still refers to the same first node, so reading its textContent gives the changed text. The snapshot's length remains two. It has not acquired either of the newly appended nodes. The live collection, in contrast, now contains four items.
Choose the property that matches the intended change
| Operation | Appropriate interface | Meaning |
|---|---|---|
| Read or replace ordinary node text | textContent | Treat the supplied string as text. |
| Read the current value of a text input | value | Obtain the control's current value. |
| Add or remove one CSS class | classList.add, remove, toggle | Change a class token while preserving other classes. |
| Set one inline style | style.backgroundColor | Change that inline CSS property. |
| Read or set an HTML attribute | getAttribute, setAttribute | Work with the attribute's string value. |
innerHTML parses its assigned string as markup, while textContent inserts text. For example, setting textContent to "<strong>Selected</strong>" displays the angle brackets and tag names. Use that behavior when the intended content is a title or message rather than a new HTML structure.
innerText relates to the text as rendered and can differ from textContent when styles hide parts of the document. For a direct text update, textContent expresses the operation clearly.
An element's style property represents inline declarations. It does not list every style that might affect the element through stylesheets. Many interaction states are easier to maintain by toggling a class and letting CSS describe the appearance. The two approaches can coexist, but one visible state should have a clear owner.
Construct and edit nodes explicitly
A newly created element is initially detached. It becomes part of the displayed document when a parent receives it.
const list = document.querySelector("#reading-list");
const item = document.createElement("li");
const label = document.createTextNode("Understanding events");
item.appendChild(label);
list.appendChild(item);
appendChild places a node at the end of its parent's children. Appending an existing node moves that node; it does not create a duplicate. insertBefore(newNode, existingChild) places a node before a chosen child. replaceChild(newNode, oldChild) replaces one child, and removeChild(child) removes a child of that parent. An element can also call remove() on itself.
The parent-child relationship matters. A node passed as oldChild to replaceChild, or as child to removeChild, must actually belong to that parent. The DOM operation describes a structural change, so identify the intended parent before applying it.
Checkpoint
Does appending a node copy it?
Two lists exist. A script stores a reference to an item in the first list, then calls secondList.appendChild(item). How many copies exist afterward, and which list owns the item?
Solution · A node has one position in the tree
There is still one item node. It moves from the first list to the end of the second list. The variable still refers to that same node. Creating a second independent element requires a separate construction or an explicit cloning operation.
Events connect actions to handlers
An event listener is a function registered for an event type on an event target. A click listener runs when the event system dispatches an appropriate click event to that target. Registering the function does not immediately execute its body.
const button = document.querySelector("#pin");
button.addEventListener("click", handlePin);
function handlePin(event) {
console.log(event.currentTarget.id);
}
Pass the function handlePin, not the result of calling handlePin(). Calling it during registration runs it at the wrong time and supplies its return value in place of the intended listener.
The event object describes the dispatched event. event.currentTarget is the target on which the currently running listener was registered. event.target identifies where the event originated. If a button contains a span and the reader clicks that span, the target can be the span while the button listener's current target remains the button.
For a bubbling event such as a normal click, listeners on ancestors may also run as the event travels through the tree. That behavior explains why a listener on a shelf can observe a click originating in one of its buttons. It does not mean every event bubbles; choose the particular event's behavior rather than assuming all event types are identical.
Other useful events describe different actions. input follows edits to an input control; change represents a committed change. Keyboard handlers receive information such as event.key. A keydown handler can run repeatedly while a key is held, so a key press and a single mouse click need not produce the same sequence of events.
A complete pinning interaction
Worked example
Trace a card through two pin changes
The following standalone document keeps a single Boolean state in memory and derives its visible and accessible representations from that state.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Reading shelf</title>
<style>
body { max-width: 36rem; margin: 2rem auto; padding: 0 1rem; }
.pinned { border-inline-start: 0.35rem solid currentColor; padding-inline-start: 1rem; }
button { font: inherit; padding: 0.5rem; }
</style>
</head>
<body>
<main>
<h1>Reading shelf</h1>
<article id="book">
<h2>Understanding browser events</h2>
<button id="pin" type="button" aria-pressed="false">
<span aria-hidden="true">★</span> Toggle pin
</button>
<p id="status" aria-live="polite">Not pinned</p>
</article>
</main>
<script>
const book = { title: "Understanding browser events", pinned: false };
const card = document.querySelector("#book");
const button = document.querySelector("#pin");
const status = document.querySelector("#status");
function render() {
card.classList.toggle("pinned", book.pinned);
button.setAttribute("aria-pressed", String(book.pinned));
status.textContent = book.pinned ? "Pinned" : "Not pinned";
}
button.addEventListener("click", () => {
book.pinned = !book.pinned;
render();
});
render();
</script>
</body>
</html>
The elements appear before the script, so the three queries find the intended nodes. book.pinned is the model state. render applies that state to the class, the aria-pressed attribute and the status text. Passing the Boolean as the second argument to classList.toggle makes the class agree with the value: true includes the class and false removes it.
| Stage | book.pinned | Card has pinned | aria-pressed | Status |
|---|---|---|---|---|
| Initial render | false | No | "false" | Not pinned |
| First click | true | Yes | "true" | Pinned |
| Second click | false | No | "false" | Not pinned |
The handler changes state once and then updates every representation through one function. Updating just the border would leave the status and button state inconsistent. Reloading the document starts another object at pinned: false; this example does not store the state between document loads.
Objects have identity as well as properties
An object is a collection of properties. Property values can themselves be objects, arrays, functions or primitive values. Dot notation uses a property name written in the program; bracket notation can use a computed key.
Worked example
Two names for one mutable object
Start with one book whose pinned property is false. Evaluate the statements in order, keeping track of whether a new object is constructed or another reference is assigned.
const book = { title: "DOM trees", pinned: false };
const key = "title";
console.log(book.title); // DOM trees
console.log(book[key]); // DOM trees
const alias = book;
alias.pinned = true;
console.log(book.pinned); // true
Both title reads produce DOM trees: key contains the name of the same property selected by dot notation. Assigning book to alias gives another reference to the same object. It does not copy the properties into a new independent object. Consequently, setting alias.pinned changes the property that book.pinned subsequently reads, so the last output is true. A const binding prevents reassignment of that binding; it does not freeze the object's properties.
Worked example
Make an independent simple record
const original = { title: "DOM trees", pinned: true };
const copy = { title: original.title, pinned: original.pinned };
copy.pinned = false;
console.log(original.pinned); // true
console.log(copy.pinned); // false
console.log(original === copy); // false
The object literal creates a new record. In this example both properties are primitive values, so the two records can change independently. If a property held another object, merely assigning that property's value would still share the nested object.
Objects can also be constructed by a function called with new:
function ReadingItem(title) {
this.title = title;
this.pinned = false;
}
const first = new ReadingItem("DOM trees");
const second = new ReadingItem("Browser events");
console.log(first === second); // false
Each shown new call creates a separate object and runs the function with that object as this. This provides a repeated initialization pattern. The literal and constructor forms are different construction techniques; both produce objects whose properties can be inspected and updated.
A method call determines its receiver
For an ordinary function, the value of this depends on how it is called. In a method call such as shelf.getCount(), the receiver is shelf. Copying the function to a variable does not permanently attach that receiver.
"use strict";
const shelf = {
count: 2,
getCount() {
return this.count;
}
};
console.log(shelf.getCount()); // 2
const detached = shelf.getCount;
// detached() has this === undefined in this strict-mode call.
// Reading this.count there would throw a TypeError.
An arrow function does not establish its own this; it retains the surrounding value. That is why it is clearer to use event.currentTarget when a listener needs its registered element, rather than assuming every function form provides the same this behavior. The pinning example needs neither a receiver nor this: its handler closes over the state and element references directly.
JSON is a text representation of data
JSON represents objects, arrays, strings, numbers, Booleans and null. Object keys and string values use double quotes. Comments and trailing commas are not part of its syntax. JSON text does not contain executable methods or DOM nodes.
Worked example
Decode a record, change it, then encode its new state
Start with a JSON string describing a pinned book and two tags. Follow the transition from text to a record, then from the changed record back to text.
const text = '{"title":"DOM trees","pinned":true,"tags":["web","reading"]}';
const record = JSON.parse(text);
console.log(record.tags[0]); // web
record.pinned = false;
const encoded = JSON.stringify(record);
console.log(typeof encoded); // string
Parsing constructs record with pinned initially true and a two-item tags array. Index zero selects web, giving the first output. The next assignment changes the record's flag to false. Stringifying that record produces a new JSON string containing "pinned":false, so the second output is string. The original variable text still contains its original string; changing the parsed object has not rewritten it.
JSON.parse reads text and constructs values. JSON.stringify produces text from supported values. Neither operation sends data anywhere or saves it automatically. A later unit will use these representations in communication, but representation and transport are separate operations.
Parsing the same JSON text twice constructs two independent records. JSON also does not preserve the identity of shared object references: it describes data values, not a graph of references and methods. A JSON round trip is therefore not a general method for copying arbitrary JavaScript objects.
The DOM and library interfaces are different layers
A library such as jQuery supplies its own interfaces for selecting elements, registering events and updating content. Those operations still act on the browser's DOM. Keep three layers distinct: JavaScript is the language, the DOM is a browser API, and a library provides another interface over selected operations. Recognising the underlying node and state change remains useful whichever interface a program uses.
Exercises
Checkpoint
Exercise 1: trace a state update
In the pinning document, the handler changes book.pinned but no longer calls render(). After one click, what is the Boolean value? What do the card, status and button attribute show? Explain why the program state and document state differ.
Solution · Exercise 1 solution
The Boolean becomes true. The initial render had removed the class, displayed “Not pinned” and set aria-pressed to "false". Without a new render, those document values remain unchanged. Updating an ordinary object does not automatically update the DOM; the program must apply the new value to its views.
Checkpoint
Exercise 2: identify an event's elements
A button with ID pin contains a star span. A click listener is registered on the button and another on its containing article. The reader clicks the star. Identify the click event's target and each listener's current target, assuming ordinary bubbling and no propagation changes.
Solution · Exercise 2 solution
The originating target is the star span. While the button listener runs, currentTarget is the button. While the article listener runs, currentTarget is the article. The event's origin stays the same, but the listener currently being invoked has a different registration target.
Checkpoint
Exercise 3: distinguish references and JSON text
Let a = { pinned: false }, b = a, and c = JSON.parse(JSON.stringify(a)). If the program then sets b.pinned = true, what values do a.pinned and c.pinned have? Does creating c save anything to disk?
Solution · Exercise 3 solution
a.pinned becomes true because a and b reference the same object. c.pinned remains false because parsing the earlier JSON text constructed another record with the earlier value. The conversion only creates a string and then an object in memory; it performs no disk write.
Checkpoint
Exercise 4: choose an update operation
A title should display the literal string <em>DOM</em>. Which property gives that result: textContent or innerHTML? If a card already has the classes book compact, how can it acquire pinned without discarding the existing classes?
Solution · Exercise 4 solution
Set the title's textContent to that string. It treats the brackets and tag names as text. innerHTML would parse an emphasis element. Use card.classList.add("pinned"), or card.classList.toggle("pinned", true), to add the class token while preserving book and compact.
The language tools used here are developed in JavaScript, functions and collections.