· 16 min read

The Only JavaScript Article You'd Ever Need - 2/7

JavaScript & DOM Manipulation

Welcome back, I hope by now you’ve understood the basics of JavaScript from part 1. Before diving into DOM manipulation, let’s establish some more JavaScript foundations that will serve you well later on down the line.

Some More JS Fundamentals

Template Literals: Better String Handling

Template literals (backticks) provide a cleaner way to create strings with embedded expressions:

const name = "Sarah";
const age = 28;

// Old approach
const message = "Hello, my name is " + name + " and I'm " + age + " years old.";

// Modern approach
const modernMessage = `Hello, my name is ${name} and I'm ${age} years old.`;

Template literals also support multi-line strings without concatenation:

const htmlTemplate = `
  <div class="user-card">
    <h2>${name}</h2>
    <p>Age: ${age}</p>
  </div>
`;

Destructuring: Cleaner Variable Assignment

Destructuring allows you to extract values from arrays and objects more elegantly:

// Array destructuring
const colors = ["red", "green", "blue"];
const [primary, secondary, tertiary] = colors;

// Object destructuring
const user = { name: "John", email: "[email protected]", role: "admin" };
const { name, email } = user;

// With default values
const { theme = "light" } = user; // theme will be "light" if not in user object

Arrow Functions: Concise Function Syntax

Arrow functions provide a more compact syntax for simple functions:

// Traditional function
function add(a, b) {
  return a + b;
}

// Arrow function
const add = (a, b) => a + b;

// With single parameter (parentheses optional)
const double = (x) => x * 2;

// With block body
const processData = (data) => {
  const cleaned = data.filter((item) => item != null);
  return cleaned.map((item) => item.toString());
};

Important: Arrow functions behave differently with the this keyword compared to regular functions. We’ll explore this concept in future articles.

The Spread Operator

The spread operator (...) is invaluable for working with arrays and objects:

// Copying arrays
const originalArray = [1, 2, 3];
const copiedArray = [...originalArray]; // [1, 2, 3]

// Combining arrays
const firstHalf = [1, 2, 3];
const secondHalf = [4, 5, 6];
const combined = [...firstHalf, ...secondHalf]; // [1, 2, 3, 4, 5, 6]

// Copying objects
const originalUser = { name: "Alice", age: 30 };
const updatedUser = { ...originalUser, age: 31 }; // { name: "Alice", age: 31 }

Default Parameters

We touched on this earlier, but it’s worth emphasizing how default parameters prevent undefined-related bugs:

function createUser(name = "Anonymous", role = "user", active = true) {
  return { name, role, active };
}

// All these calls work safely
createUser(); // { name: "Anonymous", role: "user", active: true }
createUser("Bob"); // { name: "Bob", role: "user", active: true }
createUser("Admin", "administrator"); // { name: "Admin", role: "administrator", active: true }

The Document Object Model (DOM)

Early web pages were completely static, you could view content but couldn’t interact with it dynamically. We needed a way for JavaScript to “communicate” with HTML and CSS, to inspect elements, modify content, and respond to user interactions.

Enter the Document Object Model (DOM).

At its core, the DOM is a programming interface for web documents. It represents the page so that programs (like your JavaScript) can change the document structure, style, and content. Think of it as a bridge or a structured, object-oriented representation of your HTML document.

When a web page is loaded, the browser creates a DOM of the page. It constructs a tree-like structure where each HTML element, attribute, and piece of text becomes a “node” in that tree.

  • Document: The root of the tree, representing the entire HTML page.
  • Elements: HTML tags like <body>, <h1>, <p>, <img> are represented as element nodes.
  • Attributes: id, class, src, href are represented as attribute nodes.
  • Text: The actual content within elements is represented as text nodes.

Why is this tree structure important? It defines relationships between different parts of the document: parent-child relationships (e.g., <body> is the parent of <h1> and <p>), and sibling relationships (e.g., <h1> and <p> are siblings). JavaScript can then traverse this tree, find specific nodes, and manipulate them.

The DOM essentially turns your static HTML page into a dynamic, interactive playground. It allows JavaScript to:

  • Access HTML elements: Find specific elements on the page.
  • Change HTML content: Modify text, images, or even entire sections of HTML.
  • Change CSS styles: Dynamically alter the appearance of elements.
  • Add and remove HTML elements: Create new elements or delete existing ones from the page.
  • React to user actions: (We’ll cover this in the next section with events, but it’s the ultimate goal of DOM manipulation).

