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:
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:
<div id="container">
<p>First paragraph</p>
<p>Second paragraph</p>
</div>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:
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:
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:
const element = document.querySelector(".content");
// Get text
console.log(element.innerText);
// Set text
element.innerText = "New text";textContent vs innerText:
| Feature | textContent | innerText |
|---|---|---|
| Returns hidden element text | Yes | No |
| Affected by CSS | No | Yes |
| Triggers reflow | No | Yes |
| Preserves line breaks | Preserve original format | Display as rendered |
| Performance | Faster | Slower |
<div id="example">
Visible text
<span style="display: none;">Hidden text</span>
</div>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:
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:
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:
// ❌ 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:
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:
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 elementAttribute 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:
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 Attribute | JavaScript Property |
|---|---|
| class | className or classList |
| for | htmlFor |
| readonly | readOnly |
| maxlength | maxLength |
| tabindex | tabIndex |
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 attributegetAttribute and setAttribute
For non-standard attributes or when you need to get original attribute values, use these methods:
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:
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:
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:
<div
id="user-card"
data-user-id="123"
data-role="admin"
data-last-login="2024-01-15"
data-is-active="true"
></div>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:
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
const element = document.querySelector(".to-remove");
// Modern method
element.remove();
// Traditional method (compatible with old browsers)
element.parentNode.removeChild(element);Replacing Nodes
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:
const item = document.querySelector("#item");
const newContainer = document.querySelector("#new-container");
// Move to new location
newContainer.appendChild(item);Clearing Element Content
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
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
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
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
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
// ❌ 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
// ❌ Slower and has security risks
element.innerHTML = userInput;
// ✅ Faster and safer
element.textContent = userInput;Batch Modify Attributes
// ❌ 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:
| Need | Recommended Method |
|---|---|
| Set plain text | textContent |
| Set HTML | innerHTML (escape user input) |
| Read/write standard attributes | Direct property access (like element.id) |
| Read/write custom attributes | dataset |
| Manipulate arbitrary attributes | getAttribute / setAttribute |
| Remove element | element.remove() |
| Replace element | element.replaceWith(newElement) |
Best practices:
- Use
textContentinstead ofinnerHTMLfor user input - Minimize DOM access during batch operations
- Use
datasetto store custom data - Note attribute name differences between HTML and JavaScript