1. El DOM: la página como un árbol de objetos
Cuando el navegador lee el HTML no lo guarda como texto, sino que construye un árbol en memoria: el DOM (Document Object Model). Cada etiqueta se convierte en un nodo de tipo elemento, el texto que contiene en un nodo de texto, y todos cuelgan de un único nodo raíz, document. Lo que ves en pantalla es un reflejo de ese árbol: si JavaScript cambia un nodo, la página cambia al instante, sin recargar.
El DOM es un árbol genealógico
Encontrar elementos
Los dos métodos que usarás casi siempre son querySelector, que devuelve el primer elemento que encaja con un selector CSS (o null si no hay ninguno), y querySelectorAll, que devuelve todos en una lista estática que puedes recorrer con for…of o forEach. Una vez que tienes un elemento, puedes moverte a su alrededor con parentElement, children, nextElementSibling o closest(selector), que sube por los antepasados hasta encontrar uno que encaje: la usarás muchísimo con los eventos.
2. Cambiar contenido, atributos y estilos
Para cambiar el texto de un elemento usa textContent. Existe también innerHTML, que interpreta el texto como HTML, y aquí está una de las lecciones de seguridad más importantes del módulo: si metes con innerHTML algo que ha escrito un usuario, ese usuario puede colar un <img onerror="…"> y ejecutar su propio código en la página de otros. Es el ataque XSS (Cross-Site Scripting). La regla es sencilla: los datos del usuario, siempre con textContent o creando nodos.
1const tarjeta = document.querySelector(".tarjeta");
2
3tarjeta.querySelector("h3").textContent = nombreUsuario; // seguro
4tarjeta.classList.add("destacada"); // add, remove, toggle, contains
5tarjeta.dataset.id = "42"; // → atributo data-id="42"
6tarjeta.setAttribute("aria-expanded", "true");
7tarjeta.style.setProperty("--color", "#2563eb"); // mejor clases o variables CSS que estilos sueltos
8
9const li = document.createElement("li");
10li.textContent = "Nuevo elemento";
11document.querySelector("ul").append(li); // también prepend, before, after
12li.remove();| Quiero… | Usa | Evita |
|---|---|---|
| Cambiar un texto | textContent | innerHTML con datos del usuario |
| Mostrar u ocultar | hidden o una clase CSS | Cambiar style.display a mano en muchos sitios |
| Guardar un dato en el elemento | dataset (data-*) | Atributos inventados sin data- |
| Insertar muchos elementos | Crearlos y hacer un único append (o un DocumentFragment) | Un append por elemento dentro de un bucle muy largo |
| Repetir un bloque de HTML | <template> y cloneNode(true) | Construir HTML concatenando textos |
3. Eventos: reaccionar a lo que hace el usuario
Una aplicación web se pasa la mayor parte del tiempo esperando: a que el usuario haga clic, escriba, envíe un formulario o mueva el ratón. Cada una de esas acciones genera un evento, y tu código se suscribe a los que le interesan con addEventListener(tipo, manejador). Cuando el evento ocurre, el navegador llama a tu función y le pasa un objeto con toda la información: qué elemento lo recibió, qué tecla se pulsó, dónde estaba el ratón…
| Categoría | Eventos | Propiedades útiles |
|---|---|---|
| Ratón y puntero | click, dblclick, pointerdown, pointermove, contextmenu | clientX, clientY, button |
| Teclado | keydown, keyup | key ("Enter", "a", "ArrowUp"), ctrlKey, shiftKey |
| Formularios | input (cada cambio), change (al confirmar), submit, focus, blur | target.value, target.checked |
| Documento y ventana | DOMContentLoaded, load, resize, scroll | window.innerWidth, scrollY |
Burbujeo y delegación
Un evento no se queda en el elemento donde ocurrió: primero baja desde document hasta él (fase de captura) y después sube por todos sus antepasados (fase de burbuja). Por eso un clic en un <li> también lo puede escuchar el <ul> que lo contiene. Dentro del manejador, e.target es el elemento donde se hizo clic y e.currentTarget el que tiene el manejador.
Esto permite una técnica fundamental, la delegación de eventos: en lugar de poner un manejador en cada elemento de una lista, pones uno solo en el contenedor y averiguas con e.target.closest() sobre qué elemento se actuó. Tiene dos ventajas enormes: usa menos memoria y funciona automáticamente con los elementos que añadas después.
1document.querySelector("#productos").addEventListener("click", (e) => {
2 const boton = e.target.closest("button[data-accion]");
3 if (!boton) return; // se hizo clic en otra parte
4 const tarjeta = boton.closest(".producto");
5 if (boton.dataset.accion === "borrar") tarjeta.remove();
6});Dos métodos del objeto evento completan el cuadro. e.preventDefault() cancela la acción por defecto del navegador (seguir un enlace, enviar un formulario y recargar la página) y e.stopPropagation() impide que el evento siga subiendo. El primero lo usarás a menudo; el segundo, muy poco, porque romper la propagación suele causar más problemas de los que resuelve.
(sin mensajes)4. Formularios y validación
Los formularios son la principal forma de recoger datos del usuario, y validarlos bien es la diferencia entre una aplicación agradable y una frustrante. HTML ya valida mucho por sí solo con atributos como required, type="email", min, max, minlength o pattern. JavaScript entra en juego para las reglas que HTML no puede expresar (que dos contraseñas coincidan, que una fecha sea posterior a otra) y para mostrar los errores con tu propio diseño.
- Escucha el evento
submitdel formulario, no el clic del botón: así también funciona al pulsar Enter. - Llama a
e.preventDefault()si vas a gestionar el envío tú mismo (por ejemplo, con fetch). - Lee los valores con
campo.value(siempre texto) o todos a la vez connew FormData(form). - Valida y muestra cada error junto a su campo, no en un único mensaje general.
- Marca los campos erróneos con
aria-invalid="true"y enlaza el mensaje conaria-describedbypara que los lectores de pantalla lo anuncien.
La API de validación del navegador te da acceso a lo que ya comprueba HTML: campo.checkValidity() dice si es válido, campo.validity dice por qué no (valueMissing, typeMismatch, tooShort…) y campo.setCustomValidity("mensaje") te deja añadir tus propias reglas integradas con las del navegador.
Recuerda la regla de oro
5. Retos
Estos retos son pequeñas piezas de aplicaciones reales. Las pruebas interactúan con la página como lo haría un usuario: escriben en los campos, pulsan botones, envían formularios y usan el teclado.
Al enviar el formulario #nueva se añade a #lista un <li> con un <span> con el texto de #texto y un botón con la clase «borrar» (el campo se vacía y los textos en blanco se ignoran). Al hacer clic en el <span> de una tarea, el <li> alterna la clase «hecha»; el botón «borrar» elimina su tarea. #pendientes muestra siempre cuántas tareas no están hechas. Usa un único manejador de clics en la lista (delegación de eventos).
Mientras se escribe en #filtro, oculta (con la propiedad hidden) las filas del <tbody> cuyo texto no contenga lo escrito, sin distinguir mayúsculas. #resultados debe mostrar «N resultados» (o «1 resultado»). Con el filtro vacío se ven todas.
Haz funcionar las pestañas: al pulsar un botón con role="tab", queda con aria-selected="true" (las demás con "false") y se muestra su panel (el indicado en aria-controls), ocultando los otros con hidden. Además, con las flechas derecha e izquierda sobre una pestaña se activa la siguiente o la anterior (de forma circular) y recibe el foco.
Al enviar #registro, valida los campos y, por cada error, escribe el mensaje en su <span class="error"> (#e-email, #e-pass, #e-pass2, #e-edad) y marca el campo con aria-invalid="true"; los campos correctos quedan con el mensaje vacío y aria-invalid="false". Reglas: email con el formato texto@texto.texto → «Email no válido»; contraseña de al menos 8 caracteres → «Mínimo 8 caracteres»; repetir contraseña igual → «Las contraseñas no coinciden»; edad entera entre 14 y 120 → «Edad no válida». Si todo es correcto, #ok muestra «Registro completado». La página no debe recargarse nunca.