DOM & events
The DOM is a live tree of objects — and JS can poke every branch of it.
Everything below runs against a real, live sandbox on this
page, not a simulation — the buttons genuinely call
querySelector, classList,
appendChild and friends against the little page snippet
right above them. Push every button before reading on; watching it
happen is most of the lesson.
Sandbox page
Click a button below to mutate me.
- Item 1
- Item 2
Every click below is logged with the exact DOM call that ran.
Selecting elements
| Call | Returns |
|---|---|
document.getElementById(id) | one element, or null — no # prefix |
document.querySelector(css) | the first match for any CSS selector, or null |
document.querySelectorAll(css) | a NodeList of every match — not a real array, but it has .forEach |
document.getElementById("de-text"); // exact id match
document.querySelector("#de-text"); // same element, CSS-selector syntax
document.querySelector(".btn"); // the FIRST element with class "btn"
document.querySelectorAll(".btn"); // every element with class "btn"
querySelector/
querySelectorAll take real CSS selectors, so anything you
can write in a stylesheet works here too —
"ul li:last-child", "[data-active]",
"input[type=email]". That flexibility is why they've
mostly replaced the older, narrower
getElementsByClassName/getElementsByTagName.
Reading and changing content
| Property | Reads/writes | Watch out for |
|---|---|---|
el.textContent | plain text only | the safe default — never parses HTML |
el.innerHTML | markup, parsed as HTML | user-supplied text through here is an XSS hole — see the security chapter |
el.getAttribute(name) / setAttribute(name, v) | any HTML attribute, always as a string | use for custom/data-* attributes |
el.classList | .add(), .remove(), .toggle(), .contains() | the modern way to manage classes — no manual string splitting |
el.textContent = "hello <b>there</b>"; // literal text — tags show up as text, not bold
el.innerHTML = "hello <b>there</b>"; // actually renders as bold
el.setAttribute("data-user-id", "42");
el.getAttribute("data-user-id"); // "42" — always a string, even for numbers
el.classList.add("active");
el.classList.toggle("open"); // on if it was off, off if it was on
el.classList.contains("active"); // true
Creating, appending, removing
const li = document.createElement("li"); // exists only in memory so far
li.textContent = "New item";
listEl.appendChild(li); // now it's actually in the page
listEl.removeChild(li); // gone from the page (still exists in memory until GC'd)
li.remove(); // modern shorthand — no need to know the parent
li is already somewhere in the page and you
appendChild it again elsewhere, it's relocated,
not duplicated — an element can only exist at one spot in the tree at
a time. Need it in two places? Use
el.cloneNode(true) (the true means "deep
clone, children included") and append the clone.
Events — addEventListener and the event object
button.addEventListener("click", function (event) {
console.log(event.type); // "click"
console.log(event.target); // the exact element that was clicked
console.log(event.currentTarget); // the element the LISTENER is attached to
});
target and currentTarget only differ when
events bubble — a click starts at the exact element you tapped
and travels upward through every ancestor that's listening.
target stays fixed at where it started;
currentTarget is always whichever element's listener is
currently running. That bubbling is what makes event delegation work:
put one listener on a parent list instead of one on every
item, and check event.target inside it to see which item
was actually clicked.
preventDefault — stopping the browser's own reaction
Some elements have a built-in behavior for certain events — a form
submits and reloads the page, an <a> navigates.
event.preventDefault() cancels that specific
default without stopping the event from continuing to bubble or
running your own handler.
form.addEventListener("submit", function (event) {
event.preventDefault(); // stop the page reload
const data = new FormData(form); // now handle it yourself — fetch(), validation, etc.
});
preventDefault() stops the
browser's built-in reaction. stopPropagation() stops the
event from bubbling further up the tree. They solve two different
problems and it's common to need only one of them.
Forms and input values
input.value; // the current text — always a string, even for type="number"
input.value = ""; // clearing it programmatically
checkbox.checked; // boolean — .value on a checkbox is NOT what's checked
select.value; // the selected <option>'s value
input.addEventListener("input", e => console.log(e.target.value)); // fires on every keystroke
input.addEventListener("change", e => console.log(e.target.value)); // fires once, on blur/commit
input vs change trips a lot of people up:
input is for "react live, as they type" (a character
counter, live search); change is for "react once they're
done" (a select dropdown, a checkbox, a field that loses focus).
Opens in the editor — write it, run it, and check it against real tests.