Frontend Master

JavaScript Curriculum

The Page Is a Tree
+50 XP

The Page Is a Tree

medium
~25 min·50 XP

Everything you've learned so far has run in isolation — no page, no user, no interaction. Now connect it to the browser. The Nexus dashboard is a live HTML document. When a user clicks 'View Profile', JavaScript needs to find the right element, fetch the data, and update the page — without a reload. This is the DOM.

What is the DOM?

When a browser loads an HTML file, it doesn't store it as text. It parses the markup and builds a live tree of JavaScript objects — one object per element, with every attribute, every style, and every piece of text accessible as a property. This tree is the Document Object Model — the DOM.

js
document html head body header h1 "Nexus Dashboard" nav#main-nav main section.user-card h2 "Alex Chen" p.role "Senior Engineer" button#view-btn "View Profile" footer.site-footer

Every node in that tree is a live JavaScript object. Change the object — the page changes instantly. No reload. No server round-trip.


Selecting elements

Before you can do anything to an element, you need a reference to it. JavaScript gives you several ways to get one.

Click any element in the tree below to see the exact code that selects it:

DOM tree — click any element to see its selectors

Click any element in the tree to see how to select it with JavaScript.

js
// By ID — fastest, IDs must be unique const btn = document.getElementById('view-btn') // By CSS selector — most flexible const btn = document.querySelector('#view-btn') // ID const role = document.querySelector('.user-role') // class (first match) const heading = document.querySelector('h2') // tag (first match) const input = document.querySelector('form input[type="email"]') // nested // All matching elements — returns a NodeList (array-like) const allCards = document.querySelectorAll('.user-card') allCards.forEach(card => console.log(card.textContent))
💡querySelector vs getElementById
Use getElementById when you have an ID — it's marginally faster. Use querySelector for everything else — it accepts any CSS selector and is consistent. Avoid the older getElementsByClassName and getElementsByTagName in new code: they return live HTMLCollections with confusing behaviour.

Reading and writing element properties

Once you have a reference, you can read and change almost anything:

js
const heading = document.querySelector('h2') // Text content console.log(heading.textContent) // "Alex Chen" heading.textContent = "Jordan Smith" // changes the page immediately // HTML content — use with care (XSS risk with user input) heading.innerHTML = '<strong>Jordan</strong>' // Attributes const btn = document.querySelector('#view-btn') btn.getAttribute('class') // "btn-primary" btn.setAttribute('disabled', '') // greys out the button btn.removeAttribute('disabled') // Inline styles btn.style.backgroundColor = '#388bfd' btn.style.display = 'none' // hide btn.style.display = 'block' // show

Try the sandbox below — each action shows the exact JavaScript that produced it:

DOM manipulation sandbox — every action shows its code

live element

Nexus Dashboard

Senior Engineer

actions

appendChild() — add a badge


classList — the right way to manage styles

Toggling inline styles directly is fragile. The idiomatic approach: keep visual states in CSS classes and use classList to apply them:

js
const card = document.querySelector('.user-card') card.classList.add('highlighted') // adds the class card.classList.remove('highlighted') // removes it card.classList.toggle('highlighted') // adds if absent, removes if present card.classList.contains('highlighted') // → true or false card.classList.replace('old-cls', 'new-cls') // swap

Your CSS defines what .highlighted looks like. JavaScript just switches it on and off. This separation keeps your code clean and your styles reusable.


Creating and inserting elements

You can build new DOM nodes entirely in JavaScript and insert them anywhere:

js
// Create a new element const badge = document.createElement('span') badge.textContent = 'Verified' badge.className = 'badge badge--verified' // Insert it into the DOM const container = document.querySelector('.badges') container.appendChild(badge) // add at the end container.prepend(badge) // add at the start container.insertBefore(badge, existing)// insert before a specific sibling // Remove an element badge.remove() // remove from DOM entirely container.innerHTML = '' // remove ALL children at once

Events — responding to user interaction

The DOM broadcasts events when things happen: clicks, keypresses, form submissions, mouse moves, page loads. You attach a listener function that runs when the event fires.

js
const btn = document.querySelector('#view-btn') btn.addEventListener('click', (event) => { console.log('clicked!', event.target) // event.target = the element that was clicked // event.currentTarget = element the listener is attached to }) // Common event types btn.addEventListener('mouseenter', () => btn.classList.add('hover')) btn.addEventListener('mouseleave', () => btn.classList.remove('hover')) const input = document.querySelector('input') input.addEventListener('input', (e) => console.log(e.target.value)) input.addEventListener('keydown', (e) => { if (e.key === 'Enter') submit() }) document.addEventListener('DOMContentLoaded', () => { // safe to access DOM — all elements exist })

Event bubbling and capturing

When an event fires on an element, it doesn't stay there. It travels — first down the tree (capturing phase, rarely used), then back up (bubbling phase). Every ancestor that has a listener for that event type will have its handler called.

Click the button and watch the event travel upward through each ancestor:

event bubbling — click the button, watch the event travel

DOM structure

document
<section>
<div class="card">

event listener log

click the button…

handler code

button.addEventListener('click', (e) => {
// no stopPropagation
console.log('button clicked')
})

Without stopPropagation, the event bubbles upward through every ancestor that has a listener.

js
// Bubbling in action — one listener catches clicks from any descendant document.querySelector('section').addEventListener('click', (e) => { console.log('section heard a click from:', e.target.tagName) // e.target is the element that was ORIGINALLY clicked // e.currentTarget is the section (where this listener lives) }) // event delegation — one listener for a whole list document.querySelector('ul').addEventListener('click', (e) => { if (e.target.tagName === 'LI') { e.target.classList.toggle('selected') } })
🧠Event delegation — one listener beats many
Instead of attaching a listener to every item in a list, attach one listener to the parent. When any item is clicked, the event bubbles up to the parent and you check e.target to see which one fired. This is more performant and works automatically for items added to the list later.
js
// Stop the event here — ancestors won't hear it btn.addEventListener('click', (e) => { e.stopPropagation() // event dies here handleClick() }) // Stop the default browser behaviour (e.g. form submit, link navigation) link.addEventListener('click', (e) => { e.preventDefault() // don't follow the href doSomethingElse() })

Removing event listeners

Listeners attached with addEventListener stay active until explicitly removed or the element is destroyed:

js
function handleClick() { console.log('clicked') } btn.addEventListener('click', handleClick) // Later — remove it (must pass the same function reference) btn.removeEventListener('click', handleClick) // One-time listener — fires once then auto-removes btn.addEventListener('click', handleClick, { once: true })
⚠️Memory leaks from forgotten listeners
Listeners attached to elements keep those elements alive in memory — even if you've removed the elements from the page. Always remove listeners when you no longer need them, especially in components that mount and unmount repeatedly.

Your challenge

setupCard(name, role) is pure DOM wiring. Select two elements by their selectors, set their text content, then attach a click listener to the button. The listener logs a message that uses the name parameter — which means it needs to be a closure over the function argument.

That last part is important: the listener closes over name and will still have access to it when the button is clicked later.

Challenge

Write a function called setupCard(name, role) that: (1) queries the element with id 'user-name' and sets its textContent to name, (2) queries the element with class 'user-role' and sets its textContent to role, (3) queries the button with id 'profile-btn' and adds a click event listener that logs 'Profile opened for: ' + name. Call setupCard('Jordan', 'Security Lead').

DOMquerySelectoreventsaddEventListenerevent-bubblingclassListtextContentcreateElementappendChild
The Page Is a Tree | Nexus Learn