El DOM
Has aprendido JavaScript: variables, funciones, arrays, objetos. Pero hasta ahora todo pasaba en la consola. La gracia de JavaScript en el navegador es que puede modificar la página en tiempo real: cambiar textos, añadir elementos, quitar clases, mostrar y ocultar cosas. El puente entre tu código JS y la página HTML se llama DOM (Document Object Model). Esta lección te enseña a cruzar ese puente.
Qué es el DOM
Cuando el navegador carga tu HTML, crea una representación en memoria de toda la página como un árbol de objetos. Cada etiqueta HTML se convierte en un nodo que puedes leer y modificar con JavaScript. Eso es el DOM.
<!-- El HTML que escribes -->
<body>
<header>
<h1>Mi App</h1>
</header>
<main>
<p>Hola mundo</p>
</main>
</body>
<!-- El navegador lo convierte en un árbol:
document
└── body
├── header
│ └── h1 → "Mi App"
└── main
└── p → "Hola mundo"
Y tú puedes acceder a cada nodo con JavaScript.
-->
querySelector: seleccionar elementos
Olvídate de getElementById y getElementsByClassName. Hoy usamos querySelector y querySelectorAll, que aceptan cualquier selector CSS que ya conoces:
<header class="header">
<h1 id="titulo-principal">GameTracker</h1>
<nav>
<a href="#inicio" class="nav-link active">Inicio</a>
<a href="#biblioteca" class="nav-link">Biblioteca</a>
<a href="#perfil" class="nav-link">Perfil</a>
</nav>
</header>
<main>
<div class="game-card" data-id="1">
<h2>The Legend of Zelda</h2>
<span class="badge">Aventura</span>
</div>
<div class="game-card" data-id="2">
<h2>Hades</h2>
<span class="badge">Roguelike</span>
</div>
</main>
// querySelector: devuelve el PRIMER elemento que coincide
const titulo = document.querySelector("#titulo-principal");
console.log(titulo.textContent); // "GameTracker"
const primerLink = document.querySelector(".nav-link");
console.log(primerLink.textContent); // "Inicio"
// Selectores CSS complejos — igual que en CSS
const linkActivo = document.querySelector(".nav-link.active");
const primerBadge = document.querySelector(".game-card .badge");
// querySelectorAll: devuelve TODOS los que coinciden (NodeList)
const todosLosLinks = document.querySelectorAll(".nav-link");
console.log(todosLosLinks.length); // 3
// Iterar sobre los resultados
todosLosLinks.forEach(link => {
console.log(link.textContent);
});
// "Inicio", "Biblioteca", "Perfil"
// Seleccionar por atributo data-*
const juego2 = document.querySelector('[data-id="2"]');
console.log(juego2.querySelector("h2").textContent); // "Hades"
querySelectorAlldevuelve unaNodeList, no un array. Puedes usarforEachdirectamente, pero si necesitasmap,filter, etc., conviértela con[...nodeList]oArray.from(nodeList).
Modificar texto: textContent vs innerHTML
const titulo = document.querySelector("#titulo-principal");
// textContent: cambia solo el texto (seguro)
titulo.textContent = "GameTracker Pro";
// innerHTML: interpreta HTML (¡cuidado!)
const contenedor = document.querySelector("main");
contenedor.innerHTML = "<p>Cargando juegos...</p>";
// ⚠️ PELIGRO con innerHTML y datos de usuario:
const nombreUsuario = '<img src="x" onerror="alert('hackeado')">'
// contenedor.innerHTML = nombreUsuario; // ❌ ¡Ejecutaría código malicioso! (XSS)
// contenedor.textContent = nombreUsuario; // ✅ Lo muestra como texto plano, sin ejecutar
Regla de oro: usa
textContentpara texto que venga de usuarios o fuentes externas. Solo usainnerHTMLcon HTML que tú controles al 100%.
classList: manejar clases CSS
Cambiar clases es la forma más limpia de modificar el aspecto de un elemento. En lugar de manipular estilos directamente, añades o quitas clases que ya tienes definidas en tu CSS:
const card = document.querySelector(".game-card");
// add: añadir una clase
card.classList.add("destacada");
// remove: quitar una clase
card.classList.remove("destacada");
// toggle: si la tiene la quita, si no la tiene la añade
card.classList.toggle("favorita");
// contains: comprobar si tiene una clase (devuelve true/false)
if (card.classList.contains("favorita")) {
console.log("¡Es un juego favorito!");
}
// Añadir varias clases a la vez
card.classList.add("animada", "visible", "nueva");
// Reemplazar una clase por otra
card.classList.replace("nueva", "antigua");
Ejemplo: theme switcher (modo oscuro / claro)
<body class="dark-mode">
<header>
<h1>Mi App</h1>
<button id="btn-tema">
<span class="icono-tema">🌙</span>
Cambiar tema
</button>
</header>
<main>
<p>Contenido de la aplicación</p>
</main>
</body>
/* Modo oscuro (por defecto) */
body.dark-mode {
background: #0a0a0a;
color: #e0e0e0;
}
/* Modo claro */
body.light-mode {
background: #ffffff;
color: #1a1a1a;
}
/* Transición suave entre modos */
body {
transition: background-color 0.3s, color 0.3s;
}
const btnTema = document.querySelector("#btn-tema");
const iconoTema = document.querySelector(".icono-tema");
const body = document.body; // Atajo para acceder al <body>
btnTema.addEventListener("click", () => {
// toggle devuelve true si añadió la clase, false si la quitó
const esModoClaro = body.classList.toggle("light-mode");
body.classList.toggle("dark-mode");
// Cambiar el icono
iconoTema.textContent = esModoClaro ? "☀️" : "🌙";
});
Crear elementos dinámicamente
Muchas veces necesitas crear HTML desde JavaScript: mostrar resultados de una búsqueda, renderizar una lista de datos de una API, etc. La forma segura es con createElement:
// 1. Crear el elemento
const card = document.createElement("div");
// 2. Añadirle clases, contenido, atributos
card.classList.add("game-card", "nueva");
card.setAttribute("data-id", "3");
// 3. Crear hijos
const titulo = document.createElement("h2");
titulo.textContent = "Celeste";
const badge = document.createElement("span");
badge.classList.add("badge");
badge.textContent = "Plataformas";
// 4. Montar la estructura
card.append(titulo, badge); // append acepta múltiples elementos
// 5. Insertar en el DOM
const main = document.querySelector("main");
main.append(card); // Lo añade al final de <main>
append vs appendChild
// appendChild: solo acepta UN nodo, devuelve el nodo insertado
main.appendChild(card);
// append (moderno): acepta MÚLTIPLES nodos Y texto
main.append(card1, card2, "Texto suelto");
// prepend: lo mismo pero al principio
main.prepend(card);
// before/after: insertar como hermano, no como hijo
const referencia = document.querySelector(".game-card");
referencia.after(card); // Justo después
referencia.before(card); // Justo antes
// Eliminar un elemento
card.remove();
Ejemplo: construir una character card desde datos
const personajes = [
{
nombre: "Geralt de Rivia",
clase: "Brujo",
nivel: 42,
habilidades: ["Espada", "Señales", "Alquimia"],
imagen: "https://placehold.co/120x120/1a1a2e/e0e0e0?text=Geralt",
},
{
nombre: "Aloy",
clase: "Cazadora",
nivel: 35,
habilidades: ["Arco", "Trampas", "Anulación"],
imagen: "https://placehold.co/120x120/1a1a2e/e0e0e0?text=Aloy",
},
{
nombre: "Arthur Morgan",
clase: "Forajido",
nivel: 28,
habilidades: ["Tiro", "Caballo", "Dead Eye"],
imagen: "https://placehold.co/120x120/1a1a2e/e0e0e0?text=Arthur",
},
];
const crearCharacterCard = ({ nombre, clase, nivel, habilidades, imagen }) => {
// Crear el contenedor
const card = document.createElement("div");
card.classList.add("character-card");
// Imagen
const img = document.createElement("img");
img.src = imagen;
img.alt = nombre;
img.classList.add("character-card__avatar");
// Info
const info = document.createElement("div");
info.classList.add("character-card__info");
const h3 = document.createElement("h3");
h3.textContent = nombre;
const meta = document.createElement("p");
meta.classList.add("character-card__meta");
meta.textContent = `${clase} — Nivel ${nivel}`;
// Lista de habilidades
const ul = document.createElement("ul");
ul.classList.add("character-card__skills");
habilidades.forEach(hab => {
const li = document.createElement("li");
li.textContent = hab;
ul.append(li);
});
// Montar todo
info.append(h3, meta, ul);
card.append(img, info);
return card;
};
// Renderizar todas las cards
const contenedor = document.querySelector("#personajes");
personajes.forEach(personaje => {
const card = crearCharacterCard(personaje);
contenedor.append(card);
});
Atributos y dataset
const card = document.querySelector(".game-card");
// setAttribute: establecer cualquier atributo
card.setAttribute("role", "article");
card.setAttribute("aria-label", "Tarjeta de juego");
// getAttribute: leer un atributo
console.log(card.getAttribute("data-id")); // "1"
// dataset: acceso directo a atributos data-*
// <div data-id="1" data-categoria="aventura" data-en-oferta="true">
console.log(card.dataset.id); // "1"
console.log(card.dataset.categoria); // "aventura"
console.log(card.dataset.enOferta); // "true" (siempre es string)
// Modificar data-*
card.dataset.precio = "59.99";
// Esto añade: data-precio="59.99" al HTML
// hasAttribute: comprobar si existe
console.log(card.hasAttribute("data-id")); // true
// removeAttribute
card.removeAttribute("data-en-oferta");
Modificar estilos directamente
Aunque es preferible usar clases CSS, a veces necesitas cambiar un estilo específico desde JavaScript:
const elemento = document.querySelector(".game-card");
// Cambiar estilos individuales (camelCase, no kebab-case)
elemento.style.backgroundColor = "#1a1a2e";
elemento.style.borderRadius = "12px";
elemento.style.padding = "1.5rem";
// Leer un estilo computado (el que realmente se aplica)
const estilos = getComputedStyle(elemento);
console.log(estilos.backgroundColor); // "rgb(26, 26, 46)"
console.log(estilos.display); // "block"
// Cambiar variables CSS (custom properties) — muy útil
document.documentElement.style.setProperty("--color-primario", "#F7DF1E");
// Mejor práctica: en vez de cambiar estilos uno a uno, usa classList
// ❌ Esto
elemento.style.opacity = "0";
elemento.style.transform = "translateY(20px)";
elemento.style.transition = "all 0.3s";
// ✅ Esto — definir la clase en CSS y solo togglearla
elemento.classList.add("oculto");
insertAdjacentHTML: rendimiento al insertar HTML
Cuando necesitas insertar HTML como string (y confías en el origen de los datos), insertAdjacentHTML es más eficiente que innerHTML porque no recrea los elementos existentes:
const lista = document.querySelector("#lista-juegos");
// Las 4 posiciones disponibles:
// "beforebegin" — antes del elemento (como hermano anterior)
// "afterbegin" — dentro, al principio (primer hijo)
// "beforeend" — dentro, al final (último hijo) ← la más usada
// "afterend" — después del elemento (como hermano siguiente)
// Añadir un juego al final de la lista
lista.insertAdjacentHTML("beforeend", `
<div class="game-card" data-id="4">
<h2>Hollow Knight</h2>
<span class="badge">Metroidvania</span>
</div>
`);
// Añadir una notificación al principio
lista.insertAdjacentHTML("afterbegin", `
<div class="aviso">¡Nuevo juego añadido!</div>
`);
// Ejemplo práctico: renderizar una lista de juegos
const juegos = [
{ id: 1, nombre: "Zelda", genero: "Aventura" },
{ id: 2, nombre: "Hades", genero: "Roguelike" },
{ id: 3, nombre: "Celeste", genero: "Plataformas" },
];
const html = juegos.map(juego => `
<div class="game-card" data-id="${juego.id}">
<h2>${juego.nombre}</h2>
<span class="badge">${juego.genero}</span>
</div>
`).join("");
lista.insertAdjacentHTML("beforeend", html);
Recuerda:
insertAdjacentHTMLtiene el mismo riesgo de XSS queinnerHTML. Solo úsalo con datos que controles. Para datos de usuario, crea elementos concreateElementytextContent.
Recapitulación: cuándo usar cada técnica
| Quiero... | Usar |
|---|---|
| Seleccionar un elemento | querySelector(".clase") |
| Seleccionar varios elementos | querySelectorAll(".clase") |
| Cambiar texto de forma segura | elemento.textContent |
| Cambiar el aspecto visual | elemento.classList.add/remove/toggle |
| Crear elementos nuevos (seguro) | createElement + append |
| Insertar HTML de confianza rápido | insertAdjacentHTML |
| Leer/escribir atributos data-* | elemento.dataset.nombre |
| Cambiar un estilo puntual | elemento.style.propiedad |
| Eliminar un elemento | elemento.remove() |
Construye un panel de personajes interactivo
Crea un archivo HTML con un <div id="app"></div> vacío y un archivo JS que haga todo lo siguiente dinámicamente:
- Define un array de al menos 4 personajes de videojuegos (nombre, clase, nivel, habilidades, imagen placeholder).
- Crea una función
crearCharacterCard(personaje)que devuelva un elemento DOM completo con la estructura: imagen, nombre en un<h3>, clase y nivel en un<p>, y habilidades en un<ul>. - Renderiza todas las cards dentro de
#appusandoforEachyappend. - Añade un botón "Cambiar tema" que haga
togglede la claselight-modeen el<body>. - Cada card debe tener un atributo
data-nivelcon el nivel del personaje. - Bonus: Añade un botón a cada card que al hacer clic añada la clase
favorita(usaclassList.toggle). No te preocupes si aún no has visto eventos en detalle — basta conbtn.addEventListener("click", () => { ... }).
Todo el HTML de las cards debe ser creado con JavaScript, no escrito a mano en el HTML.
lightbulb Pistas
Empieza por la función crearCharacterCard. Usa destructuring en el parámetro. Para cada habilidad, crea un <li> dentro de un bucle forEach. Para el botón de favorito: const btn = document.createElement("button"); btn.textContent = "Favorito"; btn.addEventListener("click", () => card.classList.toggle("favorita")); y luego card.append(btn).