Ir al contenido
angel.library

DOM y manipulación de documentos

El DOM es una API del navegador que representa el documento como nodos. querySelector devuelve un elemento o null; querySelectorAll devuelve una NodeList estática. Prefiere textContent para texto no confiable, propiedades para estado actual y DocumentFragment o <template> para construir estructuras antes de insertarlas.

El navegador convierte el HTML en un árbol de objetos llamado Document Object Model. document es la entrada al documento; Node es la base del árbol; Element representa etiquetas; y clases como HTMLElement, HTMLInputElement o HTMLVideoElement añaden capacidades específicas.

document instanceof Document // true
document.body instanceof HTMLElement // true
document.body.nodeType // 1: ELEMENT_NODE
document.nodeType // 9: DOCUMENT_NODE
TipoPuede representarEjemplo
Nodeelemento, texto, comentario o documentoelement.childNodes[0]
Elementuna etiqueta con atributos<article>
HTMLElementelemento HTML con propiedades comuneshidden, dataset, style
elemento especializadocomportamiento propio de una etiquetainput.value, video.play()
DocumentFragmentárbol temporal que no se muestra por sí mismoinserción por lotes

El DOM es una representación viva. Cambiar un objeto conectado modifica la página; eliminarlo o moverlo cambia su posición real, no crea una copia.

Un script clásico sin defer puede ejecutarse antes de que el parser haya creado los elementos posteriores. Los scripts con defer y los módulos se ejecutan después del parseo del HTML, antes de DOMContentLoaded.

<script
type="module"
src="/app.js"
></script>
document.readyState // 'loading', 'interactive' o 'complete'
function start() {
const app = document.querySelector("#app")
if (app) app.textContent = "Aplicación lista"
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start, { once: true })
} else {
start()
}

En un módulo colocado normalmente en el documento rara vez necesitas este patrón; es útil en scripts que pueden cargarse de varias formas.

APIDevuelveColección vivaCaso de uso
getElementById(id)Element o nullno aplicaid conocido
querySelector(selector)primer Element o nullno aplicaselector CSS flexible
querySelectorAll(selector)NodeListno, es una foto estáticavarias coincidencias
getElementsByClassName(name)HTMLCollectionsíintegración con código antiguo
getElementsByTagName(name)HTMLCollectionsícolección viva por etiqueta
const main = document.getElementById("main")
const firstCard = document.querySelector("[data-card]")
const cards = document.querySelectorAll("[data-card]")
main // HTMLElement o null
firstCard // Element o null
cards // NodeList; cards.length puede ser 0
;[...cards] // Array de elementos

Prefiere seleccionar desde el contenedor más cercano para reducir alcance y acoplamiento:

const panel = document.querySelector("[data-panel]")
const panelButtons = panel?.querySelectorAll("button") ?? []
panelButtons.length // cantidad de botones dentro de ese panel

querySelector lanza SyntaxError si recibe un selector inválido. Escapa identificadores externos con CSS.escape() antes de usarlos como parte de un selector.

const unsafeId = "item:42"
document.querySelector(`#${CSS.escape(unsafeId)}`)
// encuentra id="item:42" sin romper el selector
Propiedad o métodoDevuelveIncluye texto/comentarios
parentElementelemento padre o nullno aplica
childrenHTMLCollection vivano
childNodesNodeList vivasí
firstElementChild / lastElementChildelemento o nullno
nextElementSibling / previousElementSiblingelemento o nullno
closest(selector)ancestro más cercano, incluido el propio elementono
matches(selector)booleanono
contains(node)booleanosí
const button = document.querySelector("[data-save]")
const form = button?.closest("form")
button?.matches("button[data-save]") // true si coincide
form?.contains(button) // true
form?.children.length // cantidad de elementos hijos directos

Usa las propiedades terminadas en Element cuando solo te interesan etiquetas. Los saltos de línea del HTML también pueden convertirse en nodos de texto dentro de childNodes.

APIDevuelve¿Muta el documento?Nota
createElement(tag)elemento desconectadonocrea una etiqueta
createTextNode(text)nodo de textonotexto explícito
cloneNode(deep)nodo nuevonono copia listeners agregados con JS
append(...nodes) / prepend(...)undefinedsíacepta nodos y strings
appendChild(node)el nodo insertadosíacepta un solo Node
before(...) / after(...)undefinedsíinserta como hermano
replaceChildren(...)undefinedsíreemplaza todos los hijos
replaceWith(...)undefinedsíreemplaza el nodo
remove()undefinedsídesconecta el nodo
const item = document.createElement("li")
item.className = "result"
item.textContent = "JavaScript"
const inserted = list.appendChild(item)
inserted === item // true
list.lastElementChild === item // true
const copy = item.cloneNode(true)
copy === item // false
copy.textContent // 'JavaScript'

Insertar un nodo que ya está conectado lo mueve; no queda en los dos lugares.

secondList.append(item)
secondList.contains(item) // true
list.contains(item) // false

Un fragmento permite preparar varios nodos fuera del documento. Al insertarlo, sus hijos se mueven al destino y el fragmento queda vacío.

const fragment = document.createDocumentFragment()
for (const product of products) {
const item = document.createElement("li")
item.textContent = product.name
fragment.append(item)
}
list.replaceChildren(fragment)
fragment.childNodes.length // 0
list.children.length // products.length