In short, the DOM is the API that makes your web pages come alive. Without it, JS would be useless for creating interactive frontends.

Finding Your Way Around the Tree

Before you can manipulate an HTML element with JavaScript, you first need to select or reference it from the DOM. JavaScript provides several methods on the global document object to do this.

Consider this basic HTML structure for our examples:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DOM Selection Examples</title>
  </head>
  <body>
    <h1 id="mainHeading">Welcome to the DOM Playground</h1>
    <p class="intro-paragraph">
      This is an introductory paragraph about the DOM.
    </p>
    <p class="intro-paragraph">
      Another paragraph to demonstrate class selection.
    </p>
    <div>
      <img src="placeholder.png" alt="A placeholder image" id="myImage" />
      <button>A simple button</button>
    </div>
  </body>
</html>

document.getElementById()

This is one of the oldest and fastest methods for selecting a single element. It finds an element by its unique id attribute. Since ids are supposed to be unique within an HTML document, this method will always return either a single Element object or null if no element with the specified id is found.

// HTML: <h1 id="mainHeading">welcome</h1>
const headingElement = document.getElementById("mainHeading");
console.log("Selected element by ID:", headingElement);
// Output: Selected element by ID: <h1>...</h1> (the actual HTML element)

const nonExistentElement = document.getElementById("nonExistent");
console.log("Non-existent element by ID:", nonExistentElement);
// Output: Non-existent element by ID: null (no element with that ID)

document.getElementsByClassName()

This method returns an HTMLCollection of all elements in the document that have the specified class name. An HTMLCollection is a live (or “live-updating”) collection, meaning if elements with that class are added or removed from the document after you’ve selected them, the HTMLCollection automatically updates to reflect these changes.

// HTML: <p class="intro-paragraph">...</p>
const introParagraphs = document.getElementsByClassName("intro-paragraph");
console.log("Selected elements by Class Name:", introParagraphs);
// Output: Selected elements by Class Name: HTMLCollection(2) [...]

// You can access elements in the collection using index
console.log("First intro paragraph:", introParagraphs[0]);
// Output: <p class="intro-paragraph">This is an introductory paragraph about the DOM.</p>

// You can iterate over an HTMLCollection
for (let i = 0; i < introParagraphs.length; i++) {
  console.log(`Paragraph ${i + 1}:`, introParagraphs[i]);
}

document.getElementsByTagName()

This method returns an HTMLCollection of all elements with the given tag name (e.g., 'p', 'div', 'img'). Like getElementsByClassName(), it also returns a live HTMLCollection.

// HTML: <p>...</p>, <div>...</div>, <img>...
const allParagraphs = document.getElementsByTagName("p");
console.log("All paragraphs:", allParagraphs);
// Output: All paragraphs: HTMLCollection(2) [...]

const allDivs = document.getElementsByTagName("div");
console.log("All divs:", allDivs);
// Output: All divs: HTMLCollection(1) [...]

document.querySelector()

This is a powerful and versatile method introduced with newer DOM specifications. It returns the first Element within the document that matches the specified CSS selector(s). If no match is found, it returns null.

The power of querySelector lies in its ability to use any valid CSS selector, making your JavaScript selection logic consistent with how you style your elements in CSS.

// Select by ID (like getElementById)
const mainHeading = document.querySelector("#mainHeading");
console.log("Query selected heading (by ID):", mainHeading);
// Output: Query selected heading (by ID): <h1 id="mainHeading">...</h1>

// Select by Class (first one)
const firstIntroParagraph = document.querySelector(".intro-paragraph");
console.log(
  "Query selected first intro paragraph (by Class):",
  firstIntroParagraph
);
// Output: Query selected first intro paragraph (by Class): <p class="intro-paragraph">This is an introductory paragraph...</p>

// Select by Tag Name (first one)
const firstParagraph = document.querySelector("p");
console.log("Query selected first paragraph (by Tag):", firstParagraph);
// Output: Query selected first paragraph (by Tag): <p class="intro-paragraph">This is an introductory paragraph...</p>

// Select using a more complex CSS selector (e.g., div containing an image)
const imageInsideDiv = document.querySelector("div > img");
console.log("Query selected image inside a div:", imageInsideDiv);
// Output: Query selected image inside a div: <img src="placeholder.png"...>

