Saltar al contenido
schedule 12 min JavaScript

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.

index.html
<!-- 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:

index.html
<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>
app.js
// 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"

querySelectorAll devuelve una NodeList, no un array. Puedes usar forEach directamente, pero si necesitas map, filter, etc., conviértela con [...nodeList] o Array.from(nodeList).

Modificar texto: textContent vs innerHTML

app.js
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 textContent para texto que venga de usuarios o fuentes externas. Solo usa innerHTML con 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:

app.js
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)

index.html
<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>
styles.css
/* 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;
}
app.js
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:

app.js
// 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

app.js
// 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

character-card.js
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

app.js
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:

app.js
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:

app.js
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: insertAdjacentHTML tiene el mismo riesgo de XSS que innerHTML. Solo úsalo con datos que controles. Para datos de usuario, crea elementos con createElement y textContent.

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()
code

Construye un panel de personajes interactivo

Medio schedule 25 min

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 #app usando forEach y append.
  • Añade un botón "Cambiar tema" que haga toggle de la clase light-mode en el <body>.
  • Cada card debe tener un atributo data-nivel con el nivel del personaje.
  • Bonus: Añade un botón a cada card que al hacer clic añada la clase favorita (usa classList.toggle). No te preocupes si aún no has visto eventos en detalle — basta con btn.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).

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.