Sitemap

JavaScript for HTML DOM Manipulation

42 min readApr 12, 2025

Chapter 1: Introduction to the DOM

1.1 What is the DOM (Document Object Model)?

Think of it like this: When a web browser loads an HTML document, it doesn’t just display it as static text. It creates an internal, structured model or representation of that document in memory. This model is the Document Object Model (DOM).

It’s an API: The DOM is essentially an Application Programming Interface (API) for HTML (and XML) documents. It defines the logical structure of documents and the way a document is accessed and manipulated.

The DOM as a Tree Structure: The most common and helpful way to visualize the DOM is as a tree.

  • The document itself is the root of the tree.
  • Every HTML tag (like <html>, <body>, <h1>, <p>, <a>, <img>) becomes an Element Node in the tree.
  • The text content inside elements (like “Hello World” inside <p>Hello World</p>) becomes a Text Node.
  • HTML attributes (like href in <a href=”…”> or src in <img src=”…”>) become Attribute Nodes.
  • Even HTML comments (<! — like this →) become Comment Nodes.

Example:

Consider this simple HTML:

<!DOCTYPE html>
<html>
<head>
<title>My Page</title>
</head>
<body>
<h1>Welcome!</h1>
<p>This is a paragraph.</p>
<!-- Just a comment -->
</body>
</html>
  • A simplified view of its DOM tree structure would look something like this:
    [document]
|
[html]
/ \
[head] [body]
| / \
[title] [h1] [p] [comment]
| | |
[text:"My Page"] [text:"Welcome!"] [text:"This is a paragraph."] [text:" Just a comment "]
  • Each item in [] represents a Node.
  • You can see the parent-child relationships (html is the parent of head and body).
  • You can see sibling relationships (head and body are siblings; h1, p, and the comment are siblings).

1.2 How JavaScript Interacts with the DOM

The Bridge: JavaScript acts as the bridge that allows you to dynamically interact with this DOM tree after the page has loaded (or even while it’s loading).

The window Object: In a browser environment, the global object is window. It represents the browser window itself and contains many properties and methods related to the browser, including the document object.

The document Object: This is your primary entry point for interacting with the content of the web page. The document object (which is technically a property of window, so you can access it as window.document or just document) represents the entire HTML document loaded in that window/tab. It provides the methods you’ll use to find, change, add, or delete elements (nodes) in the DOM tree.

The Browser’s Role:

  1. You request a web page (e.g., type a URL).
  2. The browser downloads the HTML code.
  3. The browser’s rendering engine parses the HTML code.
  4. As it parses, it builds the DOM tree in memory.
  5. It also parses CSS and applies styles.
  6. It parses and executes JavaScript.
  7. Your JavaScript code can then use the DOM API (via the document object) to access and modify the already created DOM tree. Changes made via JavaScript can cause the browser to re-render parts of the page, making the page dynamic.

1.3 Developer Tools: Your Best Friend for DOM Inspection

Essential Tool: Every modern web browser comes with built-in “Developer Tools” (often opened by pressing F12, or right-clicking on an element and selecting “Inspect” or “Inspect Element”).

Key Panels:

  • Elements Panel (or Inspector): This panel shows you the live DOM tree. It looks like HTML source code, but it reflects the current state of the DOM, including any changes made by JavaScript. You can expand/collapse nodes, see CSS styles applied, and even edit the HTML/CSS directly here for temporary testing. This is invaluable for understanding the structure and seeing the results of your JS code.
  • Console Panel: This is where you can type JavaScript commands directly and execute them against the current page. It’s also where error messages and messages logged using console.log() appear. You can test simple DOM selection or manipulation commands right here (e.g., type document.body and press Enter to see the <body> element object).
  • Get Comfortable: Spend time exploring the Developer Tools on any web page. Right-click elements, inspect them, see how they map to the ‘Elements’ panel tree structure. Try simple commands in the ‘Console’. This hands-on exploration is fundamental to learning DOM manipulation.

Chapter 2: Selecting Elements (Finding Nodes)

To manipulate an element (change its text, style, attributes, etc.), you first need a reference to it in your JavaScript code. This chapter covers the common ways to get that reference.

2.1 Selecting a Single Element

These methods are designed to find one specific element.

document.getElementById(‘elementId’)

  • Purpose: Selects the single element that has the specified id attribute.
  • Argument: Takes one argument: a string representing the id of the element you want to find (case-sensitive).
  • Return Value: Returns the Element object if found, or null if no element with that id exists in the document.
  • Key Point: IDs should be unique within an HTML document. This method is very fast and efficient when you have a unique ID for the element you need.
  • Example:
<div id="main-content">Some text here</div>
<script>
const mainDiv = document.getElementById('main-content');
if (mainDiv) {
console.log('Found the main content div:', mainDiv);
} else {
console.log('Element with ID "main-content" not found.');
}
</script>