const nonExistentQuery = document.querySelector(".non-existent-class");
console.log("Non-existent query selector:", nonExistentQuery);
// Output: Non-existent query selector: null

document.querySelectorAll()

This method returns a NodeList of all elements in the document that match the specified CSS selector(s). If no matches are found, it returns an empty NodeList.

A crucial distinction: a NodeList returned by querySelectorAll() is static (non-live). This means it’s a snapshot of the DOM at the moment it was queried. If elements matching the selector are added or removed after the NodeList is created, the NodeList will not update. For most purposes, NodeList is array-like and can be easily iterated using for...of or forEach().

// Select all elements with a specific class
const allIntroParagraphs = document.querySelectorAll(".intro-paragraph");
console.log(
  "Query selected all intro paragraphs (by Class):",
  allIntroParagraphs
);
// Output: Query selected all intro paragraphs (by Class): NodeList(2) [...]

// Iterate over the NodeList
for (const p of allIntroParagraphs) {
  console.log("Paragraph content:", p.textContent);
}
// Output:
// Paragraph content: This is an introductory paragraph about the DOM.
// Paragraph content: Another paragraph to demonstrate class selection.

// Select all buttons
const allButtons = document.querySelectorAll("button");
console.log("Query selected all buttons:", allButtons);
// Output: Query selected all buttons: NodeList(1) [...]

// Select elements by attribute presence
const elementsWithId = document.querySelectorAll("[id]");
console.log("All elements with an ID attribute:", elementsWithId);

Which Selector to Use?

Generally, document.querySelector() and document.querySelectorAll() are superior because they offer the flexibility and power of CSS selectors. This allows you to select elements with much more precision and consistency with your styling rules.

  • Use getElementById() if you specifically need to select a single element by its id and know it’s unique. It’s marginally faster for this specific use case.
  • For everything else, lean towards querySelector() (for the first match) and querySelectorAll() (for all matches). They make your selection logic cleaner, more expressive, and more robust across various scenarios.

Manipulating Elements

Once you’ve selected an element, you can modify its content, attributes, and styles using JavaScript.

Changing Text: textContent vs. innerHTML

These properties allow you to get or set the content of an HTML element.

  • element.textContent:

    • Gets or sets the textual content of an element and all its descendants.
    • It treats everything as plain text, meaning any HTML tags within the string you assign to it will be displayed as literal text, not parsed as HTML.
    • Security Benefit: This makes textContent inherently safer against certain attacks, especially when dealing with user-provided input, as it prevents malicious scripts from being injected into your page.
    <p id="myParagraph">Original text content.</p>
    
    const myParagraph = document.getElementById("myParagraph");
    
    // Get text content
    console.log("Original textContent:", myParagraph.textContent); // Output: Original text content.
    
    // Set new text content
    myParagraph.textContent = "New text set by JS.";
    console.log("New textContent:", myParagraph.textContent); // Output: New text set by JS.
    
    myParagraph.textContent = "This is <strong>bold</strong> text.";
    console.log("textContent with HTML tags:", myParagraph.textContent); // Output: This is <strong>bold</strong> text.
    // On the page, you'd literally see the <strong> tags.
    
  • element.innerHTML:

    • Gets or sets the HTML content (including any tags) of an element.

    • When you assign a string containing HTML tags to innerHTML, the browser parses that string as HTML and renders it on the page.

    • Security Risk: Because innerHTML parses the string as HTML, it is vulnerable to certain (XSS) attacks if you assign untrusted (e.g., user-provided) data directly to it. Malicious scripts embedded in the string could be executed. NEVER use innerHTML with untrusted input unless you have thoroughly sanitized it.

      <div id="myDiv">Original div content.</div>
      
      const myDiv = document.getElementById("myDiv");
      
      // Get HTML content
      console.log("Original innerHTML:", myDiv.innerHTML); // Output: Original div content.
      
      // Set new HTML content
      myDiv.innerHTML = "This text is now <em>italic</em> and <u>underlined</u>.";
      console.log("New innerHTML:", myDiv.innerHTML); // Output: This text is now <em>italic</em> and <u>underlined</u>.
      // On the page, "italic" would be italicized and "underlined" would be underlined.
      
      // !!! Potential XSS vulnerability !!!
      // Imagine 'userInput' comes from a form field or API.
      // let maliciousInput = "<img src='x' onerror='alert(\"hackor man!\")'>";
      // myDiv.innerHTML = maliciousInput; // This would execute the alert if uncommented!
      console.log("innerHTML (potential XSS warning!):", myDiv.innerHTML);
      

    Rule of Thumb: Use textContent when you only need to update plain text. Use innerHTML only when you are absolutely sure the content is safe and trusted, or when you are inserting known, controlled HTML structures.

