⚡ Curso JavaScript · Nivel N3 Intermedio

Bloque 6: DOM, eventos y almacenamiento

El momento en que JavaScript toca la pantalla: seleccionas elementos, los transformas, reaccionas a clics, teclas y formularios, y guardas datos entre visitas con localStorage. Al cierre construirás tu primera app completa: una lista de tareas que sobrevive al botón de recargar.

⏱ ≈ 14 horas 📚 4 lecciones ✍️ 18 ejercicios + reto: lista de tareas 📝 Examen final superable (≥ 70 %)
Lección 1 · Teoría

Seleccionar: querySelector y el árbol

El navegador mantiene tu HTML como un árbol de objetos llamado DOM. Para tocarlo necesitas antes seleccionar piezas. La herramienta moderna y única que usarás aquí: querySelector, que entiende CUALQUIER selector CSS que ya dominas.

un selector para gobernarlos a todos
const titulo   = document.querySelector("h1");        // por etiqueta
const menu     = document.querySelector("#menu");      // por id
const tarjeta  = document.querySelector(".card");      // por clase (la PRIMERA)
const enlace   = document.querySelector("nav a.destacado"); // combinado

1.1 · Todos a la vez: querySelectorAll

Devuelve una NodeList con TODAS las coincidencias — y como sabe comportarse como array, encadena directo con forEach:

recorrer la colección
const tarjetas = document.querySelectorAll(".card");

tarjetas.forEach((card) => {
  console.log(card.querySelector("h3").textContent);
});
Métodos clásicos que verás por ahí (y su equivalente moderno)
ClásicoModerno equivalente
getElementById("x")querySelector("#x")
getElementsByClassName("c")querySelectorAll(".c")
getElementsByTagName("div")querySelectorAll("div")

1.2 · Moverte por el árbol

Cada elemento conoce a su familia:

hacia abajo y arriba
lista.children;          // sus hijos directos
item.parentElement;     // su padre
card.closest(".card");  // el antepasado más cercano
entre hermanos
item.previousElementSibling;
item.nextElementSibling;

for (const hijo of lista.children) {
  console.log(hijo.textContent);
}
⚠️ El script debe ejecutarse cuando el HTML ya exista. Colócalo AL FINAL del body (como hacemos siempre) o envuélvelo en document.addEventListener("DOMContentLoaded", …).
Lección 2 · Teoría

Manipular: texto, clases y nodos

2.1 · Cambiar contenido

✅ textContent: solo texto
titulo.textContent = "Nuevo título";

// lee el texto puro, sin etiquetas
// SEGURRO contra inyección
innerHTML: interpreta HTML
lista.innerHTML = `<li>Hecho con map</li>`;

// ⚠️ nunca con datos del usuario sin limpiar

Los campos de formulario se leen y escriben con .value:

leer y limpiar un input
const texto = entrada.value.trim();
entrada.value = "";  // vaciar tras usarlo: detalle de profesional

2.2 · Estilos: mejor con clases

Puedes escribir estilos puntuales con .style, pero lo limpio es definir clases en CSS y activarlas/desactivarlas:

puntual (ocasional)
titulo.style.color = "crimson";
titulo.style.fontSize = "2rem"; // camelCase
classList (el estándar)
card.classList.add("destacada");
card.classList.remove("oculta");
card.classList.toggle("modo-oscuro"); // interruptor
💡 toggle es el interruptor perfecto para menús hamburguesa y modales: si la clase está, la quita; si no está, la pone.

2.3 · Crear y eliminar nodos

fábrica de elementos
const li = document.createElement("li");
li.textContent = "Aprender el DOM";
li.classList.add("pendiente");

lista.appendChild(li);   // aparece en pantalla
li.remove();              // y desaparece

2.4 · Pintar datos: el puente con el Bloque 5

de array a HTML en dos líneas
const productos = ["Teclado", "Monitor", "Ratón"];

lista.innerHTML = productos
  .map((p) => `<li>${p}</li>`)
  .join("");
Lección 3 · Teoría

Eventos: listeners y el objeto event

Un evento es una señal: clic, tecla, envío de formulario… Con addEventListener te suscribes a ella. La página queda a la escucha y tu función salta cuando ocurre:

la tríada: elemento + evento + callback
const boton = document.querySelector("#saludar");
boton.addEventListener("click", () => {
  console.log("¡Clic recibido!");
});
⚠️ Olvida el viejo onclick = …: con addEventListener puedes colgar VARIOS listeners al mismo elemento y es el estándar profesional.
Los seis eventos del 90 % de los casos
EventoSalta cuando…
clickpulsan el elemento (botones, tarjetas, items)
inputel valor de un campo CAMBIA en vivo, letra a letra
changeel campo termina de cambiar (selects, checkboxes)
submitse envía un formulario (en el FORM, no en el botón)
keydownse pulsa una tecla (Enter para enviar…)
DOMContentLoadedel HTML está listo para manipularse