document.querySelector(‘cssSelector’)

  • Purpose: Selects the first element within the document that matches the specified CSS selector(s).
  • Argument: Takes one argument: a string containing one or more CSS selectors (e.g., ‘p’, .my-class, #myId, div > span, input[type=”text”]).
  • Return Value: Returns the first matching Element object, or null if no matches are found.
  • Key Point: Extremely versatile because it uses the powerful CSS selector syntax you already know (or will learn for CSS). If multiple elements match the selector, it only returns the very first one encountered in the document order.
  • Example:
<p class="info">First paragraph.</p>
<p class="info">Second paragraph.</p>
<div id="container">
<span>Inside div</span>
</div>
<script>
const firstInfoParagraph = document.querySelector('.info'); // Gets the first <p> with class "info"
const spanInContainer = document.querySelector('#container > span'); // Gets the span directly inside #container
const firstParagraphAny = document.querySelector('p'); // Gets the very first <p> tag on the page

console.log(firstInfoParagraph);
console.log(spanInContainer);
</script>

2.2 Selecting Multiple Elements

These methods are used when you expect to find zero, one, or more elements matching your criteria.

document.getElementsByTagName(‘tagName’)

  • Purpose: Selects all elements in the document with the specified tag name.
  • Argument: Takes one argument: a string representing the tag name (e.g., ‘p’, ‘div’, ‘a’).
  • Return Value: Returns a live HTMLCollection of all matching elements. If no elements match, it returns an empty HTMLCollection.
  • Key Point: The collection is live, meaning if elements with that tag name are added or removed from the DOM after you called this method, the collection automatically updates.
  • Example:
<p>Para 1</p>
<a>Link 1</a>
<p>Para 2</p>
<script>
const allParagraphs = document.getElementsByTagName('p');
console.log('Found paragraphs:', allParagraphs); // HTMLCollection [p, p]
console.log('Number of paragraphs:', allParagraphs.length); // 2
</script>

document.getElementsByClassName(‘className’)

  • Purpose: Selects all elements in the document that have the specified class name.
  • Argument: Takes one argument: a string representing the class name (e.g., ‘highlight’, ‘nav-item’). You can specify multiple classes separated by spaces, and it will find elements that have all those classes.
  • Return Value: Returns a live HTMLCollection of all matching elements. Returns an empty collection if none match.
  • Key Point: Also returns a live HTMLCollection.
  • Example:
<div class="box alert">Warning</div>
<span class="box">Info</span>
<p class="alert">Another alert</p>
<script>
const allBoxes = document.getElementsByClassName('box');
console.log('Found boxes:', allBoxes); // HTMLCollection [div.box.alert, span.box]

const allAlerts = document.getElementsByClassName('alert');
console.log('Found alerts:', allAlerts); // HTMLCollection [div.box.alert, p.alert]

const boxAndAlert = document.getElementsByClassName('box alert'); // Elements with BOTH classes
console.log('Found box and alert:', boxAndAlert); // HTMLCollection [div.box.alert]
</script>

document.querySelectorAll(‘cssSelector’)

  • Purpose: Selects all elements within the document that match the specified CSS selector(s).
  • Argument: Takes one argument: a string containing one or more CSS selectors (just like querySelector).
  • Return Value: Returns a static (non-live) NodeList containing all matching Element objects. Returns an empty NodeList if no matches are found.
  • Key Point: Very versatile due to CSS selectors. The returned NodeList is static, meaning it’s a snapshot of the DOM at the time the method was called. It won’t automatically update if the DOM changes later.
  • Example:
<ul>
<li class="item">Apple</li>
<li class="item special">Banana</li>
<li>Orange</li>
</ul>
<script>
const allListItems = document.querySelectorAll('li'); // Selects all <li> elements
const allItemsWithClass = document.querySelectorAll('.item'); // Selects all elements with class "item"
const specialItem = document.querySelectorAll('li.special'); // Selects <li> with class "special"

console.log('All LIs:', allListItems); // NodeList [li.item, li.item.special, li]
console.log('Items with class:', allItemsWithClass); // NodeList [li.item, li.item.special]
</script>

2.3 Understanding HTMLCollection vs. NodeList

It’s important to understand the difference between the two types of collections returned by the methods above:

HTMLCollection (returned by getElementsByTagName, getElementsByClassName)

  • Live: Automatically updates if the underlying document changes (elements are added/removed that match the original criteria). This can sometimes lead to unexpected behavior in loops if you modify the collection while iterating.
  • Contents: Contains only Element nodes.
  • Access: Elements can be accessed by index (e.g., collection[0]) or sometimes by id or name attributes (e.g., collection.namedItem(‘myId’)), though index access is more common.
  • Iteration: Does not have built-in methods like forEach. To iterate easily, you often need to convert it to an array first (e.g., using Array.from(collection) or […collection]).

NodeList (returned by querySelectorAll)

  • Static (Usually): Most commonly encountered NodeLists (like those from querySelectorAll) are static snapshots. They do not update automatically when the DOM changes. (Note: There are some exceptions, like the childNodes property we’ll see later, which returns a live NodeList).
  • Contents: Can contain any type of node (Element, Text, Comment, etc.), though querySelectorAll specifically returns a NodeList containing only Element nodes.
  • Access: Elements are accessed by index (e.g., nodeList[0]).
  • Iteration: Modern browsers provide the forEach method directly on NodeLists, making iteration easier than with HTMLCollection. You can also use for…of loops.

Iterating Example:

<p class="para">One</p>
<p class="para">Two</p>
<p class="para">Three</p>
<script>
// Using querySelectorAll (NodeList) - Preferred for iteration
const parasNodeList = document.querySelectorAll('.para');
parasNodeList.forEach((paragraph, index) => {
console.log(`NodeList Item ${index}:`, paragraph.textContent);
});

// Using getElementsByClassName (HTMLCollection)
const parasHTMLCollection = document.getElementsByClassName('para');

// Option 1: Convert to Array for forEach
Array.from(parasHTMLCollection).forEach((paragraph, index) => {
console.log(`HTMLCollection (via Array.from) Item ${index}:`, paragraph.textContent);
});

// Option 2: Standard for loop
for (let i = 0; i < parasHTMLCollection.length; i++) {
console.log(`HTMLCollection (for loop) Item ${i}:`, parasHTMLCollection[i].textContent);
}
</script>

Chapter 3: Traversing the DOM (Navigating the Tree)

These properties are accessed directly on an element node you’ve already selected. Let’s assume we have a variable element that holds a reference to a DOM element (e.g., const element = document.getElementById(‘myElement’);).

3.1 Moving Up: Parents

element.parentNode

  • Returns: The immediate parent node of the element. This could be an element node, but theoretically, it could also be the document node itself if the element is the root <html> element.
  • Includes: Any node type (Element, Document, DocumentFragment).
  • Example:
<div id="parentDiv">
<p id="childPara">I am a child.</p>
</div>
<script>
const childPara = document.getElementById('childPara');
const parent = childPara.parentNode; // parent will be the <div id="parentDiv"> element
console.log(parent.id); // Output: parentDiv
</script>

element.parentElement

  • Returns: The immediate parent element node of the element.
  • Includes: Only Element nodes. It returns null if the parent is not an element node (e.g., if the parent is the document itself).
  • Why preferred? In most practical scenarios when dealing with HTML structure, you’re interested in the parent element, not just any parent node. parentElement is slightly more specific and often safer. In almost all common cases within the <body>, parentNode and parentElement will return the same element.
  • Example: (Same HTML as above)
const childPara = document.getElementById('childPara');
const parentEl = childPara.parentElement; // parentEl will be the <div id="parentDiv"> element
console.log(parentEl.tagName); // Output: DIV

element.closest(‘cssSelector’)

  • Purpose: Travels up the DOM tree (starting from the element itself, then its parent, grandparent, and so on) and returns the first ancestor element that matches the provided CSS selector.
  • Returns: The matching ancestor Element or null if no match is found (even the document root isn’t matched).
  • Very Useful: Great for finding a specific type of container or ancestor without knowing exactly how many levels up it is.
  • Example:
<div class="container section">
<article id="content">
<p>Some text <strong id="startNode">here</strong>.</p>
</article>
</div>
<script>
const startNode = document.getElementById('startNode');
const containingArticle = startNode.closest('article'); // Finds the <article> element
const containingSection = startNode.closest('.section'); // Finds the <div class="container section">
const nonExistent = startNode.closest('ul'); // Finds nothing, returns null

console.log(containingArticle.id); // Output: content
console.log(containingSection.className); // Output: container section
console.log(nonExistent); // Output: null
</script>

3.2 Moving Down: Children

element.childNodes

  • Returns: A live NodeList containing all child nodes (including element nodes, text nodes, and comment nodes).
  • Includes Whitespace: Be very careful! Whitespace (spaces, tabs, newlines) between HTML tags in your source code often creates text nodes in the DOM. This means childNodes might contain more items than you initially expect.
  • Example:
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li><!-- comment -->
</ul>
<script>
const list = document.getElementById('myList');
console.log(list.childNodes);
// Output will likely be a NodeList containing:
// 1. Text node (newline/whitespace before first <li>)
// 2. <li> element
// 3. Text node (newline/whitespace between <li>s)
// 4. <li> element
// 5. Text node (newline/whitespace after second <li>)
// 6. Comment node
// 7. Text node (newline/whitespace after comment)
</script>

element.children

  • Returns: A live HTMLCollection containing only the child element nodes.
  • Excludes: Text nodes, comment nodes, and whitespace nodes.
  • Generally Preferred: Usually, when you want children, you want the actual HTML elements, not the text/comments in between. children provides this directly.
  • Example: (Same HTML as above)
const list = document.getElementById('myList');
console.log(list.children);
// Output will be an HTMLCollection containing:
// 1. <li id="item1"> element
// 2. <li id="item2"> element
console.log(list.children.length); // Output: 2
console.log(list.children[0].textContent); // Output: Item 1

element.firstChild, element.lastChild

  • Returns: The first or last child node respectively (can be an element, text, or comment node). Returns null if the element has no children.
  • Includes Whitespace: Subject to the same whitespace issues as childNodes.

element.firstElementChild, element.lastElementChild

  • Returns: The first or last child element node respectively. Returns null if the element has no element children.
  • Preferred: Generally more useful than firstChild/lastChild when you specifically need the first/last tag.

3.3 Moving Sideways: Siblings

Siblings are nodes that share the same parent.

element.previousSibling, element.nextSibling

  • Returns: The immediately preceding or succeeding node at the same level in the tree. This can be an element, text, or comment node. Returns null if there is no sibling in that direction.
  • Includes Whitespace: Again, often returns whitespace text nodes if your HTML formatting includes line breaks or spaces between sibling elements.

element.previousElementSibling, element.nextElementSibling

  • Returns: The immediately preceding or succeeding element node at the same level. Skips over any text or comment nodes. Returns null if there is no element sibling in that direction.
  • Preferred: Usually what you want when navigating between adjacent tags.
  • Example:
<div id="container">
<p>First paragraph.</p> <!-- Comment --> <span id="middle">Middle span.</span> <em>Emphasis</em>
</div>
<script>
const middleSpan = document.getElementById('middle');

// Using node siblings (might get comment or text nodes)
console.log(middleSpan.previousSibling); // Might be the comment node or whitespace text node
console.log(middleSpan.nextSibling); // Might be a whitespace text node

// Using element siblings (gets the actual elements)
const prevElem = middleSpan.previousElementSibling;
const nextElem = middleSpan.nextElementSibling;

console.log(prevElem.tagName); // Output: P
console.log(nextElem.tagName); // Output: EM
</script>

Chapter 4: Manipulating Element Content and Structure

4.1 Reading and Changing Text Content

These properties control the text inside an element, ignoring any HTML tags within it.

element.textContent

  • Gets: Returns a string containing the raw text content of the element and all its descendants, without any HTML tags. It includes text from hidden elements and preserves spacing and line breaks as found in the DOM (though rendering might differ).
  • Sets: Replaces the entire content of the element with the provided text string. Any existing child nodes (elements, text, etc.) are removed. Any special HTML characters in the string (like < or &) are treated as literal text and are not parsed as HTML. This makes it safe against Cross-Site Scripting (XSS) attacks when setting text content.
  • Example (Get):
<div id="data">This is <strong>important</strong> text.</div>
<script>
const dataDiv = document.getElementById('data');
console.log(dataDiv.textContent); // Output: "This is important text."
</script>
  • Example (Set):
<p id="message">Initial message.</p>
<script>
const msgPara = document.getElementById('message');
msgPara.textContent = "New message! Less than < 5."; // Replaces content
// The <p> now looks like: <p id="message">New message! Less than < 5.</p>
// The browser displays: New message! Less than < 5.
</script>

element.innerText

  • Gets: Returns a string representing the “rendered” text content of the element. It attempts to mimic what the user sees:
  • It does not include text from hidden elements (e.g., styled with display: none).
  • It attempts to normalize whitespace and line breaks based on layout.
  • Performance: Getting innerText can be slower than textContent because it requires layout awareness.
  • Sets: Similar to textContent, it replaces the content with the provided text string, treating special characters literally.
  • Difference Highlight:
<div id="example">
Hello <span style="display: none;">Secret</span> World!
<style>#example { text-transform: uppercase; }</style>
</div>
<script>
const exDiv = document.getElementById('example');
console.log(exDiv.textContent); // " Hello Secret World! " (includes hidden, respects source whitespace)
console.log(exDiv.innerText); // "HELLO WORLD!" (excludes hidden, reflects CSS transform, collapses whitespace)
</script>
  • Recommendation: Prefer textContent for performance and predictability unless you specifically need the rendered text representation (which is less common for manipulation). For setting text, textContent is generally safer and more efficient.

4.2 Reading and Changing HTML Content

element.innerHTML

  • Gets: Returns a string containing the HTML markup within the element.
  • Sets: Parses the provided string as HTML and replaces the element’s existing content with the resulting nodes.
  • POWERFUL BUT DANGEROUS: Setting innerHTML using content that might come from user input or external sources is a major security risk (Cross-Site Scripting — XSS). A malicious user could inject <script> tags or other harmful HTML that would then be executed in the context of your page.
  • Use Cases: Useful for inserting complex pre-generated HTML snippets or completely replacing the content with new markup. Only use it with trusted HTML sources or after properly sanitizing any untrusted input.
  • Example:
<div id="container">Old content.</div>
<script>
const container = document.getElementById('container');
console.log(container.innerHTML); // Output: "Old content."

// Setting HTML (Use with caution!)
const userProvidedData = "<em>Emphasized!</em>"; // Assume this is safe/sanitized
container.innerHTML = `<h2>New Title</h2><p>${userProvidedData}</p>`;
// The div now contains the h2 and p elements.

// DANGEROUS EXAMPLE (Do not use with untrusted input):
// const maliciousInput = '<img src="x" onerror="alert(\'XSS Attack!\')">';
// container.innerHTML = maliciousInput; // This would execute the alert!
</script>

4.3 Creating New Elements

document.createElement(‘tagName’)

  • Purpose: Creates a new, empty HTML element node with the specified tag name.
  • Argument: A string representing the HTML tag (e.g., ‘p’, ‘div’, ‘img’, ‘a’).
  • Return Value: The newly created element node. This element exists only in memory; it’s not yet part of the visible page (the DOM tree).
  • Example:
const newParagraph = document.createElement('p');
const newImage = document.createElement('img');
const newDiv = document.createElement('div');

// You can now set properties on these elements:
newParagraph.textContent = "This paragraph was created by JS.";
newImage.src = 'path/to/image.jpg';
newImage.alt = 'Description';
newDiv.id = 'js-created-div';

console.log(newParagraph); // Shows the <p> element object in the console
// These elements are not yet visible on the page.

4.4 Adding Elements to the DOM

Once you’ve created an element (or selected an existing one you want to move), you need to insert it into the document tree.

parentElement.appendChild(newElement) (Classic)

  • Action: Adds newElement as the last child of parentElement.
  • Return Value: The appended element (newElement).
  • Note: If newElement already exists elsewhere in the DOM, appendChild will move it from its current location to become the last child of parentElement.
  • Example:
<div id="parent"></div>
<script>
const parent = document.getElementById('parent');
const newPara = document.createElement('p');
newPara.textContent = 'Appended paragraph.';
parent.appendChild(newPara); // Adds the <p> inside the <div>
</script>

parentElement.insertBefore(newElement, referenceElement) (Classic)

  • Action: Inserts newElement into parentElement before referenceElement. referenceElement must be an existing child of parentElement.
  • Return Value: The inserted element (newElement).
  • If referenceElement is null: Acts like appendChild.
  • Example:
<div id="parent">
<p id="second">Second paragraph.</p>
</div>
<script>
const parent = document.getElementById('parent');
const secondPara = document.getElementById('second');
const firstPara = document.createElement('p');
firstPara.textContent = 'First paragraph.';
parent.insertBefore(firstPara, secondPara); // Inserts firstPara before secondPara
</script>

Modern Insertion Methods (More Flexible):

  • element.append(…nodesOrStrings): Appends nodes (element, text) or DOMStrings (text) after the last child of element. Can append multiple items at once. Does not return a value.
  • element.prepend(…nodesOrStrings): Inserts nodes or DOMStrings before the first child of element. Can insert multiple items. Does not return a value.
  • element.before(…nodesOrStrings): Inserts nodes or DOMStrings before element itself (i.e., as a previous sibling). Does not return a value.
  • element.after(…nodesOrStrings): Inserts nodes or DOMStrings after element itself (i.e., as a next sibling). Does not return a value.
  • Example (Modern Methods):
<div id="container">
<p id="marker">Marker paragraph.</p>
</div>
<script>
const container = document.getElementById('container');
const marker = document.getElementById('marker');

const newPara1 = document.createElement('p');
newPara1.textContent = 'Appended para.';
const newPara2 = document.createElement('p');
newPara2.textContent = 'Prepended para.';
const newSpanBefore = document.createElement('span');
newSpanBefore.textContent = 'Span Before ';
const newSpanAfter = document.createElement('span');
newSpanAfter.textContent = ' Span After';

// Append inside container (at the end)
container.append(newPara1, " Text added via append");

// Prepend inside container (at the beginning)
container.prepend(newPara2, "Text added via prepend ");

// Insert before the marker paragraph
marker.before(newSpanBefore, "Text added via before ");

// Insert after the marker paragraph
marker.after(newSpanAfter, " Text added via after");
</script>
  • Advantages of Modern Methods: Can insert multiple nodes/strings at once, can insert strings directly (they become text nodes), more intuitive names (before/after for siblings, prepend/append for children).

4.5 Removing Elements from the DOM

parentElement.removeChild(childElement) (Classic)

  • Action: Removes childElement from its parentElement. childElement must be a direct child of parentElement.
  • Return Value: The removed childElement. The element still exists in memory (if you stored it in a variable), it’s just detached from the DOM.
  • Requires Parent: You need a reference to the parent to remove the child.
  • Example:
<ul id="list">
<li id="item-to-remove">Remove Me</li>
<li>Keep Me</li>
</ul>
<script>
const list = document.getElementById('list');
const itemToRemove = document.getElementById('item-to-remove');
if (list && itemToRemove) {
const removedItem = list.removeChild(itemToRemove);
console.log('Removed:', removedItem); // Logs the removed <li>
}
// Alternative: remove using the parentNode property
// if (itemToRemove && itemToRemove.parentNode) {
// itemToRemove.parentNode.removeChild(itemToRemove);
// }
</script>

element.remove() (Modern)

  • Action: Removes the element directly from the DOM tree it belongs to.
  • Return Value: undefined.
  • Simpler: More direct, as you just call remove() on the element you want to get rid of, without needing an explicit reference to its parent.
  • Example:
<ul id="list">
<li id="item-to-remove">Remove Me</li>
<li>Keep Me</li>
</ul>
<script>
const itemToRemove = document.getElementById('item-to-remove');
if (itemToRemove) {
itemToRemove.remove(); // Simply remove the element directly
}
</script>

4.6 Replacing Elements

parentElement.replaceChild(newElement, oldElement)

  • Action: Replaces the existing oldElement (which must be a child of parentElement) with newElement.
  • Return Value: The oldElement that was replaced.
  • Note: If newElement already exists elsewhere, it’s moved.
  • Example:
<div id="container">
<p id="original">Original paragraph.</p>
</div>
<script>
const container = document.getElementById('container');
const originalP = document.getElementById('original');
const replacementP = document.createElement('p');
replacementP.textContent = 'This is the replacement.';

if (container && originalP) {
container.replaceChild(replacementP, originalP);
}
</script>

4.7 Cloning Elements

element.cloneNode(deep)

  • Purpose: Creates a duplicate of the element node.
  • Argument deep (boolean):
  • true: Clones the element and all of its descendants (children, grandchildren, etc.). Event listeners are not copied.
  • false (or omitted): Clones only the element itself, without any children.
  • Return Value: The cloned node. The clone is not part of the DOM until you explicitly add it (using appendChild, insertBefore, etc.).
  • Example:
<ul id="original-list">
<li>Item 1</li>
<li>Item 2</li>
</ul>
<div id="clone-container"></div>
<script>
const originalList = document.getElementById('original-list');
const cloneContainer = document.getElementById('clone-container');

// Shallow clone (only the <ul> tag, no <li> children)
const shallowClone = originalList.cloneNode(false);
shallowClone.id = 'shallow-clone-list'; // Good practice to change IDs
// cloneContainer.appendChild(shallowClone); // Would add an empty <ul>

// Deep clone (<ul> tag AND its <li> children)
const deepClone = originalList.cloneNode(true);
deepClone.id = 'deep-clone-list'; // Good practice to change IDs
cloneContainer.appendChild(deepClone); // Adds the cloned list with items
</script>

Chapter 5: Working with Attributes and Properties

5.1 Standard HTML Attributes (id, class, src, href, etc.)

For many standard HTML attributes, JavaScript provides direct corresponding properties on the element object.

Direct Property Access (Common & Convenient):

  • You can often access or modify standard attributes as if they were properties of the element object.
  • Examples:
<a id="myLink" href="initial-page.html" target="_blank">Click Me</a>
<img id="myImage" src="old-image.png" alt="An image">
<input id="myInput" type="text" value="Default Text" disabled>
<script>
const link = document.getElementById('myLink');
const img = document.getElementById('myImage');
const input = document.getElementById('myInput');

// Read using properties
console.log(link.href); // Output: Full URL (e.g., http://...)
console.log(img.src); // Output: Full URL (e.g., http://...)
console.log(input.value); // Output: Default Text
console.log(input.disabled); // Output: true (boolean)
console.log(link.id); // Output: myLink

// Modify using properties
link.href = 'new-page.html';
img.alt = 'A new description';
input.value = 'User typed this';
input.disabled = false; // Enable the input
// link.id = 'newId'; // Can change IDs too

// Note: Property names are usually camelCase for multi-word attributes
// e.g., tabIndex, readOnly
</script>
  • Synchronization: For standard attributes, changing the property often (but not always perfectly) updates the underlying attribute, and vice-versa. Boolean attributes like disabled, checked, readonly directly reflect state (true/false).

element.getAttribute(‘attributeName’)

  • Purpose: Explicitly retrieves the value of an HTML attribute as it was defined in the HTML or last set by setAttribute.
  • Argument: A string with the exact attribute name (case-insensitive for HTML, but usually written lowercase).
  • Return Value: A string containing the attribute’s value, or null if the attribute doesn’t exist.
  • Key Difference: Unlike property access, getAttribute(‘href’) or getAttribute(‘src’) usually returns the literal string value from the HTML, not the fully resolved URL. For boolean attributes, it returns the string value (e.g., “true”, “”) or null, not a boolean true/false.
  • Example: (Using the same HTML as above)
console.log(link.getAttribute('href'));   // Output: "initial-page.html" (Literal value)
console.log(img.getAttribute('src')); // Output: "old-image.png" (Literal value)
console.log(input.getAttribute('value'));// Output: "Default Text"
console.log(input.getAttribute('disabled')); // Output: "" (empty string, indicates presence) or null if absent
console.log(link.getAttribute('target')); // Output: "_blank"

element.setAttribute(‘attributeName’, ‘value’)

  • Purpose: Sets the value of an attribute on the specified element. If the attribute already exists, the value is updated; otherwise, a new attribute is added with the specified name and value.
  • Arguments:
  1. attributeName: A string with the attribute name.
  2. value: A string containing the value to set. To set a boolean attribute like disabled or checked, you often set its value to an empty string (“”) or the attribute name itself (e.g., setAttribute(‘disabled’, ‘disabled’)), although setting it to any string technically adds it.
  • Example:
link.setAttribute('href', 'another-page.html');
img.setAttribute('alt', 'Another description');
input.setAttribute('maxlength', '10'); // Add a new attribute
input.setAttribute('disabled', ''); // Disable the input (common way)

element.hasAttribute(‘attributeName’)

  • Purpose: Checks if the element has the specified attribute explicitly set (either in HTML or via setAttribute).
  • Return Value: true if the attribute exists, false otherwise. Useful for boolean attributes where the presence matters.
  • Example:
console.log(input.hasAttribute('disabled')); // Output: true (initially)
console.log(input.hasAttribute('maxlength')); // Output: false (initially)
input.setAttribute('maxlength', '5');
console.log(input.hasAttribute('maxlength')); // Output: true

element.removeAttribute(‘attributeName’)

  • Purpose: Removes the specified attribute from the element.
  • Example:
input.removeAttribute('disabled'); // Enable the input by removing the attribute
console.log(input.hasAttribute('disabled')); // Output: false
console.log(input.disabled); // Output: false (property also updates)

When to Use Which?

  • For standard, well-known HTML attributes (id, src, href, value, disabled, checked, etc.), using the direct DOM properties is usually more convenient, often works with the correct data types (booleans for disabled/checked), and is generally preferred.
  • Use getAttribute / setAttribute / hasAttribute / removeAttribute when:
  • You need the literal attribute value as written in HTML (e.g., relative URL).
  • You are working with custom attributes (though data-* attributes have a better way, see below).
  • You need to explicitly check for the presence of an attribute, regardless of its value (e.g., boolean attributes).
  • You are dealing with non-standard attributes.

5.2 Working with Classes

Manipulating the class attribute is very common for toggling styles or element states.

element.className (Older Way)

  • Gets/Sets: The entire string value of the class attribute.
  • Inconvenient: If an element has multiple classes (e.g., class=”box alert important”), you have to work with the whole space-separated string. Adding or removing a single class requires careful string manipulation (splitting, filtering, joining) which is error-prone.
  • Example (Avoid if possible):
<div id="tester" class="info active">...</div>
<script>
const tester = document.getElementById('tester');
console.log(tester.className); // Output: "info active"

// To add 'hidden' (clumsy):
if (!tester.className.includes('hidden')) {
tester.className += ' hidden'; // Be careful about leading space!
}
// To remove 'active' (even more clumsy):
tester.className = tester.className.replace(/\bactive\b/g, '').trim();
</script>

element.classList (Modern API — Preferred)

  • Returns: A DOMTokenList object, which provides helpful methods for managing classes. This is much easier and safer than manipulating the className string.
  • Methods:
  • classList.add(‘className’, ‘anotherClassName’, …): Adds one or more classes. Duplicates are ignored.
  • classList.remove(‘className’, ‘anotherClassName’, …): Removes one or more classes. Non-existent classes are ignored.
  • classList.toggle(‘className’, forceBoolean): Adds the class if it’s not present, removes it if it is. If the optional forceBoolean is true, it only adds the class (if missing); if false, it only removes the class (if present). Returns true if the class is now present, false otherwise.
  • classList.contains(‘className’): Checks if the element has the specified class. Returns true or false.
  • classList.replace(‘oldClass’, ‘newClass’): Replaces an existing class with a new one.
  • Example (Preferred):
<div id="tester" class="info active">...</div>
<script>
const tester = document.getElementById('tester');

console.log(tester.classList.contains('info')); // Output: true
console.log(tester.classList.contains('warning')); // Output: false

tester.classList.add('hidden', 'extra'); // Adds 'hidden' and 'extra'
console.log(tester.className); // Output: "info active hidden extra"

tester.classList.remove('active'); // Removes 'active'
console.log(tester.className); // Output: "info hidden extra"

tester.classList.toggle('info'); // Removes 'info' because it was present
console.log(tester.classList.contains('info')); // Output: false

tester.classList.toggle('warning'); // Adds 'warning' because it wasn't present
console.log(tester.classList.contains('warning')); // Output: true

tester.classList.toggle('hidden', false); // Forces removal of 'hidden'
console.log(tester.classList.contains('hidden')); // Output: false

tester.classList.replace('warning', 'error'); // Replaces 'warning' with 'error'
console.log(tester.className); // Output: "extra error"
</script>

5.3 Data Attributes (data-*)

HTML5 introduced custom data attributes as a standard way to store private custom data associated with an element.

Syntax: Attributes named data-*, where * is your chosen name (e.g., data-user-id, data-item-name).

Accessing via getAttribute/setAttribute: You can always use the standard attribute methods.

  • element.getAttribute(‘data-user-id’)
  • element.setAttribute(‘data-action’, ‘update’)

Using the element.dataset Property (Convenient):

  • Returns: A DOMStringMap object, which provides property-like access to the data-* attributes.
  • Mapping: The part of the attribute name after data- becomes the property name. Any hyphens (-) in the name are converted to camelCase.
  • data-user-id becomes element.dataset.userId
  • data-item-name becomes element.dataset.itemName
  • data-action becomes element.dataset.action
  • Access: You can read and write these properties directly. Writing adds/updates the underlying data-* attribute. Deleting a property (delete element.dataset.propertyName) removes the attribute.
  • Example:
<div id="product" data-product-id="12345" data-stock-level="15" data-item-category="electronics">...</div>
<script>
const product = document.getElementById('product');

// Read using dataset
console.log(product.dataset.productId); // Output: "12345" (Always strings)
console.log(product.dataset.stockLevel); // Output: "15"
console.log(product.dataset.itemCategory); // Output: "electronics"

// Modify using dataset
product.dataset.stockLevel = '10'; // Update value (still a string)
console.log(product.getAttribute('data-stock-level')); // Output: "10"

// Add new data attribute via dataset
product.dataset.orderStatus = 'processing';
console.log(product.hasAttribute('data-order-status')); // Output: true
console.log(product.getAttribute('data-order-status')); // Output: "processing"

// Remove data attribute via dataset
delete product.dataset.itemCategory;
console.log(product.hasAttribute('data-item-category')); // Output: false
</script>
  • Note: All values read from dataset are strings. If you store numbers, booleans, or JSON, you’ll need to parse/convert them (e.g., using parseInt(), JSON.parse()).

Chapter 6: Manipulating Element Styles

6.1 Setting Inline Styles

You can directly apply CSS styles to an element as if you were setting them using the style attribute in HTML.

The element.style Property:

  • Every HTML element object in the DOM has a style property.
  • This style property is an object (CSSStyleDeclaration) that represents the element’s inline styles (those defined directly in the style=”…” attribute).
  • You can read and set properties on this object to modify the element’s inline style.

Mapping CSS Properties:

  • CSS properties containing hyphens (like background-color, font-size, z-index) are converted to camelCase when used as properties of the style object in JavaScript.
  • background-color becomes backgroundColor
  • font-size becomes fontSize
  • z-index becomes zIndex
  • border-left-width becomes borderLeftWidth
  • Properties without hyphens usually remain the same (e.g., color, width, padding, margin).
  • The float CSS property is a reserved keyword in JavaScript, so it’s accessed via cssFloat (e.g., element.style.cssFloat = ‘left’;).

Setting Values:

  • Style values are assigned as strings, just like in CSS (e.g., ‘10px’, ‘#ff0000’, ‘bold’, ‘block’).
  • If you set a style property to an empty string (‘’), it typically removes that specific inline style, allowing styles from stylesheets to take precedence again.

Example:

<p id="myPara">This is a paragraph.</p>
<button id="styleButton">Change Style</button>

<script>
const para = document.getElementById('myPara');
const btn = document.getElementById('styleButton');

// Reading initial inline styles (often empty unless set in HTML)
console.log('Initial inline color:', para.style.color); // Likely ""

btn.addEventListener('click', () => {
// Setting inline styles
para.style.color = 'blue';
para.style.backgroundColor = '#eee'; // camelCase for background-color
para.style.fontSize = '20px';
para.style.padding = '15px';
para.style.border = '1px solid black'; // Set border shorthand

// The element in the DOM now effectively looks like:
// <p id="myPara" style="color: blue; background-color: rgb(238, 238, 238); font-size: 20px; padding: 15px; border: 1px solid black;">...</p>
});
</script>

Specificity & Limitations:

  • Styles set via element.style are inline styles. In CSS, inline styles have very high specificity, meaning they will usually override styles defined in external or internal stylesheets.
  • Reading Limitation: element.style can only read styles that were set inline (either in the original HTML’s style attribute or previously set using element.style in JS). It cannot read styles applied via CSS rules in stylesheets (<style> tags or linked .css files).
  • Maintainability: Over-reliance on setting inline styles can make your code harder to manage. It mixes presentation logic directly into your JavaScript. For complex styling changes or reusable styles, using CSS classes is generally better.

6.2 Getting Computed Styles

What if you need to know the actual style being applied to an element, considering all sources (inline styles, internal/external stylesheets, browser defaults)? That’s where computed styles come in.

  • window.getComputedStyle(element, [pseudoElement])
  • Purpose: Returns a read-only CSSStyleDeclaration object containing the computed values for all CSS properties of an element after all styling sources have been applied.
  • Arguments:
  1. element: The element whose computed style you want to get.
  2. pseudoElement (Optional): A string specifying a pseudo-element (e.g., ‘::before’, ‘::after’). If omitted or null, gets styles for the element itself.
  • Return Value: A CSSStyleDeclaration object. Property values are typically absolute (e.g., colors as rgb() or rgba(), dimensions often in pixels).
  • Read-Only: You cannot set styles using the object returned by getComputedStyle.
  • Example:
<style>
.styled-div {
width: 150px;
padding: 10px;
color: green;
font-weight: bold; /* Not set inline */
}
</style>
<div id="myDiv" class="styled-div" style="padding: 5px; color: red;">A div</div>

<script>
const div = document.getElementById('myDiv');

// Reading from element.style (only shows inline styles)
console.log('Inline padding:', div.style.padding); // Output: "5px"
console.log('Inline color:', div.style.color); // Output: "red"
console.log('Inline font-weight:', div.style.fontWeight); // Output: "" (empty string)

// Getting the computed styles
const computedStyles = window.getComputedStyle(div);

console.log('Computed padding:', computedStyles.padding); // Output: "5px" (inline overrides stylesheet)
console.log('Computed color:', computedStyles.color); // Output: "rgb(255, 0, 0)" (inline red overrides stylesheet green)
console.log('Computed width:', computedStyles.width); // Output: "150px" (from stylesheet)
console.log('Computed font-weight:', computedStyles.fontWeight); // Output: "700" (bold, from stylesheet - numeric value)
console.log('Computed display:', computedStyles.display); // Output: "block" (browser default for div)

// Getting pseudo-element style (if defined in CSS)
// const beforeStyle = window.getComputedStyle(div, '::before');
// console.log('Computed content for ::before:', beforeStyle.content);
</script>

6.3 Changing Styles by Modifying Classes (Generally Preferred)

While setting inline styles works, the most common and recommended approach for managing dynamic styling is to define styles in your CSS using classes and then use JavaScript to add or remove those classes from elements.

The Concept:

  1. Define States/Looks in CSS: Create CSS rules associated with specific class names (e.g., .active, .hidden, .highlighted, .error).
/* styles.css */
.highlight {
background-color: yellow;
font-weight: bold;
}

.hidden {
display: none;
}

.error-message {
color: red;
border: 1px solid red;
padding: 10px;
}

2. Toggle Classes with JavaScript: Use the element.classList API (from Chapter 5) to add/remove/toggle these classes on your elements based on application logic or user interaction.

<p id="status">Normal status.</p>
<button id="toggleHighlight">Toggle Highlight</button>
<button id="showError">Show Error</button>
<button id="hideStatus">Hide</button>

<script>
const statusPara = document.getElementById('status');
const btnHighlight = document.getElementById('toggleHighlight');
const btnError = document.getElementById('showError');
const btnHide = document.getElementById('hideStatus');

btnHighlight.addEventListener('click', () => {
statusPara.classList.toggle('highlight');
});

btnError.addEventListener('click', () => {
statusPara.textContent = 'An error occurred!';
// Ensure only error class is applied for styling
statusPara.className = 'error-message'; // Reset classes first
// Or more carefully:
// statusPara.classList.remove('highlight', 'hidden'); // Remove others
// statusPara.classList.add('error-message');
});

btnHide.addEventListener('click', () => {
statusPara.classList.add('hidden');
// Or toggle visibility:
// statusPara.classList.toggle('hidden');
});
</script>

Benefits of Using Classes:

  • Separation of Concerns: Keeps presentation logic (CSS) separate from behavioral logic (JavaScript).
  • Maintainability: Styles are defined in one place (CSS file), making them easier to update and manage across the application.
  • Reusability: CSS classes can be reused on multiple elements.
  • Performance: Browsers are highly optimized for applying styles based on class changes. Modifying multiple inline styles individually can sometimes cause more layout recalculations (reflows/repaints) than changing a single class that groups multiple style changes.
  • Readability: Code often becomes cleaner (element.classList.add(‘active’) vs multiple element.style.property = ‘value’ lines).

Chapter 7: Handling Events

7.1 Introduction to Events

What are Events? Events are actions or occurrences that happen in the browser, which your code can respond to (“listen” for). Think of them as signals that something interesting has happened.

Examples:

  • User Actions: Clicking a button, moving the mouse, pressing a key on the keyboard, submitting a form, selecting text.
  • Browser/API Actions: A page finishing loading, an image failing to load, a video starting to play, the browser window being resized, an animation finishing.
  • Event-Driven Programming: Much of front-end JavaScript is event-driven. Instead of running linearly from top to bottom and stopping, the code sets up “listeners” and then waits for events to occur. When an event happens on an element that has a listener for that event type, the associated code (the “event handler” or “event listener function”) is executed.

7.2 Event Listeners: The Modern Approach

The standard and most flexible way to handle events is using the addEventListener method.

element.addEventListener(‘eventName’, functionNameOrCode, [options/useCapture])

  • Purpose: Attaches an event handler function to the specified element. This function will be called whenever the specified event occurs on that element.
  • Arguments:
  1. eventName (string): The type of event to listen for, without the “on” prefix (e.g., ‘click’, ‘mouseover’, ‘keydown’, ‘load’).
  2. functionNameOrCode (function): The function object (or an anonymous function/arrow function) that will be executed when the event occurs. This function automatically receives an Event object as its first argument (often named event or e).
  3. options/useCapture (optional object or boolean):
  • Boolean (Older way): true means use event capturing phase, false (default) means use event bubbling phase (more on this later).
  • Object (Modern way): Allows setting multiple options:

a. capture (boolean): Same as the boolean argument (false default).

b. once (boolean): If true, the listener is automatically removed after it fires once (false default).

c. passive (boolean): If true, indicates the listener will not call preventDefault(). Can improve scrolling performance for events like touchmove or wheel (false default, but some browsers may change defaults).

  • Multiple Listeners: You can add multiple listeners for the same event type on the same element. They will be executed in the order they were added.

element.removeEventListener(‘eventName’, functionName, [options/useCapture])

  • Purpose: Removes an event listener that was previously attached with addEventListener.
  • Important: To successfully remove a listener, you must pass the exact same function reference that was used in addEventListener. This means you cannot easily remove listeners added using anonymous functions unless you stored a reference to that anonymous function (which is unusual). This is a key reason to use named functions if you anticipate needing to remove the listener later.
  • The options/useCapture argument must also match the one used during addEventListener.

Using Anonymous Functions vs. Named Functions:

  • Anonymous Functions (or Arrow Functions): Convenient for simple, self-contained handlers. Cannot be easily removed with removeEventListener.
button.addEventListener('click', () => {
console.log('Button clicked! (anonymous arrow function)');
});
  • Named Functions: Reusable, better for organization, and can be removed using removeEventListener.
function handleButtonClick(event) {
console.log('Button clicked! (named function)');
console.log('Event details:', event);
// Maybe remove the listener after the first click:
// event.currentTarget.removeEventListener('click', handleButtonClick);
}
button.addEventListener('click', handleButtonClick);

// Sometime later...
// button.removeEventListener('click', handleButtonClick); // This works!

7.3 The Event Object (event or e)

When an event occurs and your listener function is called, the browser automatically passes an Event object (or a more specific subclass like MouseEvent, KeyboardEvent) as the first argument to your function. This object contains valuable information about the event.

event.target

  • The actual element on which the event originally occurred (e.g., if you click on a <span> inside a <button>, event.target might be the <span>). This is often the element you’re most interested in.

event.currentTarget

  • The element to which the event listener is currently attached. In the simple case where the listener is directly on the element that was interacted with, target and currentTarget are the same. They differ during event bubbling/capturing (see below).

event.preventDefault()

  • Purpose: Stops the browser from executing the default action associated with that event on that element.
  • Examples:
  • Preventing a click on a submit button from submitting the form (so you can validate first).
  • Preventing a click on a link (<a>) from navigating to the URL.
  • Preventing a key press in a text field from typing the character (less common, but possible).
  • Usage:
const myForm = document.getElementById('myForm');
myForm.addEventListener('submit', (event) => {
console.log('Form submit event triggered.');
if (!isValid(myForm)) { // Assume isValid is your validation function
event.preventDefault(); // Stop the form from actually submitting
console.log('Form submission prevented due to invalid data.');
}
});

event.stopPropagation()

  • Purpose: Stops the event from propagating further up or down the DOM tree (stops bubbling or capturing, see below).
  • Usage: Less common than preventDefault(), but useful in complex UI scenarios to prevent parent elements from reacting to an event that was already handled by a child. Be cautious, as it can sometimes interfere with expected behavior if other scripts rely on the event propagating.
  • Example:
<div id="outer" style="padding: 20px; background: lightblue;">
<button id="inner">Click Me</button>
</div>
<script>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');

outer.addEventListener('click', () => {
console.log('Outer div clicked (Bubbling)');
});

inner.addEventListener('click', (event) => {
console.log('Inner button clicked');
event.stopPropagation(); // Prevent the click from reaching the outer div
});
// Clicking the button will now only log "Inner button clicked"
</script>

7.4 Common Event Types

There are many event types, but here are some of the most frequently used:

Mouse Events:

  • click: A single click.
  • dblclick: A double click.
  • mousedown: Mouse button is pressed down over the element.
  • mouseup: Mouse button is released over the element.
  • mouseover: Mouse pointer moves onto the element or one of its children.
  • mouseout: Mouse pointer moves off the element or one of its children.
  • mouseenter: Mouse pointer moves onto the element itself (doesn’t fire for children). Generally preferred over mouseover for simple hover effects.
  • mouseleave: Mouse pointer moves off the element itself (doesn’t fire when moving to/from children). Generally preferred over mouseout.
  • mousemove: Mouse pointer moves while over the element.

Keyboard Events:

  • keydown: Any key is pressed down. Fires repeatedly if held down. Good for detecting which key is pressed (using event.key or event.code).
  • keyup: A key is released.
  • keypress (Deprecated but still seen): Fires when a key that produces a character value is pressed down. Avoid in modern code; use keydown.

Form Events:

  • submit: Fired on the <form> element when it’s submitted (e.g., by clicking a submit button or pressing Enter in a field). Use event.preventDefault() here for client-side validation.
  • change: Fired when the value of an <input>, <select>, or <textarea> element changes and the element loses focus. For checkboxes and radio buttons, it fires immediately when the checked state changes.
  • input: Fired immediately whenever the value of an <input> or <textarea> changes. Useful for real-time feedback as the user types.
  • focus: Fired when an element receives focus (e.g., clicking in a text field).
  • blur: Fired when an element loses focus.

Window/Document Events:

  • load (usually on window): Fired when the entire page has fully loaded, including all dependent resources like stylesheets and images.
  • DOMContentLoaded (usually on document): Fired when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. This is generally the preferred event for running setup code that needs the DOM to be ready, as it fires earlier than load.
document.addEventListener('DOMContentLoaded', () => {
// DOM is ready, safe to manipulate elements
console.log('DOM fully loaded and parsed');
// Initialize scripts, attach event listeners, etc. here
});
  • resize (on window): Fired when the browser window is resized.
  • scroll (on window or scrollable elements): Fired when the document view or an element has been scrolled.

7.5 Event Bubbling and Capturing (Conceptual Understanding)

When an event occurs on an element (e.g., a click on a button inside a div), it doesn’t just happen on that one element. It goes through phases:

  1. Capturing Phase: The event travels down the DOM tree from the window to the target element’s parent, then to the target element. Listeners attached in the capturing phase (using addEventListener with the third argument as true or { capture: true }) are triggered during this descent. (Less commonly used).
  2. Target Phase: The event reaches the target element itself. Listeners attached directly to the target are triggered.
  3. Bubbling Phase: The event travels back up the DOM tree from the target element to its parent, grandparent, all the way back up to the window. Listeners attached in the bubbling phase (using addEventListener with the third argument as false or omitted, or { capture: false }) are triggered during this ascent. (This is the default and most common behavior).

Why Bubbling Matters: If you click a button inside a div, and both the button and the div have click listeners (in the default bubbling phase), the button’s listener will fire first, and then the div’s listener will fire as the event “bubbles up”.

  • event.stopPropagation() can be called in any phase to prevent the event from continuing its journey (down during capture, or up during bubbling).

7.6 Event Delegation

This is a powerful pattern that leverages event bubbling. Instead of adding individual event listeners to many child elements, you add a single listener to a common ancestor (parent or container element).

How it Works:

  1. Attach an event listener (e.g., for click) to a parent element (like a <ul> or a <div> containing many buttons).
  2. When an event occurs on a child element (e.g., a click on an <li> or a specific button), the event bubbles up to the parent.
  3. The parent’s listener catches the event.
  4. Inside the listener function, use event.target to identify the specific child element that was actually clicked.
  5. Perform the action based on which child (event.target) triggered the event.

Example: Handling clicks on list items in a <ul>.

<ul id="itemList">
<li data-id="1">Item 1 <button class="deleteBtn">X</button></li>
<li data-id="2">Item 2 <button class="deleteBtn">X</button></li>
<li data-id="3">Item 3 <button class="deleteBtn">X</button></li>
<!-- More items might be added dynamically -->
</ul>

<script>
const itemList = document.getElementById('itemList');

itemList.addEventListener('click', (event) => {
console.log('Clicked inside the list!');
console.log('Target:', event.target); // The exact element clicked (e.g., <li> or <button>)
console.log('Current Target:', event.currentTarget); // The <ul> element

// Check if the clicked element was a delete button
if (event.target.classList.contains('deleteBtn')) {
console.log('Delete button clicked!');
// Find the parent LI element to remove
const listItem = event.target.closest('li'); // Use closest() to find the parent li
if (listItem) {
console.log('Removing item with ID:', listItem.dataset.id);
listItem.remove(); // Remove the list item
}
}
// You could add other checks here, e.g., if an <li> itself was clicked
else if (event.target.tagName === 'LI') {
console.log('List item text clicked:', event.target.textContent);
}
});
</script>

Benefits of Event Delegation:

  • Performance: Only one event listener is needed, reducing memory usage, especially for large numbers of interactive elements.
  • Dynamic Elements: If new child elements are added to the parent later (e.g., via JavaScript), the event delegation logic will automatically work for them without needing to attach new listeners. This is a huge advantage.
  • Simplicity: Can simplify code by centralizing event handling logic.

Chapter 8: Working with Forms

8.1 Accessing Form Elements

First, you need references to the form itself and its input elements (often called controls).

Accessing the <form> Element:

  • Use standard methods like document.getElementById(‘myFormId’) or document.querySelector(‘form.search-form’).
  • If a form has a name attribute (<form name=”loginForm”>), you can potentially access it via document.forms.loginForm or document.forms[‘loginForm’] (though using IDs is generally more robust). document.forms is an HTMLCollection of all forms in the document.

Accessing Controls within a Form:

  • formElement.elements: This property of a form element returns an HTMLFormControlsCollection (similar to an HTMLCollection) containing all the form controls (<input>, <textarea>, <select>, <button>, etc.) within that specific form. You can access elements by index or by their name or id attribute.
<form id="dataForm">
<input type="text" name="username" id="user">
<input type="password" name="password">
<button type="submit">Go</button>
</form>
<script>
const myForm = document.getElementById('dataForm');
console.log(myForm.elements); // HTMLFormControlsCollection
const usernameInput = myForm.elements.username; // Access by name
const passwordInput = myForm.elements['password']; // Access by name (bracket notation)
const userInputById = myForm.elements.user; // Access by id
console.log(usernameInput.type); // Output: text
</script>
  • Direct Access by name on the Form: You can often access controls directly as properties of the form element using their name attribute.
const usernameInputDirect = myForm.username; // Same as myForm.elements.username
console.log(usernameInputDirect.name); // Output: username
  • Caution: Avoid using names that clash with standard form properties or methods (like name, elements, submit, reset).
  • Standard Selectors: You can always use getElementById, querySelector, or querySelectorAll scoped to the form or document to find controls.
const passwordInputQS = myForm.querySelector('input[name="password"]');
const usernameInputByIdDoc = document.getElementById('user');

8.2 Getting and Setting Input Values

Text Inputs (<input type=”text”>, password, email, url, number, search, etc.) & Text Areas (<textarea>):

  • Use the value property to get or set the text content.
  • Example:
<form id="loginForm">
Email: <input type="email" name="email" value="test@example.com"><br>
Comments: <textarea name="comments"></textarea>
</form>
<script>
const form = document.getElementById('loginForm');
const emailInput = form.elements.email;
const commentsArea = form.elements.comments;

// Get values
console.log('Email:', emailInput.value); // Output: test@example.com
console.log('Comments:', commentsArea.value); // Output: "" (initially empty)

// Set values
emailInput.value = 'new@example.com';
commentsArea.value = 'Some initial comment text.';
</script>

8.3 Handling Checkboxes and Radio Buttons

These use the checked property.

Checkboxes (<input type=”checkbox”>):

  • checkbox.checked: A boolean property (true if checked, false otherwise). Use this to get or set the checked state.
  • checkbox.value: Represents the value sent to the server if the checkbox is checked (defaults to “on” if the value attribute isn’t explicitly set in HTML). You usually read checked, not value, to determine state.
  • Example:
<input type="checkbox" id="agree" name="terms" value="agreed"> I agree to the terms.
<script>
const agreeCheckbox = document.getElementById('agree');
console.log('Is checked?', agreeCheckbox.checked); // Output: false (initially)
console.log('Checkbox value:', agreeCheckbox.value); // Output: "agreed"

// Check it programmatically
agreeCheckbox.checked = true;
console.log('Is checked now?', agreeCheckbox.checked); // Output: true
</script>

Radio Buttons (<input type=”radio”>):

  • Usually grouped by having the same name attribute. Only one radio button in a group can be checked at a time.
  • radio.checked: Boolean (true if this specific radio button is the one selected in its group, false otherwise).
  • Finding the selected value: You often need to find which radio button within the group is checked to get its value.
  • Example:
<form id="choiceForm">
Choose color:
<input type="radio" name="color" value="red"> Red
<input type="radio" name="color" value="green" checked> Green
<input type="radio" name="color" value="blue"> Blue
</form>
<script>
const form = document.getElementById('choiceForm');
const colorRadios = form.elements.color; // Gets a RadioNodeList

let selectedColorValue = '';
// Option 1: Loop through the NodeList/RadioNodeList
for (const radio of colorRadios) {
if (radio.checked) {
selectedColorValue = radio.value;
break;
}
}
console.log('Selected Color (Loop):', selectedColorValue); // Output: "green"

// Option 2: Use the value property of the RadioNodeList directly (convenient!)
console.log('Selected Color (Direct):', colorRadios.value); // Output: "green"

// Option 3: Use querySelector (if you need the element itself)
const checkedRadio = form.querySelector('input[name="color"]:checked');
if (checkedRadio) {
console.log('Selected Color (QS):', checkedRadio.value); // Output: "green"
}

// Select 'blue' programmatically
colorRadios[2].checked = true; // Select the third radio (index 2)
console.log('New Selected Color (Direct):', colorRadios.value); // Output: "blue"
</script>

8.4 Handling Select Options (<select>)

Dropdown lists require interacting with their <option> elements.

selectElement.value: Gets or sets the value attribute of the currently selected <option>. This is often the most convenient way to get/set the selection for single-select dropdowns.

selectElement.options: An HTMLCollection of all the <option> elements within the <select>.

selectElement.selectedIndex: The index (0-based) of the currently selected <option> within the options collection. Returns -1 if nothing is selected (possible for <select multiple> or initially). You can also set this property to change the selection.

Accessing Option Text: selectElement.options[index].text gives the visible text of the option at that index.

<select multiple>: If the multiple attribute is present, the user can select multiple options.

  • selectElement.value doesn’t work reliably for getting multiple values.
  • You need to iterate through selectElement.options and check the selected boolean property of each option (option.selected).

Example (Single Select):

<select id="countrySelect" name="country">
<option value="">--Please choose an option--</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="MX" selected>Mexico</option>
</select>
<script>
const countrySelect = document.getElementById('countrySelect');

// Get current selection
console.log('Selected value:', countrySelect.value); // Output: "MX"
console.log('Selected index:', countrySelect.selectedIndex); // Output: 3
console.log('Selected text:', countrySelect.options[countrySelect.selectedIndex].text); // Output: "Mexico"

// Change selection programmatically
countrySelect.value = 'CA'; // Easiest way for single select
console.log('New selected value:', countrySelect.value); // Output: "CA"
console.log('New selected index:', countrySelect.selectedIndex); // Output: 2

// Change selection by index
countrySelect.selectedIndex = 1;
console.log('Selected value by index:', countrySelect.value); // Output: "US"
</script>
  • Example (Multiple Select):
<select id="fruitSelect" name="fruits" multiple size="3">
<option value="apple">Apple</option>
<option value="banana" selected>Banana</option>
<option value="orange" selected>Orange</option>
<option value="grape">Grape</option>
</select>
<script>
const fruitSelect = document.getElementById('fruitSelect');
const selectedFruits = [];

for (const option of fruitSelect.options) {
if (option.selected) {
selectedFruits.push(option.value);
}
}
console.log('Selected fruits:', selectedFruits); // Output: ["banana", "orange"]

// Select 'apple' and 'grape' programmatically (deselect others)
for (const option of fruitSelect.options) {
option.selected = (option.value === 'apple' || option.value === 'grape');
}
// Re-check selected values
const newSelectedFruits = Array.from(fruitSelect.options) // Convert HTMLCollection to array
.filter(option => option.selected)
.map(option => option.value);
console.log('Newly selected fruits:', newSelectedFruits); // Output: ["apple", "grape"]
</script>

8.5 Form Submission (submit event)

The most common use case is to intercept the form submission to perform validation or send data using JavaScript (e.g., Fetch API).

Listen on the <form>: Attach the event listener to the <form> element itself, not the submit button. The event fires when the form is submitted via a submit button (<button type=”submit”> or <input type=”submit”>) or by pressing Enter in certain fields.

event.preventDefault(): Call this inside your listener to stop the browser’s default behavior of sending the data to the server and reloading the page.

Gathering Data: Inside the handler, access the values of the form controls as shown above. The FormData object is often very useful here.

Example:

<form id="signupForm" action="/register" method="post">
Username: <input type="text" name="username" required><br>
Password: <input type="password" name="password" required><br>
<button type="submit">Sign Up</button>
<div id="form-message" style="color: red;"></div>
</form>
<script>
const signupForm = document.getElementById('signupForm');
const messageDiv = document.getElementById('form-message');

signupForm.addEventListener('submit', (event) => {
event.preventDefault(); // Stop default submission!
messageDiv.textContent = ''; // Clear previous messages

const username = signupForm.elements.username.value.trim();
const password = signupForm.elements.password.value;

console.log('Submitting via JS...');
console.log('Username:', username);
console.log('Password:', password);

// --- Basic Validation ---
if (username === '' || password === '') {
messageDiv.textContent = 'Username and Password are required.';
return; // Stop processing
}
if (password.length < 8) {
messageDiv.textContent = 'Password must be at least 8 characters long.';
return;
}
// --- End Basic Validation ---

// Option 1: Use FormData (modern & convenient)
const formData = new FormData(signupForm);

// You can inspect FormData
for (let [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}

// Here you would typically use fetch() to send formData to the server
/*
fetch('/register', { // Or signupForm.action
method: 'POST', // Or signupForm.method
body: formData
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
messageDiv.style.color = 'green';
messageDiv.textContent = 'Signup successful!';
})
.catch((error) => {
console.error('Error:', error);
messageDiv.style.color = 'red';
messageDiv.textContent = 'Signup failed. Please try again.';
});
*/
messageDiv.style.color = 'green';
messageDiv.textContent = 'Validation passed! (Submission logic would go here)';


// Option 2: Manually create data object (less common now with FormData)
// const data = { username: username, password: password };
// Use fetch() with JSON.stringify(data) and Content-Type: application/json

});
</script>

Programmatic Submission/Reset:

  • formElement.submit(): Submits the form programmatically. Important: This does not trigger the submit event listener. It bypasses it and submits directly, as if JavaScript wasn’t involved. Use this only if you deliberately want to skip your submit handler logic.
  • formElement.reset(): Resets the form fields to their initial HTML values. This does trigger a reset event on the form, which you can also listen for if needed.

8.6 Basic Form Validation Concepts

Client-side validation provides quick feedback to the user without waiting for the server.

HTML5 Validation Attributes: Leverage built-in browser validation using attributes like:

  • required: Field must not be empty.
  • type=”email” / type=”url” / type=”number”: Basic format checking.
  • minlength, maxlength: For text inputs.
  • min, max, step: For numeric inputs.
  • pattern: Specify a regular expression the value must match.
  • Browsers often provide default UI feedback (popups, outlines) for these. CSS pseudo-classes :valid and :invalid can be used for styling based on validity.

JavaScript Validation:

  • Perform checks within the submit event handler (as shown above) or potentially on input or change events for real-time feedback.
  • Check for empty values (.trim() === ‘’).
  • Check lengths (.length).
  • Use regular expressions (regex.test(value)) for complex patterns (e.g., password strength, specific formats).
  • Compare values (e.g., password confirmation).

Constraint Validation API: A more advanced JS API to interact with HTML5 validation:

  • inputElement.checkValidity(): Returns true if the element meets all its HTML5 constraints, false otherwise. Triggers the invalid event on the element if it’s invalid.
  • inputElement.validity: A ValidityState object with boolean properties indicating specific validation failures (e.g., valueMissing, typeMismatch, patternMismatch, tooShort).
  • inputElement.validationMessage: A localized message describing the validation failure.
  • formElement.checkValidity(): Checks all controls within the form.
  • inputElement.setCustomValidity(‘message’): Allows you to set a custom error message from JS. Set to ‘’ to clear the custom error.

Providing Feedback: Don’t just rely on browser defaults. Use JavaScript to:

  • Display clear error messages near the relevant fields.
  • Add/remove CSS classes (e.g., .error, .invalid) to style invalid fields (borders, icons).
  • Focus the first invalid field.

Important Note: Client-side validation is for user experience, not security. Always re-validate everything on the server-side, as client-side code can be bypassed.

Chapter 9: Advanced Topics & Best Practices

9.1 Performance Considerations (Minimizing DOM Reflows/Repaints)

Directly manipulating the DOM can be computationally expensive because changes can trigger browser rendering updates.

Rendering Steps: When you change something that affects layout or appearance, the browser often goes through steps like:

  1. Recalculate Style: Recomputing the styles for affected elements.
  2. Layout (or Reflow): Calculating the geometry (position and size) of elements. This happens if you change dimensions, position, add/remove elements, or even read certain properties like offsetHeight or getComputedStyle. Reflows can be costly as they might affect a large portion of the DOM tree.
  3. Paint (or Repaint): Filling in the pixels for elements on the screen (colors, images, borders). Happens when visual styles change but layout doesn’t (e.g., changing background-color, color, visibility). Usually less expensive than reflow.
  4. Compositing: Putting the painted layers together on screen.
  • Minimizing Impact: Frequent or unnecessary reflows and repaints can make your page feel sluggish. Strategies include:
  • Batch DOM Changes: Instead of making many small, consecutive changes to the live DOM, try to make them “offline” or group them together. (See Document Fragments below).
  • Avoid Layout Thrashing: Don’t interleave reading layout properties (like element.offsetWidth, element.offsetTop, getComputedStyle()) with writing/setting styles or attributes that affect layout within a loop. Read all necessary values first, then make all the changes.
// BAD: Causes reflow in each loop iteration
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = elements[i].offsetWidth + 10 + 'px'; // Read then write
}

// BETTER: Read all, then write all
const widths = [];
for (let i = 0; i < elements.length; i++) {
widths.push(elements[i].offsetWidth); // Read all widths
}
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = widths[i] + 10 + 'px'; // Write all widths
}

Use Class Toggles: Changing a class often allows the browser to optimize the style recalculation and rendering better than setting multiple individual inline styles.

Animate Efficiently: For animations, prefer CSS Transitions or CSS Animations where possible. If using JavaScript animations (requestAnimationFrame), focus on changing properties that are cheap to update, primarily transform (translate, scale, rotate) and opacity, as these can often be handled by the compositor layer without triggering full layout or paint cycles.

9.2 Using Document Fragments for Efficiency

When you need to add multiple elements (e.g., creating a list or table rows from data), appending each one individually to the live DOM can trigger multiple reflows/repaints. A DocumentFragment provides a solution.

What is it? A DocumentFragment is a minimal, lightweight DOM node container. It acts like a temporary, off-screen staging area.

How it Works:

  1. Create a fragment: const fragment = document.createDocumentFragment();
  2. Create and append your new elements to the fragment (not the live DOM). This step doesn’t cause reflows/repaints on the visible page.
  3. Once all elements are ready in the fragment, append the entire fragment to the actual parent element in the live DOM in a single operation (parentElement.appendChild(fragment);).

Benefit: All the nodes within the fragment are moved into the DOM at once, typically resulting in only a single reflow/repaint for the entire batch, making it much more performant for bulk insertions.

Example:

<ul id="myList"></ul>
<script>
const list = document.getElementById('myList');
const data = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];