Changing Attributes: setAttribute() and getAttribute()

As you already know, HTML elements have attributes (e.g., src for images, href for links, class, id). You can modify these as well through JS.

  • element.setAttribute(name, value): Sets the value of a specified attribute on an element. If the attribute already exists, its value is updated; otherwise, a new attribute is created.

    <img id="myImage" src="original.png" alt="An original image" />
    <a id="myLink" href="https://oldurl.com">Visit Old Site</a>
    
    const myImage = document.getElementById("myImage");
    const myLink = document.getElementById("myLink");
    
    // Change the 'src' attribute of an image
    myImage.setAttribute("src", "new_image.jpg");
    console.log("Image src changed to:", myImage.getAttribute("src")); // Output: new_image.jpg
    
    // Change the 'alt' attribute
    myImage.setAttribute("alt", "A new descriptive image");
    console.log("Image alt changed to:", myImage.getAttribute("alt")); // Output: A new descriptive image
    
    // Change the 'href' attribute of a link
    myLink.setAttribute("href", "https://newurl.com");
    myLink.setAttribute("target", "_blank"); // Add a new attribute
    console.log("Link href changed to:", myLink.getAttribute("href")); // Output: https://newurl.com
    console.log("Link target attribute:", myLink.getAttribute("target")); // Output: _blank
    
  • element.getAttribute(name): Returns the value of a specified attribute on the element. Returns null if the attribute does not exist.

    console.log("Current image src:", myImage.getAttribute("src")); // Output: new_image.jpg
    console.log(
      "Non-existent attribute:",
      myImage.getAttribute("data-nonexistent")
    ); // Output: null
    

Messing with Styles (element.style)

You can directly modify the inline CSS styles of an element using its style property. This property returns a CSSStyleDeclaration object, which contains all the inline style properties for that element.

Important: When accessing or setting CSS properties via element.style, use camelCase for property names that are typically hyphenated in CSS (e.g., background-color becomes backgroundColor, font-size becomes fontSize).

<p id="styledParagraph">This paragraph will be styled.</p>
const styledParagraph = document.getElementById("styledParagraph");

// Set background color
styledParagraph.style.backgroundColor = "lightblue";
console.log("Background color set to:", styledParagraph.style.backgroundColor); // Output: lightblue

// Set text color
styledParagraph.style.color = "darkblue";

// Set font size
styledParagraph.style.fontSize = "20px";

// Set padding
styledParagraph.style.padding = "15px";

// Multiple properties
styledParagraph.style.border = "2px solid red";

While direct element.style manipulation works, for more complex or theme-based styling, it’s generally recommended to define your styles in a separate CSS stylesheet and then apply or remove CSS classes to elements using element.classList.add(), element.classList.remove(), or element.classList.toggle(). This keeps your styles separated from your JavaScript logic, making your code more maintainable and organized. (We’ll get into classList and its methods when we discuss events and more advanced DOM interactions.)

Creating and Removing Elements

The DOM API also allows you to dynamically build new HTML elements from scratch and inject them into the page, or remove existing ones.

document.createElement(tagName)

This method creates a new Element node with the specified HTML tag name (e.g., 'div', 'p', 'li'). The newly created element is not automatically added to the document. It exists only in JavaScript’s memory until you explicitly append it to an existing element in the DOM.

const newDiv = document.createElement("div");
console.log("Created new div element:", newDiv); // Output: <div></div> (in memory)

const newListItem = document.createElement("li");
console.log("Created new list item element:", newListItem); // Output: <li></li> (in memory)

parentNode.appendChild(childNode)

Once you’ve created an element, or if you have a reference to an existing element, appendChild() allows you to insert it into the document. It appends a node as the last child of a specified parent node.

<ul id="myList">
  <li>Existing List Item 1</li>
</ul>
<div id="containerForNewElements"></div>
const myList = document.getElementById("myList");
const container = document.getElementById("containerForNewElements");