<template> conserva HTML inerte y reutilizable. Sus hijos viven en template.content.

<template id="product-template">
<article class="product">
<h2 data-name></h2>
</article>
</template>
const template = document.querySelector("#product-template")
const clone = template.content.cloneNode(true)
clone.querySelector("[data-name]").textContent = "Teclado"
container.append(clone)
APIInterpreta HTMLConsidera estilos visualesUso adecuado
textContentnonoleer o escribir texto de forma segura
innerTextnosítexto visible; puede forzar cálculo de layout
innerHTMLsínoplantilla completamente controlada
insertAdjacentHTML(position, html)sínoinsertar HTML controlado sin reemplazar todo
insertAdjacentText(position, text)nonoinsertar texto junto al elemento
const untrusted = "<img src=x onerror=alert(1)>"
output.textContent = untrusted
// se muestra literalmente; no crea una imagen

No pases datos externos a innerHTML ni a insertAdjacentHTML sin una sanitización diseñada para HTML. Escapar una URL o usar JSON.stringify no evita XSS en un contexto HTML.

Los atributos pertenecen al HTML serializado; las propiedades representan el estado actual del objeto. Algunas se reflejan entre sí, pero no siempre tienen el mismo tipo o comportamiento.

APIDevuelve¿Muta?
getAttribute(name)string o nullno
hasAttribute(name)booleanono
setAttribute(name, value)undefinedsí
toggleAttribute(name, force?)booleano finalsí
removeAttribute(name)undefinedsí
classList.add/remove(...)undefinedsí
classList.toggle(name, force?)booleano finalsí
classList.contains(name)booleanono
button.disabled = true
button.disabled // true: propiedad booleana
button.hasAttribute("disabled") // true
button.getAttribute("disabled") // normalmente ''
panel.classList.toggle("is-open", true) // true
panel.dataset.projectId = "42"
panel.dataset.projectId // '42'
panel.getAttribute("data-project-id") // '42'

Todo valor de dataset es texto. No guardes secretos, objetos grandes ni estado que deba ser privado.

element.style representa estilos inline. getComputedStyle devuelve valores calculados. Para estados visuales, cambiar una clase suele separar mejor comportamiento y presentación.

panel.style.setProperty("--progress", "60%")
panel.style.getPropertyValue("--progress") // '60%'
const styles = getComputedStyle(panel)
styles.display // por ejemplo: 'block'
MedidaIncluyeCaso de uso
getBoundingClientRect()posición y tamaño respecto al viewportoverlays y geometría visible
clientWidth/Heightcontenido + paddingespacio interior visible
offsetWidth/Heightpadding + borde + scrollbartamaño de la caja
scrollWidth/Heightcontenido total, incluso oculto por scrolldetectar overflow
scrollTop/Leftdesplazamiento actualleer o cambiar scroll
const rect = panel.getBoundingClientRect()
rect.width // ancho en píxeles CSS
rect.top // distancia al borde superior del viewport
panel.scrollHeight > panel.clientHeight // true si hay overflow vertical

Agrupa primero lecturas y después escrituras. Alternar medidas con cambios de estilo puede forzar layout repetidamente.

const form = document.querySelector("form")
const email = form.elements.namedItem("email")
email instanceof HTMLInputElement // true, si es un input
email.value // valor actual como string
email.validity.valid // resultado de restricciones HTML
form.checkValidity() // booleano
APIDevuelveEfecto
new FormData(form)pares de controles con nameno modifica el formulario
checkValidity()booleanodispara invalid en controles inválidos
reportValidity()booleanoademás muestra mensajes nativos
setCustomValidity(message)undefineddefine o limpia error personalizado
requestSubmit(button?)undefinedvalida y dispara submit
submit()undefinedenvía sin validación ni evento submit; suele evitarse
reset()undefinedvuelve a valores iniciales
const data = new FormData(form)
data.get("email") // string, File o null
data.getAll("interest") // todos los valores repetidos
Object.fromEntries(data) // objeto simple; pierde claves repetidas

La validación del navegador mejora la experiencia, pero el servidor siempre debe validar de nuevo.

dialogButton.focus({ preventScroll: true })
document.activeElement === dialogButton // true
section.scrollIntoView({
behavior: "smooth",
block: "start"
})

No muevas el foco por cada actualización. Hazlo cuando ayude a conservar el contexto: abrir un diálogo, mostrar un error relevante o devolver el foco al control que cerró una interfaz.

function renderProducts(container, products) {
const fragment = document.createDocumentFragment()
for (const product of products) {
const article = document.createElement("article")
const title = document.createElement("h2")
const price = document.createElement("p")
title.textContent = product.name
price.textContent = product.price.toLocaleString("es-CO", {
style: "currency",
currency: "COP"
})
article.dataset.productId = String(product.id)
article.append(title, price)
fragment.append(article)
}
container.replaceChildren(fragment)
}
renderProducts(document.querySelector("#products"), [
{ id: 1, name: "Teclado", price: 120000 }
])
// #products queda con un article, un h2 y un párrafo

Este enfoque trata el nombre como texto, conserva una estructura semántica y realiza una sustitución principal del contenido.