const fragment = document.createDocumentFragment(); // Create fragment

data.forEach(itemText => {
const li = document.createElement('li');
li.textContent = itemText;
fragment.appendChild(li); // Append LI to the fragment (off-DOM)
});

// Now, append the entire fragment (with all LIs) to the live list
list.appendChild(fragment); // Only one append to the live DOM
</script>

9.3 Introduction to Shadow DOM (Brief Overview)

Shadow DOM is a key part of the Web Components suite (Custom Elements, HTML Templates, Shadow DOM).

Concept: Allows you to encapsulate the structure and styling of a component. A “shadow root” is attached to a host element, and the DOM subtree attached to the shadow root (the “shadow tree”) is largely isolated from the main document’s DOM (“light DOM”).

Benefits:

  • Encapsulation: Styles defined within the shadow DOM don’t leak out and affect the main page, and styles from the main page (usually) don’t leak in and break the component’s internals. CSS selectors from the outside don’t pierce the shadow boundary.
  • Avoids Naming Conflicts: IDs and class names inside the shadow DOM are scoped locally.
  • Component Reusability: Makes it easier to create self-contained, reusable widgets.
  • Not for General Manipulation: You don’t typically use Shadow DOM for everyday manipulation of existing page elements. It’s specifically for building component-based UIs where encapsulation is desired. Accessing elements inside a shadow root from outside requires specific steps (element.shadowRoot.querySelector(…)).
  • Awareness: It’s good to be aware of Shadow DOM because you might encounter it when working with certain frameworks, third-party libraries, or even native browser elements (like <video> controls) which sometimes use it internally.