3.1 · El objeto event

Tu callback puede recibir un parámetro — el objeto event — con toda la información del suceso:

e.target: ¿quién disparó?
lista.addEventListener("click", (e) => {
  e.target.classList.toggle("hecha");
});
// cada <li> marcable sin un listener por li
e.key: ¿qué tecla?
entrada.addEventListener("keydown", (e) => {
  if (e.key === "Enter") {
    agregarTarea();
  }
});

3.2 · Formularios validados: preventDefault

Al enviar un form el navegador recarga la página. Con e.preventDefault() tomas el control: validas TÚ y decides qué pasa:

el patrón completo
form.addEventListener("submit", (e) => {
  e.preventDefault();               // frena la recarga

  const texto = input.value.trim();
  if (texto.length < 3) {
    error.textContent = "Mínimo 3 caracteres";
    return;                          // corta aquí si no valida
  }

  error.textContent = "";
  agregarTarea(texto);
  input.value = "";
});
Lección 4 · Teoría

Web Storage: localStorage y sessionStorage

Hasta ahora cada recarga borraba todo. Web Storage da a tu app una mini-base-de-datos en el navegador, superviviente a los F5:

Dos almacenes, dos vidas útiles
localStoragesessionStorage
Vidapara siempre (hasta borrarlo)solo la pestaña actual
Úsalo para…preferencias, listas guardadasestado temporal de un paso
la API completa son 4 métodos
localStorage.setItem("usuario", "Ada");  // guardar
localStorage.getItem("usuario");           // "Ada" o null
localStorage.removeItem("usuario");       // borrar una
localStorage.clear();                     // vaciar todo
⚠️ REGLA DE ORO: storage solo guarda STRINGS. Un número se convierte en texto; un array u objeto… también, pero mal. Para estructuras: JSON.

4.1 · Guardar estructuras: el dúo JSON

Aquí reaparece el Bloque 4 — JSON.stringify al guardar, JSON.parse al leer:

guardar un array
const tareas = ["Comprar pan", "Estudiar DOM"];

localStorage.setItem("tareas",
  JSON.stringify(tareas));
cargarlo intacto
const guardadas = JSON.parse(
  localStorage.getItem("tareas")
) || [];

// || [] protege la primera visita (null)

4.2 · El patrón completo de persistencia

  1. Cargar: al arrancar, lee el storage y pinta lo que haya.
  2. Actualizar: cada cambio del usuario modifica el array en memoria…
  3. Sincronizar: …y de inmediato reescribe el storage.
🎉 Con selección + eventos + storage ya tienes las tres patas de toda app front-end. El reto final del bloque une las tres. Después llega el Bloque 7: asincronía y APIs, donde tus datos vendrán de internet en tiempo real.
Manos a la obra

✍️ Ejercicios guiados

Crea una carpeta bloque6. Cada ejercicio es un archivo HTML propio (con su <script> al final del body). El reto final une todo.

Sección A · Seleccionar

1. hola-dom.html — Un <h1> con texto. Selecciónalo por id con querySelector y muestra su textContent en consola. Luego cámbialo por «DOM conquistado».

Ver solución
<h1 id="titulo">Hola DOM</h1>

<script>
const titulo = document.querySelector("#titulo");
console.log(titulo.textContent);   // "Hola DOM"
titulo.textContent = "DOM conquistado";
</script>

2. contar-tarjetas.html — Tres div.card. Cuenta cuántas hay con querySelectorAll y recórrelas con forEach mostrando el texto de cada una.

Ver solución
const tarjetas = document.querySelectorAll(".card");
console.log("Total:", tarjetas.length);

tarjetas.forEach((card) => {
  console.log(card.querySelector("h3").textContent);
});

3. selectores-css.html — Practica selectores: selecciona el primer p.intro, el enlace dentro de footer ("footer a") y todos los li.destacado.

Ver solución
const intro = document.querySelector("p.intro");
const enlaceFooter = document.querySelector("footer a");
const destacados = document.querySelectorAll("li.destacado");

console.log(intro, enlaceFooter, destacados.length);

4. familia-arbol.html — Una lista de 3 elementos. Desde el segundo li: muestra su parentElement.tagName, cuántos hijos tiene la lista (children.length) y su hermano anterior con previousElementSibling.

Ver solución
const items = document.querySelectorAll("li");
const segundo = items[1];

