Skip to content

Modifying DOM Elements: Updating Page Content and Attributes

Multiple Ways to Modify Content

DOM element content can be modified in various ways, each with different characteristics and applicable scenarios. Choosing the right method affects not only code simplicity but also security and performance.

textContent

textContent is used to get or set the plain text content of an element, and is the safest content modification method:

javascript
const paragraph = document.querySelector("#intro");

// Get text
console.log(paragraph.textContent);

// Set text
paragraph.textContent = "This is the new content";

textContent gets the text content of the element and all its descendants:

html
<div id="container">
  <p>First paragraph</p>
  <p>Second paragraph</p>
</div>
javascript
const container = document.getElementById("container");
console.log(container.textContent);
// Output:
// "
//   First paragraph
//   Second paragraph
// "

When setting textContent, all child elements are removed, leaving only plain text:

javascript
container.textContent = "All HTML is gone";
// <div id="container">All HTML is gone</div>

Security of textContent

textContent automatically escapes HTML characters to prevent XSS attacks:

javascript
const userInput = '<script>alert("Hacked!")</script>';

// Safe: Script tags display as text
element.textContent = userInput;
// Displays: <script>alert("Hacked!")</script>

innerText

innerText is similar to textContent but has several key differences:

javascript
const element = document.querySelector(".content");

// Get text
console.log(element.innerText);

// Set text
element.innerText = "New text";

textContent vs innerText:

FeaturetextContentinnerText
Returns hidden element textYesNo
Affected by CSSNoYes
Triggers reflowNoYes
Preserves line breaksPreserve original formatDisplay as rendered
PerformanceFasterSlower
html
<div id="example">
  Visible text
  <span style="display: none;">Hidden text</span>
</div>
javascript
const div = document.getElementById("example");

console.log(div.textContent); // "Visible text Hidden text"
console.log(div.innerText); // "Visible text"

In general, textContent is recommended unless you need to retrieve "user-visible" text content.

innerHTML

innerHTML can get or set the HTML content of an element:

javascript
const container = document.querySelector(".container");

// Get HTML
console.log(container.innerHTML);
// "<p>First paragraph</p><p>Second paragraph</p>"

// Set HTML
container.innerHTML = "<h2>New Title</h2><p>New content</p>";

Using innerHTML allows you to create complex DOM structures in one go:

javascript
function renderCard(data) {
  const card = document.createElement("div");
  card.className = "card";

  card.innerHTML = `
    <img src="${data.image}" alt="${data.title}">
    <h3>${data.title}</h3>
    <p>${data.description}</p>
    <button class="btn">Learn More</button>
  `;

  return card;
}

Risks of innerHTML

Setting innerHTML directly with user input poses serious security risks:

javascript
// ❌ Dangerous: XSS attack
const userComment = '<img src=x onerror="stealCookies()">';
element.innerHTML = userComment; // Malicious code will execute

// ✅ Safe: Use textContent
element.textContent = userComment; // Display original text

// ✅ Safe: Manually escape
function escapeHtml(text) {
  const div = document.createElement("div");
  div.textContent = text;
  return div.innerHTML;
}

element.innerHTML = `<p>${escapeHtml(userComment)}</p>`;

outerHTML

outerHTML includes the element itself and its content:

javascript
const paragraph = document.querySelector("p");

console.log(paragraph.outerHTML);
// "<p class="intro">Hello World</p>"

// Replace entire element
paragraph.outerHTML = '<div class="intro">Hello World</div>';

After setting outerHTML, the original variable reference becomes invalid:

javascript
const element = document.querySelector(".old");
element.outerHTML = '<div class="new">Replaced</div>';

console.log(element.className); // Still "old"
// element still points to the removed old element

Attribute Manipulation

DOM elements have various attributes that can be read and modified in multiple ways.

Direct Property Access

Many HTML attributes can be accessed directly as DOM object properties:

javascript
const link = document.querySelector("a");

// Read attributes
console.log(link.href); // Full URL
console.log(link.id); // ID attribute
console.log(link.className); // class attribute

// Set attributes
link.href = "https://example.com";
link.id = "main-link";
link.className = "external-link";