9.4 Introduction to Web APIs Interacting with DOM

Beyond the core DOM manipulation methods, browsers offer higher-level APIs that interact with the DOM or help observe changes:

Intersection Observer API: Efficiently detects when an element enters or exits the browser’s viewport (or another specified ancestor element). Great for:

  • Lazy-loading images or content.
  • Implementing infinite scrolling.
  • Triggering animations when elements become visible.
  • Tracking ad visibility.
  • Much more performant than listening to scroll events and calling getBoundingClientRect() repeatedly.

Mutation Observer API: Allows you to watch for changes being made to the DOM tree (e.g., elements being added/removed, attributes changing, text content changing). Useful when you need to react to DOM modifications made by other scripts or dynamically.

Resize Observer API: Lets you react when an element’s size changes (its content rectangle). More reliable than listening to the window’s resize event if you care about specific element dimensions.

(Others): Fullscreen API, Pointer Events API, Web Animations API, etc., all interact with or affect DOM elements in various ways.

9.5 Debugging DOM Manipulation Issues

Debugging is a crucial skill. When your DOM manipulation doesn’t work as expected:

Browser Developer Tools are Key:

  • Elements Panel: Inspect the live DOM state. Did your element get added? Does it have the right attributes/classes/styles? Did you select the correct element? You can edit HTML/CSS live here for quick tests.
  • Console Panel:
  • Check for error messages! Read them carefully.
  • Use console.log() extensively to check the values of your variables (Is the element reference null? What’s the value of element.textContent or element.style.color?). Log element references themselves to inspect them.
  • Test selectors directly: document.querySelector(‘.my-class’) — does it return the element or null?
  • Set breakpoints (debugger; statement in your code or click in the Sources panel) to pause execution and inspect the state of variables and the DOM at specific points. Step through your code line by line.
  • Sources Panel: View your loaded JavaScript files, set breakpoints, watch expressions.
  • Network Panel: Check if scripts or related resources loaded correctly.

