The Browser and the DOM
Selecting and updating elements, handling events safely, working with forms, and persisting small amounts of data in localStorage.
By the end of this lesson, you can
- Select elements and read or update their content, attributes, and classes through the DOM
- Attach an event listener, read the event object, and explain event propagation at an introductory level
- Insert user-provided text into the page without introducing an XSS vulnerability
- Persist and safely restore small amounts of data with localStorage, handling missing or malformed data
Why it matters
Everything covered so far runs the same in a browser or Node. This lesson is where the browser’s own host APIs — recall Orientation’s distinction between the language and host APIs — start mattering: the DOM, events, and browser-only storage.
Selecting and updating elements
The DOM (Document Object Model) is the browser’s live, in-memory tree representation of the page — every HTML element is a node you can read from and write to with JavaScript:
const heading = document.querySelector('h1'); // first match
const items = document.querySelectorAll('.item'); // all matches, as a static NodeList
heading.textContent = 'Updated title'; // safe: sets text, never parsed as HTML
heading.classList.add('highlighted'); // toggle CSS classes
heading.setAttribute('data-status', 'active');
console.log(heading.getAttribute('data-status')); // "active"
querySelector/querySelectorAll accept any CSS selector, which makes them the most flexible way
to find elements — '.item', '#main-nav', 'button[type="submit"]' all work exactly as they
would in a stylesheet.
Events
An event listener runs a function in response to something happening — a click, a keypress, a form submission:
const button = document.querySelector('#save-button');
button.addEventListener('click', function (event) {
console.log('Clicked:', event.target); // the exact element that was clicked
event.preventDefault(); // stop the browser's default action, if there is one
});
The event object (event above) describes what happened — event.target is the element the
event originated on, and event.preventDefault() stops a default browser behavior (like a form
actually submitting and reloading the page).
Events propagate: a click on a button inside a <div> fires on the button first, then “bubbles”
up through each ancestor in turn. This is enough to know for now — event delegation, which relies
on this behavior deliberately, is covered in the Intermediate tier.
Creating elements and the XSS risk
const li = document.createElement('li');
li.textContent = userSuppliedText; // safe — always treated as plain text
list.appendChild(li);
list.removeChild(li); // or: li.remove();
localStorage
localStorage persists small amounts of text data in the browser between page loads — but it only
stores strings, so structured data needs JSON.stringify/JSON.parse (from Arrays and
Objects), and reading it back always needs a failure plan:
function loadPreferences() {
try {
const raw = localStorage.getItem('preferences');
if (!raw) return { theme: 'light' }; // nothing stored yet
const parsed = JSON.parse(raw);
return { theme: 'light', ...parsed }; // merge over sensible defaults
} catch {
return { theme: 'light' }; // stored data was corrupted or unreadable
}
}
function savePreferences(prefs) {
localStorage.setItem('preferences', JSON.stringify(prefs));
}Check your understanding
Or reveal the answer without checking
Answer:A comment containing a <script> tag or event-handler attribute could execute in every visitor's browser — a stored XSS vulnerability
innerHTML parses its input as HTML. Untrusted text set this way can include a script tag or an inline event handler that the browser will execute — a classic stored XSS vulnerability. textContent avoids this by never parsing its input as markup.
Or reveal the answer without checking
Answer:Throws a SyntaxError
JSON.parse throws a SyntaxError on malformed input — it never silently returns a fallback value. Code reading from localStorage needs a try/catch to handle this.
Practice: warm-up
Given <button id="counter" data-count="0">Count: 0</button> in the page, write the JavaScript to
select it, and on each click, increment data-count by 1 and update the button’s text to match.
Stuck? Reveal one hint at a time.
Hint 1
getAttribute/setAttribute read and write data-count as a string — you'll need Number() to do arithmetic on it.
Hint 2
addEventListener("click", handler) runs handler each time the button is clicked.
Hint 3
Update both the data-count attribute and the visible textContent inside the same handler, so they stay in sync.
Reveal a reference solution
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
const button = document.querySelector('#counter');
button.addEventListener('click', () => {
const next = Number(button.getAttribute('data-count')) + 1;
button.setAttribute('data-count', String(next));
button.textContent = `Count: ${next}`;
});Practice: apply it
Write loadTheme() and saveTheme(theme) functions backed by localStorage, under the key
'app:theme', where loadTheme() returns 'light' if nothing is stored or the stored value is
invalid JSON, and otherwise returns the stored value.
Stuck? Reveal one hint at a time.
Hint 1
Since a theme name is already a plain string, this doesn't strictly need JSON.stringify/parse — but write it as if it might grow into a larger settings object later.
Hint 2
Wrap the read in try/catch, and treat both a missing key and a parse failure the same way: fall back to "light".
Reveal a reference solution
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
const STORAGE_KEY = 'app:theme';
function loadTheme() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return 'light';
return JSON.parse(raw);
} catch {
return 'light';
}
}
function saveTheme(theme) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(theme));
}Summary
querySelector/querySelectorAllfind elements with any CSS selector;textContent,classList, andsetAttribute/getAttributeread and update them safely.addEventListenerruns a function in response to an event; the event object’stargetidentifies what triggered it, andpreventDefault()stops the browser’s default behavior.- Events bubble up through ancestor elements — enough to know for now, with event delegation covered later.
- Never set
innerHTMLfrom untrusted text — it parses as HTML and can execute a<script>tag or event handler, a real vulnerability class called XSS. UsetextContentinstead. localStorageonly stores strings; reading structured data back needsJSON.parsewrapped in atry/catch, since stored data can be missing or corrupted.