// For images
const img = document.querySelector("img");
console.log(img.src); // Full URL
console.log(img.alt); // alt text
img.src = "/images/new-image.jpg";

// For inputs
const input = document.querySelector("input");
console.log(input.value); // Current value
console.log(input.type); // Input type
console.log(input.disabled); // Whether disabled (boolean)
input.value = "New value";
input.disabled = true;

Note Attribute Name Differences

Some HTML attribute names differ from JavaScript property names:

HTML AttributeJavaScript Property
classclassName or classList
forhtmlFor
readonlyreadOnly
maxlengthmaxLength
tabindextabIndex
javascript
const label = document.querySelector("label");
console.log(label.htmlFor); // Corresponds to HTML's for attribute

const input = document.querySelector("input");
console.log(input.readOnly); // Corresponds to HTML's readonly attribute

getAttribute and setAttribute

For non-standard attributes or when you need to get original attribute values, use these methods:

javascript
const element = document.querySelector(".card");

// Get attributes
const role = element.getAttribute("role");
const ariaLabel = element.getAttribute("aria-label");
const customAttr = element.getAttribute("data-custom");

// Set attributes
element.setAttribute("role", "button");
element.setAttribute("aria-label", "Click to expand");
element.setAttribute("data-id", "12345");

// Check if attribute exists
if (element.hasAttribute("disabled")) {
  console.log("Element is disabled");
}

// Remove attribute
element.removeAttribute("disabled");

getAttribute vs Direct Property Access

The two methods may return different values:

javascript
const link = document.querySelector("a");
// HTML: <a href="/about">About</a>

console.log(link.href); // "https://example.com/about" (full URL)
console.log(link.getAttribute("href")); // "/about" (original value)

const input = document.querySelector('input[type="checkbox"]');
// HTML: <input type="checkbox" checked>

console.log(input.checked); // true (boolean)
console.log(input.getAttribute("checked")); // "" or "checked" (string)

Boolean Attributes

For boolean attributes like disabled, checked, readonly, direct property access returns boolean values:

javascript
const checkbox = document.querySelector('input[type="checkbox"]');

// Use boolean values
checkbox.checked = true;
checkbox.disabled = false;

// ❌ Don't do this (sets to string "false", but still treated as true)
checkbox.setAttribute("checked", false);

// ✅ Correct way to remove
checkbox.removeAttribute("checked");

data-* Custom Attributes

HTML5 introduced data-* attributes for storing custom data, accessible through dataset:

html
<div
  id="user-card"
  data-user-id="123"
  data-role="admin"
  data-last-login="2024-01-15"
  data-is-active="true"
></div>
javascript
const card = document.getElementById("user-card");

// Read data attributes
console.log(card.dataset.userId); // "123"
console.log(card.dataset.role); // "admin"
console.log(card.dataset.lastLogin); // "2024-01-15"
console.log(card.dataset.isActive); // "true"

// Set data attributes
card.dataset.userId = "456";
card.dataset.newAttr = "value";

// Delete data attributes
delete card.dataset.role;

Note the attribute name conversion rules:

  • HTML: data-user-id → JS: dataset.userId
  • HTML: data-last-login → JS: dataset.lastLogin
  • Kebab-case converts to camelCase

attributes Collection

You can iterate through all attributes of an element:

javascript
const element = document.querySelector(".example");

// Iterate through all attributes
for (const attr of element.attributes) {
  console.log(`${attr.name} = ${attr.value}`);
}

// Get attribute count
console.log(element.attributes.length);

// Access by index
console.log(element.attributes[0].name);
console.log(element.attributes[0].value);

// Access by name
console.log(element.attributes.getNamedItem("class").value);

Node Manipulation

Besides modifying content and attributes, you can also manipulate the nodes themselves.

Removing Nodes

javascript
const element = document.querySelector(".to-remove");

// Modern method
element.remove();

// Traditional method (compatible with old browsers)
element.parentNode.removeChild(element);

Replacing Nodes

javascript
const oldElement = document.querySelector(".old");
const newElement = document.createElement("div");
newElement.textContent = "New content";