// Create a new list item
const newItem = document.createElement("li");
newItem.textContent = "Dynamically Added List Item";

// Append the new list item to the <ul> element
myList.appendChild(newItem);
console.log("New list item added to UL.");
// The HTML list will now show "Dynamically Added List Item" at the end.

// Create a new paragraph and append it to the container div
const newParagraph = document.createElement("p");
newParagraph.textContent =
  "This paragraph was added dynamically by JavaScript!";
newParagraph.style.color = "purple";
container.appendChild(newParagraph);
console.log("New paragraph added to container div.");

parentNode.removeChild(childNode)

This method removes a specified child node from its parent. To use it, you need a reference to both the parent element and the child element you want to remove.

<div id="parentDiv">
  <p id="childParagraph">This paragraph will be removed.</p>
  <button id="removeButton">Remove Child</button>
</div>
const parentDiv = document.getElementById("parentDiv");
const childParagraph = document.getElementById("childParagraph");

// Before removing
console.log("Child paragraph exists:", parentDiv.contains(childParagraph)); // Output: true

// Remove the child paragraph from its parent div
parentDiv.removeChild(childParagraph);
console.log("Child paragraph removed.");

// After removing
console.log("Child paragraph exists:", parentDiv.contains(childParagraph)); // Output: false (it's gone from the DOM)

// Note: The 'childParagraph' variable still holds a reference to the element in memory,
// but it's no longer part of the document.

Performance Issues to Keep in Mind

Manipulating the DOM is powerful, but it can also be a significant source of performance bottlenecks, especially in complex applications with frequent updates. Browsers have to do a lot of work to render your web page. When you change the DOM, they often have to re-calculate the layout and repaint parts of the screen. These processes are called reflows (or layouts) and repaints. Understanding reflows and repaints is crucial for writing performant DOM manipulation code. For a deeper dive into these concepts, refer to the CSS article in this series.

Key Performance Considerations

  1. Modify Elements “Offline”: If you need to make several changes to an element or a subtree of elements, it’s more efficient to remove the element from the DOM, make all your modifications, and then re-add it. This triggers only one reflow/repaint instead of many. Even better, you can use document.createDocumentFragment() to build up complex structures in memory before appending them to the live DOM.

    const myUL = document.getElementById("myList");
    // Imagine this list has many items.
    // If you add items one by one, each appendChild can trigger a reflow.
    
    // Better: Use a DocumentFragment
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < 100; i++) {
      const li = document.createElement("li");
      li.textContent = `Item ${i + 1}`;
      fragment.appendChild(li); // Appending to fragment doesn't touch the live DOM
    }
    myUL.appendChild(fragment); // Appending the fragment triggers only one reflow
    
  2. Avoid Accessing Computed Styles in Loops: Requesting certain computed styles (like offsetWidth, offsetHeight, getComputedStyle()) inside a loop can be disastrous for performance. These properties require the browser to perform an immediate reflow to give you the up-to-date value, effectively synchronizing layout calculations which are normally asynchronous and optimized.

    const box = document.getElementById("myBox");
    // Bad (forces reflow on each iteration)
    // for (let i = 0; i < 100; i++) {
    //     box.style.width = (box.offsetWidth + 1) + 'px'; // Reading offsetWidth forces layout
    // }
    
    // Better (calculate once, then apply)
    let currentWidth = box.offsetWidth;
    for (let i = 0; i < 100; i++) {
      currentWidth += 1;
    }
    box.style.width = currentWidth + "px";
    

By understanding how the DOM works and keeping these performance considerations in mind, you can write JavaScript that not only makes your web pages dynamic but also keeps them fast and smooth. For comprehensive DOM documentation, refer to the MDN Web Docs.


Building Forward

JS requires both theoretical understanding and practical application. The concepts we’ve explored here (and in the previous part) would build a solid foundation for you, given that you UNDERSTAND them thoroughly (not just learn or follow along).

Most importantly, these fundamentals prepare you for advanced JS concepts like closures, prototypes, asynchronous programming,frameworks (topics I’ll explore in future articles). Each article builds on this foundation, creating the comprehensive knowledge that defines skilled software engineers.

Take time to experiment with these concepts. Build small projects that combine multiple topics—a simple todo app that uses modern syntax, proper error handling, and DOM manipulation. The intersection of these fundamentals is where true understanding develops.

See you aronud, learner :)