The Living Tree: The DOM and Events
This is the layer where a page stops being a document and starts being a program — and the layer every framework you'll ever use is quietly standing on.
Up to now the page has been a document: written once, rendered once, sitting there. Now it has to react — a menu opens, a row deletes, a counter increases, a field validates as you type.
Everything in that sentence happens through one interface. The browser exposes the page to JavaScript as a tree of objects you can read, change, add to and listen to. Frameworks will eventually do this for you, but they do exactly this, and the day something behaves strangely you'll be back here reading it directly. It's worth being able to.
The DOM is not your HTML. It's the live object the browser built from it — and from the first script onwards, it's the only thing that exists.
Finding things
Two methods cover nearly all selection, and both take a CSS selector — the same syntax you already know from stylesheets, which is a large piece of learning you don't have to repeat.
querySelector returns the first match or null. querySelectorAll returns all of them, as a static list — a snapshot, not a live view, so elements added afterwards aren't in it.
Two habits save real time. Search inside a container, not the whole document — asking a component's root element for .title is faster and, more importantly, can't accidentally find some other component's title. And check for null: querySelector returning nothing is the origin of a large share of "cannot read properties of null" errors, and it usually means the script ran before the element existed — which is the parser story from the first article, arriving in person.
Changing things
Reading and writing content comes down to a few properties, and one of them is a security decision.
textContent sets text as text. innerHTML parses a string as markup and builds elements from it. The second is convenient and is the standard way to introduce a cross-site scripting hole: any user-supplied string put through innerHTML can bring a script with it. The rule is simple and absolute — textContent unless you have a real reason, and never innerHTML with anything a user typed. The Security sphere tells this story properly; here, just take the rule.
classList — add, remove, toggle, contains — is how you change appearance. Prefer it strongly over setting inline styles from JavaScript. Toggling is-open keeps the appearance in the stylesheet where it belongs and leaves you one place to change it; writing element.style.display scatters design decisions through your logic and produces the highest-specificity styles in the document, which nothing can later override.
dataset reads and writes data-* attributes, which is the sanctioned way to attach a small piece of state to an element — a row's id, an item's status.
And one distinction that confuses everyone once: attributes are not properties. An attribute is the value in the HTML; the property is the live state of the object. A checkbox's checked property changes as the user clicks; its checked attribute still says what the document said at load. Read the property when you want to know what's true now.
Creating things
createElement makes an element, append puts it somewhere. For a repeated structure, a template element holding the markup and cloneNode for each copy is cleaner than assembling elements one at a time.
The thing to know is the cost. Every insertion into the live document can trigger layout and paint — the expensive stations from the first article. Appending a thousand rows one by one asks for that work a thousand times; building them into a document fragment and inserting once asks for it once. This is the difference between a list that appears instantly and one that hangs the tab for two seconds.
The same principle in its most common form: don't interleave reading and writing. Reading a size or position forces the browser to compute layout right now; writing invalidates it. Alternating the two in a loop makes the browser recalculate on every iteration — a pathology with its own name, layout thrashing. Read everything you need, then write everything you need.
Events
addEventListener connects an element, an event name, and a function to run. The function receives an event object, and three of its parts do most of the work:
target— the element the event actually happened on.currentTarget— the element whose listener is running. Different fromtargetwhenever the event came from a child, which is the whole basis of the next section.preventDefault()— cancel the browser's built-in reaction: a form submitting and reloading, a link navigating, a checkbox toggling.
Then there's stopPropagation(), which halts the event's journey. It's occasionally necessary and usually a trap: it means some component elsewhere silently stops receiving events, and the resulting bug — "why doesn't the dropdown close when I click this one button?" — is invisible from both ends. Reach for it last.
Bubbling and delegation
An event doesn't happen at one element. It travels down from the document to the target (the capture phase), fires at the target, then travels back up through every ancestor (the bubble phase). Almost all listeners run on the way up.
This looks like trivia and is actually the most useful mechanism in this article, because it enables delegation: instead of attaching a listener to each of a thousand rows, attach one to the table and ask event.target which row it came from.
Delegation gives you two things. It replaces a thousand listeners with one, which matters for memory and for setup time. And it works for elements that don't exist yet — a row added in five minutes is handled by the listener you attached now, with no re-binding. Anyone who has ever wondered why their click handler stopped working on dynamically added items has met the problem delegation solves.
The events worth knowing
click — fires for taps and for keyboard activation of real buttons and links, which is one more reason to use real elements.
input versus change — input fires on every keystroke, change fires when the value is committed and the field is left. Search-as-you-type wants input; "validate when they're done" wants change.
submit on the form, not click on the button — that's what catches Enter as well.
keydown for keyboard handling, reading event.key — the actual key name.
focus and blur for validation timing, and their bubbling counterparts focusin and focusout when you need delegation.
scroll and resize fire enormously often, so anything expensive attached to them needs limiting: debounce waits until the activity stops before running (a search field), throttle runs at most once per interval (a scroll position readout). Knowing which of the two a situation needs is a small, genuinely reusable piece of judgement.
Cleanup
A listener holds a reference to its function, which holds a reference to everything in its closure. Remove an element without removing its listeners in a long-lived application and you have a memory leak that grows every time the user opens the same panel.
Two ways to stay clean: keep the function in a variable so you can pass it to removeEventListener, or — much better — create an AbortController and pass its signal to every listener you add. One abort() call then removes all of them at once. In component-based code, this is what "unmount" is for, and forgetting it is one of the most common real leaks in single-page applications.
Why this leads to frameworks
Build something small this way and you'll feel the problem yourself. You have data in variables and a picture in the DOM, and every change means updating both, by hand, in the right order. Add a third place where the same number is displayed and you must remember all three. The bugs that follow aren't hard, they're endless: the screen and the data quietly disagree.
That is the exact problem frameworks solve, and it's why the next stage exists. But do this by hand first, on something small — a to-do list, a filterable table — because the reason for everything in the next stage is invisible unless you've had the problem it removes.
And note what you're really doing while you build it: making a thing you can't yet make, with immediate feedback, correcting as you go. That's the deliberate practice the How to Learn sphere describes, and it's why an hour of building beats three hours of reading here.
In practice
Build one interactive thing with no framework. A to-do list with add, delete, filter and a count. Two hundred lines, one evening, and it converts this article into something your hands know.
Use delegation by default for anything list-shaped. One listener on the container, event.target to find the item.
Toggle classes, don't set styles. If you catch yourself writing element.style, ask what class you should have toggled instead.
Say the phase out loud when an event does something unexpected: where did it start, what did it pass through, who else is listening. Half of event bugs are answered by that sentence.
Add an AbortController to every component you build. Make it habitual before you have a leak, not after.
When something's inexplicable, step away for ten minutes. The How to Learn sphere's two modes: focused attention finds the bug you're looking for, and the diffuse mode finds the one you aren't. Staring harder at the same eight lines is the least productive thing you can do.
Check yourself
Close the article and answer in your own words:
- Why is
querySelectorAll's result not a live view, and when does that matter? - What's the difference between
textContentandinnerHTML, and what's the rule? - Why prefer toggling a class over setting an inline style?
- What's the difference between an attribute and a property?
- Describe the three phases of an event and explain delegation.
- When do you want
inputand whenchange? Debounce or throttle? - What creates a listener leak, and what removes a whole group at once?
In short
- The DOM is a live object tree, not your HTML file, and from the first script it's the only version that exists.
- Select with CSS selectors, scope the search to a container, and expect
nullwhen the script ran too early. textContentby default;innerHTMLwith user data is how XSS gets in.- Change appearance by toggling classes, keep small state in
data-*, and remember that properties are live while attributes are what the document said. - Batch DOM insertions and never interleave reads and writes, or you make the browser recompute layout on every iteration.
- Events capture down and bubble up; delegation replaces many listeners with one and covers elements that don't exist yet.
- Know
inputversuschange,submiton the form, and when to debounce rather than throttle. - Clean up listeners with an
AbortController— and build something by hand first, because it's the only way to feel the problem frameworks exist to solve.