// Modern method
oldElement.replaceWith(newElement);

// Traditional method
oldElement.parentNode.replaceChild(newElement, oldElement);

Moving Nodes

Adding a node to a new location automatically removes it from its original location:

javascript
const item = document.querySelector("#item");
const newContainer = document.querySelector("#new-container");

// Move to new location
newContainer.appendChild(item);

Clearing Element Content

javascript
const container = document.querySelector(".container");

// Method 1: Set innerHTML
container.innerHTML = "";

// Method 2: Set textContent
container.textContent = "";

// Method 3: Remove child nodes one by one
while (container.firstChild) {
  container.removeChild(container.firstChild);
}

// Method 4: Use replaceChildren (modern browsers)
container.replaceChildren();

Practical Application Examples

Real-time Form Feedback

javascript
class FormValidator {
  constructor(formId) {
    this.form = document.getElementById(formId);
    this.init();
  }

  init() {
    const inputs = this.form.querySelectorAll("input, textarea");

    inputs.forEach((input) => {
      input.addEventListener("input", () => this.validateField(input));
      input.addEventListener("blur", () => this.validateField(input));
    });
  }

  validateField(input) {
    const errorElement = this.getOrCreateError(input);
    const rules = input.dataset.rules?.split("|") || [];

    for (const rule of rules) {
      const error = this.checkRule(input, rule);
      if (error) {
        this.showError(input, errorElement, error);
        return false;
      }
    }

    this.hideError(input, errorElement);
    return true;
  }

  checkRule(input, rule) {
    const value = input.value.trim();

    if (rule === "required" && !value) {
      return "This field is required";
    }

    if (rule === "email" && value) {
      const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      if (!emailPattern.test(value)) {
        return "Please enter a valid email address";
      }
    }

    if (rule.startsWith("min:")) {
      const min = parseInt(rule.split(":")[1]);
      if (value.length < min) {
        return `Minimum ${min} characters required`;
      }
    }

    if (rule.startsWith("max:")) {
      const max = parseInt(rule.split(":")[1]);
      if (value.length > max) {
        return `Maximum ${max} characters allowed`;
      }
    }

    return null;
  }

  getOrCreateError(input) {
    let errorElement = input.nextElementSibling;

    if (!errorElement || !errorElement.classList.contains("field-error")) {
      errorElement = document.createElement("span");
      errorElement.className = "field-error";
      input.after(errorElement);
    }

    return errorElement;
  }

  showError(input, errorElement, message) {
    input.classList.add("invalid");
    input.classList.remove("valid");
    input.setAttribute("aria-invalid", "true");

    errorElement.textContent = message;
    errorElement.style.display = "block";
  }

  hideError(input, errorElement) {
    input.classList.remove("invalid");
    input.classList.add("valid");
    input.removeAttribute("aria-invalid");

    errorElement.textContent = "";
    errorElement.style.display = "none";
  }
}

// Usage
// <input data-rules="required|email" type="email" name="email">
const validator = new FormValidator("signup-form");

Content Editor

javascript
class SimpleEditor {
  constructor(containerId) {
    this.container = document.getElementById(containerId);
    this.init();
  }

  init() {
    // Create editor structure
    this.container.innerHTML = `
      <div class="toolbar">
        <button data-command="bold" title="Bold"><b>B</b></button>
        <button data-command="italic" title="Italic"><i>I</i></button>
        <button data-command="underline" title="Underline"><u>U</u></button>
        <button data-command="h1" title="Heading 1">H1</button>
        <button data-command="h2" title="Heading 2">H2</button>
        <button data-command="ul" title="Bullet List">• List</button>
      </div>
      <div class="editor" contenteditable="true"></div>
    `;

    this.editor = this.container.querySelector(".editor");
    this.toolbar = this.container.querySelector(".toolbar");

    this.toolbar.addEventListener("click", (e) => {
      const button = e.target.closest("button");
      if (button) {
        this.executeCommand(button.dataset.command);
      }
    });

    this.editor.addEventListener("input", () => {
      this.onContentChange();
    });
  }