console.log(segundo.parentElement.tagName);      // UL
console.log(segundo.parentElement.children.length); // 3
console.log(segundo.previousElementSibling.textContent);

5. atributos.html — Una imagen sin src. Asígnale uno con img.src = … y un alt con setAttribute(). Compruébalo leyendo ambos.

Ver solución
const logo = document.querySelector("#logo");
logo.src = "img/logo.png";
logo.setAttribute("alt", "Logo del sitio");

console.log(logo.getAttribute("src"), logo.getAttribute("alt"));

Sección B · Manipular

6. estilos-inline.html — Un párrafo que cambie a color rojo y tamaño 1.5rem mediante .style al pulsar un botón (adelanto del Bloque de eventos: usa addEventListener).

Ver solución
const btn = document.querySelector("#rojo");
const parrafo = document.querySelector("p");

btn.addEventListener("click", () => {
  parrafo.style.color = "crimson";
  parrafo.style.fontSize = "1.5rem";
});

7. modo-oscuro.html — Define en CSS una clase .modo-oscuro { background:#111; color:#eee }. Con un botón, alterna la clase sobre el body usando classList.toggle().

Ver solución
const btnTema = document.querySelector("#tema");

btnTema.addEventListener("click", () => {
  document.body.classList.toggle("modo-oscuro");
});

8. crear-nodos.html — Partiendo de una lista vacía, crea con createElement tres li con tus tres series favoritas, añádeles la clase serie y píntalos en la lista.

Ver solución
const lista = document.querySelector("#series");
const titulos = ["Dark", "Breaking Bad", "Mr. Robot"];

titulos.forEach((t) => {
  const li = document.createElement("li");
  li.textContent = t;
  li.classList.add("serie");
  lista.appendChild(li);
});

9. pintar-array.html — Dado const precios = [10, 25, 8]; pinta en una lista cada precio formateado como «10,00 €» usando map + toFixed(2) + join sobre innerHTML (el puente con el Bloque 5).

Ver solución
const precios = [10, 25, 8];
const ul = document.querySelector("#precios");

ul.innerHTML = precios
  .map((p) => `<li>${p.toFixed(2)} €</li>`)
  .join("");

Sección C · Eventos

10. contador-clics.html — Un botón y un contador. Cada clic incrementa el número mostrado en un <span id="marcador">.

Ver solución
let clics = 0;
const btn = document.querySelector("#btn");
const marcador = document.querySelector("#marcador");

btn.addEventListener("click", () => {
  clics++;
  marcador.textContent = clics;
});

11. quien-dispara.html — Tres tarjetas. Pon UN solo listener en su contenedor y usa e.target para saber cuál fue pulsada; márcala con la clase activa.

Ver solución
const contenedor = document.querySelector(".panel");

contenedor.addEventListener("click", (e) => {
  console.log("Pulsaste:", e.target.textContent);
  e.target.classList.toggle("activa");
});

12. escribiendo.html — Un input de bio de 120 caracteres. Con el evento input, muestra EN VIVO «X/120 caracteres» y ponlo en rojo al llegar al límite.

Ver solución
const bio = document.querySelector("#bio");
const resto = document.querySelector("#resto");

bio.addEventListener("input", (e) => {
  resto.textContent = `${e.target.value.length}/120 caracteres`;
  resto.style.color =
    e.target.value.length >= 120 ? "crimson" : "";
});

13. tecla-enter.html — Un input donde, al pulsar Enter (keydown + e.key === "Enter"), se añada su valor como nuevo li a una lista y se vacíe el campo.

Ver solución
const campo = document.querySelector("#campo");
const lista = document.querySelector("#salida");

campo.addEventListener("keydown", (e) => {
  if (e.key !== "Enter") return;
  const texto = campo.value.trim();
  if (!texto) return;

  const li = document.createElement("li");
  li.textContent = texto;
  lista.appendChild(li);
  campo.value = "";
});

14. form-validado.html — Formulario con email. En submit: preventDefault(), valida que contenga «@» y que mida ≥ 5; muestra el error en un párrafo rojo o «✓ Email válido» en verde.

Ver solución
const form = document.querySelector("#form");
const email = document.querySelector("#email");
const aviso = document.querySelector("#aviso");

form.addEventListener("submit", (e) => {
  e.preventDefault();
  const valor = email.value.trim();

  if (valor.length < 5 || !valor.includes("@")) {
    aviso.textContent = "Email no válido";
    aviso.style.color = "crimson";
    return;
  }

  aviso.textContent = "✓ Email válido";
  aviso.style.color = "seagreen";
  form.reset();
});

Sección D · Almacenamiento + integración

15. mi-nombre.html — Pide el nombre por prompt, guárdalo en localStorage y muéstralo en un h1. Al recargar, si existe guardado, recupéralo con getItem en lugar de volver a preguntar.

Ver solución
const titulo = document.querySelector("h1");
let nombre = localStorage.getItem("usuario");

if (!nombre) {
  nombre = prompt("¿Cómo te llamas?");
  localStorage.setItem("usuario", nombre);
}

titulo.textContent = `Hola, ${nombre}`;

16. visitas-sesion.html — Con sessionStorage, cuenta cuántas veces se ha recargado esta pestaña: lee el contador, súmale 1 y guárdalo de nuevo.

Ver solución
let visitas = Number(sessionStorage.getItem("visitas")) || 0;
visitas++;

sessionStorage.setItem("visitas", visitas);
document.querySelector("output").textContent =
  `Visitas en esta pestaña: ${visitas}`;

17. tema-persistente.html — Une todo: el toggle de modo oscuro del ejercicio 7, pero guardando la preferencia en localStorage y aplicándola automáticamente al cargar la página.

Ver solución
const btnTema = document.querySelector("#tema");

if (localStorage.getItem("tema") === "oscuro") {
  document.body.classList.add("modo-oscuro");
}

btnTema.addEventListener("click", () => {
  const oscuro = document.body.classList.toggle("modo-oscuro");
  localStorage.setItem("tema", oscuro ? "oscuro" : "claro");
});

18. 🏆 RETO INTEGRADOR — lista-de-tareas.html Tu primera app completa. Requisitos:

  • Campo de texto + botón «Añadir» + lista vacía + zona de aviso.
  • Añadir tarea con el botón y con la tecla Enter.
  • Rechazar textos vacíos con un mensaje de error visible.
  • Clic sobre una tarea para marcarla como hecha (clase CSS).
  • Doble clic para eliminarla.
  • Persistencia total: guardar y cargar las tareas con localStorage + JSON. Al recargar, tus tareas siguen ahí.
Ver una solución posible
<input id="entrada"><button id="agregar">Añadir</button>
<p id="aviso"></p>
<ul id="lista"></ul>

<script>
const entrada = document.querySelector("#entrada");
const boton   = document.querySelector("#agregar");
const lista   = document.querySelector("#lista");
const aviso   = document.querySelector("#aviso");

function guardar(tareas) {
  localStorage.setItem("tareas", JSON.stringify(tareas));
}

function anadir() {
  const texto = entrada.value.trim();
  if (!texto) {
    aviso.textContent = "Escribe algo primero";
    return;
  }
  aviso.textContent = "";

  const li = document.createElement("li");
  li.textContent = texto;
  lista.appendChild(li);

  guardar([...lista.children].map((item) => item.textContent));
  entrada.value = "";
}

boton.addEventListener("click", anadir);

entrada.addEventListener("keydown", (e) => {
  if (e.key === "Enter") anadir();
});

lista.addEventListener("click", (e) => {
  if (e.target.tagName === "LI") e.target.classList.toggle("hecha");
});

lista.addEventListener("dblclick", (e) => {
  if (e.target.tagName === "LI") {
    e.target.remove();
    guardar([...lista.children].map((item) => item.textContent));
  }
});

// Cargar lo guardado al arrancar
(JSON.parse(localStorage.getItem("tareas")) || []).forEach((t) => {
  const li = document.createElement("li");
  li.textContent = t;
  lista.appendChild(li);
});
</script>
Evaluación

📝 Examen del Bloque 6

14 preguntas, una sola respuesta correcta por pregunta. Necesitas 70 % (10/14) para superarlo.

1 · ¿Qué devuelve document.querySelector(".card")?
2 · ¿Qué devuelve document.querySelectorAll("li")?
3 · Diferencia clave: textContent frente a innerHTML…
4 · element.classList.toggle("activo") hace…
5 · Para crear un elemento nuevo en el DOM…
6 · boton.addEventListener("click", fn) ejecuta fn…
7 · Dentro de un listener, e.target es…
8 · En un submit, e.preventDefault() sirve para…
9 · Para reaccionar MIENTRAS el usuario escribe en un input…
10 · localStorage frente a sessionStorage…
11 · ¿Qué tipo de datos guarda Web Storage?
12 · Recuperar un array guardado como JSON…
13 · Pulsar <button type="submit"> dentro de un form dispara…
14 · li.remove() sirve para…

🧪 Parte B · Laboratorio: lista-de-tareas

Abre el laboratorio y construye la mini app completa: selecciona con querySelector, reacciona con addEventListener, crea nodos, valida y persiste en localStorage. Marca cada avance: