Lecture 13: DOM Manipulation and Event Handling¶
Every interactive web page you have ever used — one that updates without a full reload, reacts to clicks, or validates a form as you type — does it through the DOM. This lecture shows you how JavaScript reads, changes, and reacts to the structure of a web page.
In This Lecture¶
- What the DOM tree, nodes, and the
documentobject actually are - Selecting elements with
getElementById,querySelector, andquerySelectorAll - Creating, updating, and removing elements
- Working with
classListand inline styles - Handling events with
addEventListenerand the event object - Event bubbling, capturing, delegation,
preventDefault, andstopPropagation
The DOM Tree, Nodes, and the document Object¶
The DOM (Document Object Model) is a live, in-memory, tree-shaped representation of an HTML page that the browser builds after parsing your HTML. Every tag becomes an object — called a node — that JavaScript can read and modify. When you change the DOM, the browser immediately redraws the page to match.
graph TD
Doc[document] --> HTML[html]
HTML --> Body[body]
Body --> H1[h1: 'My Page']
Body --> P[p]
P --> Text1["Text: 'Hello '"]
P --> Strong[strong: 'world']
A few important vocabulary words:
- Node: any single item in the tree — an element, a piece of text, or a comment.
- Element: a node that represents an HTML tag, like
<p>or<div>. document: a special global object that represents the entire page, and is your entry point for interacting with the DOM (document.getElementById(...),document.title, etc.).- Parent / child / sibling:
bodyis the parent ofh1andp;h1andpare siblings (children of the same parent).
The DOM is not the same as your HTML file
The DOM is what the browser builds from your HTML — and JavaScript can change it after the fact. If you use "View Page Source" you see the original HTML file, but if you use "Inspect Element" (DevTools) you see the current DOM, which may look very different after scripts have run.
Selecting Elements¶
Before you can change something on the page, you need to select the node(s) you want.
// By id — returns a single element, or null if not found
const title = document.getElementById("main-title");
// By CSS selector — returns the FIRST matching element, or null
const firstButton = document.querySelector(".btn");
const firstInput = document.querySelector("input[type='email']");
// By CSS selector — returns ALL matching elements as a NodeList
const allButtons = document.querySelectorAll(".btn");
querySelector and querySelectorAll accept any valid CSS selector — by class (.btn), by
tag (div), by attribute ([type="text"]), or combinations (ul li.active) — which makes them
far more flexible than the older getElementById/getElementsByClassName methods.
querySelectorAll returns a NodeList, not an Array
A NodeList supports forEach, but not map or filter directly. Convert it first if you
need those: Array.from(allButtons) or [...allButtons].
Creating, Updating, and Removing Nodes¶
Creating and Inserting¶
const newItem = document.createElement("li"); // create a new <li> element
newItem.textContent = "New task"; // give it text content
const list = document.querySelector("#task-list");
list.appendChild(newItem); // add as the last child
list.prepend(newItem); // add as the first child
list.insertBefore(newItem, list.children[1]); // insert at a specific position
Updating Content and Attributes¶
const heading = document.querySelector("h1");
heading.textContent = "Updated Title"; // sets plain text (safe — treats input as text)
heading.innerHTML = "<em>Updated</em> Title"; // parses the string AS HTML
const link = document.querySelector("a");
link.setAttribute("href", "https://example.com");
console.log(link.getAttribute("href"));
link.removeAttribute("target");
innerHTML and security
innerHTML parses whatever string you give it as real HTML. If that string ever comes from
user input (a comment box, a search field) and you insert it with innerHTML, a malicious
user could inject a <script> tag — this is called XSS (Cross-Site Scripting), and you
will study it later in this course. Prefer textContent unless you specifically need to
insert HTML markup, and always trust textContent over innerHTML for anything user-typed.
Removing Nodes¶
const item = document.querySelector("#task-3");
item.remove(); // modern, simplest way
// Older way, still seen in existing code:
item.parentNode.removeChild(item);
classList and Inline Styles¶
Rather than changing CSS with raw strings, use classList to toggle predefined CSS classes — this
keeps styling logic in your CSS file, where it belongs.
const box = document.querySelector(".box");
box.classList.add("highlighted");
box.classList.remove("hidden");
box.classList.toggle("active"); // adds it if missing, removes it if present
console.log(box.classList.contains("active")); // true or false
You can also set individual CSS properties directly through the style property, which writes
inline styles (CSS applied directly on the element, with the highest priority):
Prefer classList over style
Toggling a class is usually better than setting individual style properties in JavaScript, because it keeps the "what it should look like" (CSS) separate from "when it should look that way" (JavaScript), and it's easier to change the look later without touching your script.
Events and addEventListener¶
An event is something that happens in the browser that your code can respond to — a click, a key press, a page load, a form submission, and many more. An event listener is a function you register to run when a specific event happens on a specific element.
const button = document.querySelector("#save-btn");
button.addEventListener("click", function () {
console.log("Button was clicked!");
});
// Arrow function version
button.addEventListener("click", () => console.log("Clicked!"));
addEventListener(eventType, handlerFunction) is preferred over the older onclick = ... style
because it lets you attach multiple listeners to the same event on the same element, and you
can remove a specific one later with removeEventListener.
function handleClick() {
console.log("Handled once");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick); // must pass the SAME function reference
The Event Object¶
Every event handler automatically receives an event object describing what happened:
document.querySelector("input").addEventListener("keydown", (event) => {
console.log(event.key); // which key was pressed, e.g. "Enter"
console.log(event.target); // the exact element the event happened on
console.log(event.type); // "keydown"
});
document.querySelector("form").addEventListener("submit", (event) => {
event.preventDefault(); // stop the default action (page reload) — see below
console.log("Form data captured without reloading the page");
});
Common events you will use constantly: click, submit, input, change, keydown,
keyup, mouseover, mouseout, load, DOMContentLoaded.
Bubbling, Capturing, and Event Delegation¶
When an event fires on an element nested inside other elements, it doesn't just affect that one element — it travels through the DOM tree in two phases:
- Capturing phase: the event travels down from
documentto the target element. - Bubbling phase: the event then travels back up from the target element to
document.
flowchart TD
subgraph Capturing["1. Capturing (top to bottom)"]
D1[document] --> B1[body] --> U1[ul] --> L1["li (target)"]
end
subgraph Bubbling["2. Bubbling (bottom to top)"]
L2["li (target)"] --> U2[ul] --> B2[body] --> D2[document]
end
By default, addEventListener listens during the bubbling phase. You can listen during the
capturing phase instead by passing true as a third argument:
element.addEventListener("click", handler, true); // capturing phase
element.addEventListener("click", handler, false); // bubbling phase (default)
Event Delegation¶
Because events bubble up, you can attach one listener on a parent element instead of separate listeners on every child — this is called event delegation. It's especially useful for lists where items are added or removed dynamically, since a listener on individual items would need to be re-attached every time.
const list = document.querySelector("#task-list");
// One listener on the parent handles clicks on ANY current or future <li>
list.addEventListener("click", (event) => {
if (event.target.tagName === "LI") {
event.target.classList.toggle("done");
}
});
Here, event.target is the specific element that was actually clicked (a particular <li>),
while event.currentTarget is the element the listener is attached to (list itself).
Why event delegation matters
If you add 100 new <li> items after the page loads, a delegated listener on the parent
<ul> automatically handles clicks on all of them — no need to attach 100 separate listeners
or re-attach one every time an item is added.
preventDefault and stopPropagation¶
These two methods on the event object control the event's behavior, but they do very different things:
event.preventDefault(): stops the browser's default behavior for that event — like a form actually submitting and reloading the page, or a link actually navigating.event.stopPropagation(): stops the event from continuing to bubble (or capture) to other elements — but the default browser behavior still happens unless you also callpreventDefault().
document.querySelector("a.disabled-link").addEventListener("click", (event) => {
event.preventDefault(); // the link will NOT navigate anywhere
});
document.querySelector(".dropdown-toggle").addEventListener("click", (event) => {
event.stopPropagation(); // clicking this button won't also trigger a listener
// on a parent element (e.g. a "click outside to close" handler)
});
Don't confuse the two
preventDefault() cancels what the browser would have done. stopPropagation() cancels
the event from reaching other listeners further up (or down) the tree. A single handler can
call both when needed, but they solve different problems.
Try It Yourself¶
- Build a simple to-do list: an
<input>, an "Add"<button>, and an empty<ul id="list">. When the button is clicked, create a new<li>with the input's text and append it to the list, then clear the input. Useevent.preventDefault()if you wrap the input in a<form>. - Add one delegated
clicklistener on#listthat toggles adoneCSS class (with a line-through style) when any<li>is clicked, and removes the<li>entirely if the user clicks a small "×" you add inside each item — usingevent.targetto tell which part was clicked.
Key Takeaways¶
- The DOM is a live, tree-shaped, in-memory representation of the page;
documentis your entry point into it. getElementByIdselects by id;querySelector/querySelectorAllselect by any CSS selector and are more flexible.- Prefer
textContentoverinnerHTMLunless you specifically need to insert HTML, to avoid XSS security risks with user-supplied text. classList.add/remove/toggle/containsis the preferred way to change an element's appearance; reserve directstylechanges for one-off, dynamic values.addEventListenerregisters event handlers and supports multiple listeners per event; every handler receives an event object describing what happened.- Events bubble up (and can capture down) through the DOM tree — this enables event delegation, where one listener on a parent handles events from many (even future) children.
preventDefault()stops the browser's default action;stopPropagation()stops the event from reaching other listeners — they are not the same thing.