Common Pitfalls:

  • Timing Issues: Trying to access an element before the DOM is fully loaded. Wrap your code in a DOMContentLoaded listener (Chapter 7).
  • Selector Errors: Typos in CSS selectors, incorrect specificity, element doesn’t actually exist with that ID/class/tag when the code runs. Log the result of your selector methods!
  • null or undefined Errors: Trying to access a property or call a method on a variable that holds null or undefined (often because getElementById or querySelector failed to find the element). Always check if you successfully got the element reference before using it.
  • HTMLCollection vs. NodeList: Forgetting that getElementsByTagName/ClassName return live collections, or trying to use forEach directly on an HTMLCollection without converting it.
  • Incorrect Event Handling: Listener attached to the wrong element, wrong event name, trying to remove an anonymous listener, event propagation issues (stopPropagation/preventDefault misuse).
  • Scope Issues: Variables not accessible where you expect them (related to core JS understanding).
  • Inline Styles vs. Stylesheets: Forgetting that element.style only reads/writes inline styles, not computed styles from CSS rules. Use getComputedStyle for reading final styles. Forgetting that inline styles have high specificity.

This post is based on interaction with https://aistudio.corp.google.com.

Happy learning !!!

--

--

Dilip Kumar
Dilip Kumar

Written by Dilip Kumar

With 18+ years of experience as a software engineer. Enjoy teaching, writing, leading team. Last 4+ years, working at Google as a backend Software Engineer.

No responses yet