  executeCommand(command) {
    this.editor.focus();

    switch (command) {
      case "bold":
        document.execCommand("bold", false, null);
        break;
      case "italic":
        document.execCommand("italic", false, null);
        break;
      case "underline":
        document.execCommand("underline", false, null);
        break;
      case "h1":
        document.execCommand("formatBlock", false, "h1");
        break;
      case "h2":
        document.execCommand("formatBlock", false, "h2");
        break;
      case "ul":
        document.execCommand("insertUnorderedList", false, null);
        break;
    }
  }

  onContentChange() {
    // Auto-save or trigger event
    const content = this.getContent();
    this.container.dispatchEvent(
      new CustomEvent("contentchange", {
        detail: { content },
      })
    );
  }

  getContent() {
    return this.editor.innerHTML;
  }

  setContent(html) {
    this.editor.innerHTML = html;
  }

  getPlainText() {
    return this.editor.textContent;
  }
}

// Usage
const editor = new SimpleEditor("editor-container");
editor.container.addEventListener("contentchange", (e) => {
  console.log("Content changed:", e.detail.content);
});

Dynamic Table Editing

javascript
class EditableTable {
  constructor(tableId) {
    this.table = document.getElementById(tableId);
    this.init();
  }

  init() {
    this.table.addEventListener("dblclick", (e) => {
      const cell = e.target.closest("td");
      if (cell && !cell.classList.contains("editing")) {
        this.startEdit(cell);
      }
    });

    this.table.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && e.target.tagName === "INPUT") {
        e.preventDefault();
        this.finishEdit(e.target.closest("td"));
      }
      if (e.key === "Escape" && e.target.tagName === "INPUT") {
        this.cancelEdit(e.target.closest("td"));
      }
    });

    this.table.addEventListener(
      "blur",
      (e) => {
        if (e.target.tagName === "INPUT") {
          this.finishEdit(e.target.closest("td"));
        }
      },
      true
    );
  }

  startEdit(cell) {
    const currentValue = cell.textContent;
    cell.dataset.originalValue = currentValue;
    cell.classList.add("editing");

    const input = document.createElement("input");
    input.type = "text";
    input.value = currentValue;
    input.className = "cell-editor";

    cell.textContent = "";
    cell.appendChild(input);
    input.focus();
    input.select();
  }

  finishEdit(cell) {
    if (!cell || !cell.classList.contains("editing")) return;

    const input = cell.querySelector("input");
    const newValue = input.value;
    const oldValue = cell.dataset.originalValue;

    cell.classList.remove("editing");
    cell.textContent = newValue;
    delete cell.dataset.originalValue;

    if (newValue !== oldValue) {
      this.onCellChange(cell, oldValue, newValue);
    }
  }

  cancelEdit(cell) {
    if (!cell || !cell.classList.contains("editing")) return;

    cell.classList.remove("editing");
    cell.textContent = cell.dataset.originalValue;
    delete cell.dataset.originalValue;
  }

  onCellChange(cell, oldValue, newValue) {
    const row = cell.parentElement;
    const rowIndex = row.rowIndex;
    const cellIndex = cell.cellIndex;

    console.log(
      `Cell [${rowIndex}, ${cellIndex}] changed from "${oldValue}" to "${newValue}"`
    );

    // Trigger custom event
    this.table.dispatchEvent(
      new CustomEvent("cellchange", {
        detail: { cell, rowIndex, cellIndex, oldValue, newValue },
      })
    );
  }

  addRow(data) {
    const row = this.table.insertRow();

    data.forEach((value) => {
      const cell = row.insertCell();
      cell.textContent = value;
    });

    return row;
  }

  deleteRow(rowIndex) {
    this.table.deleteRow(rowIndex);
  }

  getData() {
    const data = [];
    const rows = this.table.querySelectorAll("tbody tr");

    rows.forEach((row) => {
      const rowData = [];
      row.querySelectorAll("td").forEach((cell) => {
        rowData.push(cell.textContent);
      });
      data.push(rowData);
    });

    return data;
  }
}

// Usage
const table = new EditableTable("data-table");
table.table.addEventListener("cellchange", (e) => {
  console.log("Cell changed:", e.detail);
});

Dynamic Property Toggling

javascript
class ToggleManager {
  constructor() {
    this.init();
  }

  init() {
    document.addEventListener("click", (e) => {
      // Handle collapse panels
      if (e.target.matches('[data-toggle="collapse"]')) {
        this.toggleCollapse(e.target);
      }

      // Handle tabs
      if (e.target.matches('[data-toggle="tab"]')) {
        this.toggleTab(e.target);
      }

      // Handle modals
      if (e.target.matches('[data-toggle="modal"]')) {
        this.toggleModal(e.target);
      }
    });
  }

  toggleCollapse(trigger) {
    const targetId = trigger.dataset.target;
    const target = document.querySelector(targetId);

    if (!target) return;

    const isExpanded = trigger.getAttribute("aria-expanded") === "true";

    trigger.setAttribute("aria-expanded", !isExpanded);
    target.setAttribute("aria-hidden", isExpanded);
    target.classList.toggle("collapsed", isExpanded);
  }

  toggleTab(trigger) {
    const targetId = trigger.dataset.target;
    const tabContainer = trigger.closest(".tabs");

    if (!tabContainer) return;

    // Remove all active states
    tabContainer.querySelectorAll('[data-toggle="tab"]').forEach((tab) => {
      tab.classList.remove("active");
      tab.setAttribute("aria-selected", "false");
    });

    tabContainer.querySelectorAll(".tab-panel").forEach((panel) => {
      panel.classList.remove("active");
      panel.setAttribute("aria-hidden", "true");
    });

    // Activate current tab
    trigger.classList.add("active");
    trigger.setAttribute("aria-selected", "true");

    const panel = document.querySelector(targetId);
    if (panel) {
      panel.classList.add("active");
      panel.setAttribute("aria-hidden", "false");
    }
  }

  toggleModal(trigger) {
    const targetId = trigger.dataset.target;
    const modal = document.querySelector(targetId);

    if (!modal) return;

    const isOpen = modal.classList.contains("open");

    if (isOpen) {
      modal.classList.remove("open");
      modal.setAttribute("aria-hidden", "true");
      document.body.classList.remove("modal-open");
    } else {
      modal.classList.add("open");
      modal.setAttribute("aria-hidden", "false");
      document.body.classList.add("modal-open");

      // Focus on modal
      modal.focus();
    }
  }
}

// Initialize
new ToggleManager();

Performance Optimization Recommendations

Avoid Frequent DOM Reads/Writes

javascript
// ❌ Bad: Read/write DOM in every loop
for (let i = 0; i < 100; i++) {
  element.style.left = element.offsetLeft + 1 + "px";
}

// ✅ Good: Read once, write at the end
let left = element.offsetLeft;
for (let i = 0; i < 100; i++) {
  left += 1;
}
element.style.left = left + "px";

Use textContent Instead of innerHTML

javascript
// ❌ Slower and has security risks
element.innerHTML = userInput;

// ✅ Faster and safer
element.textContent = userInput;

Batch Modify Attributes

javascript
// ❌ Triggers multiple reflows
element.style.width = "100px";
element.style.height = "100px";
element.style.padding = "10px";
element.style.margin = "20px";

// ✅ Use cssText to set all at once
element.style.cssText =
  "width: 100px; height: 100px; padding: 10px; margin: 20px;";

// ✅ Or use CSS class
element.className = "box-style";

Summary

Modifying DOM elements is a routine operation in web development. Mastering the right methods makes code safer and more efficient:

NeedRecommended Method
Set plain texttextContent
Set HTMLinnerHTML (escape user input)
Read/write standard attributesDirect property access (like element.id)
Read/write custom attributesdataset
Manipulate arbitrary attributesgetAttribute / setAttribute
Remove elementelement.remove()
Replace elementelement.replaceWith(newElement)

Best practices:

  1. Use textContent instead of innerHTML for user input
  2. Minimize DOM access during batch operations
  3. Use dataset to store custom data
  4. Note attribute name differences between HTML and JavaScript