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.
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:
const tarjetas = document.querySelectorAll(".card");
tarjetas.forEach((card) => {
console.log(card.querySelector("h3").textContent);
});
| Clásico | Moderno 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:
lista.children; // sus hijos directos
item.parentElement; // su padre
card.closest(".card"); // el antepasado más cercano
item.previousElementSibling;
item.nextElementSibling;
for (const hijo of lista.children) {
console.log(hijo.textContent);
}
document.addEventListener("DOMContentLoaded", …).Manipular: texto, clases y nodos
2.1 · Cambiar contenido
titulo.textContent = "Nuevo título";
// lee el texto puro, sin etiquetas
// SEGURRO contra inyección
lista.innerHTML = `<li>Hecho con map</li>`;
// ⚠️ nunca con datos del usuario sin limpiar
Los campos de formulario se leen y escriben con .value:
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:
titulo.style.color = "crimson";
titulo.style.fontSize = "2rem"; // camelCase
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
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
const productos = ["Teclado", "Monitor", "Ratón"];
lista.innerHTML = productos
.map((p) => `<li>${p}</li>`)
.join("");
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:
const boton = document.querySelector("#saludar");
boton.addEventListener("click", () => {
console.log("¡Clic recibido!");
});
onclick = …: con addEventListener puedes
colgar VARIOS listeners al mismo elemento y es el estándar profesional.| Evento | Salta cuando… |
|---|---|
click | pulsan el elemento (botones, tarjetas, items) |
input | el valor de un campo CAMBIA en vivo, letra a letra |
change | el campo termina de cambiar (selects, checkboxes) |
submit | se envía un formulario (en el FORM, no en el botón) |
keydown | se pulsa una tecla (Enter para enviar…) |
DOMContentLoaded | el 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:
lista.addEventListener("click", (e) => {
e.target.classList.toggle("hecha");
});
// cada <li> marcable sin un listener por li
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:
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 = "";
});
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:
| localStorage | sessionStorage | |
|---|---|---|
| Vida | para siempre (hasta borrarlo) | solo la pestaña actual |
| Úsalo para… | preferencias, listas guardadas | estado temporal de un paso |
localStorage.setItem("usuario", "Ada"); // guardar
localStorage.getItem("usuario"); // "Ada" o null
localStorage.removeItem("usuario"); // borrar una
localStorage.clear(); // vaciar todo
4.1 · Guardar estructuras: el dúo JSON
Aquí reaparece el Bloque 4 — JSON.stringify al guardar,
JSON.parse al leer:
const tareas = ["Comprar pan", "Estudiar DOM"];
localStorage.setItem("tareas",
JSON.stringify(tareas));
const guardadas = JSON.parse(
localStorage.getItem("tareas")
) || [];
// || [] protege la primera visita (null)
4.2 · El patrón completo de persistencia
- Cargar: al arrancar, lee el storage y pinta lo que haya.
- Actualizar: cada cambio del usuario modifica el array en memoria…
- Sincronizar: …y de inmediato reescribe el storage.
✍️ 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>📝 Examen del Bloque 6
14 preguntas, una sola respuesta correcta por pregunta. Necesitas 70 % (10/14) para superarlo.
🧪 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: