JavaScript en el navegador: DOM, eventos, formularios y comunicación asíncrona
Contenido
- Introducción
- Conocimiento previo
- Referencias
- Índice
- 1. Qué es una página interactiva
- 2. El DOM como árbol de nodos
- 3. Seleccionar elementos
- 4. Modificar contenido, atributos y clases
- 4.1. Texto con textContent
- 4.2. HTML interno con cautela
- 4.3. Atributos
- 4.4. Clases con classList
- 5. Crear, insertar y eliminar nodos
- 6. Eventos del navegador
- 7. Delegación de eventos
- 8. Formularios y lectura de datos
- 9. Validación de formularios
- 10. Expresiones regulares en validaciones sencillas
- 11. Asincronía: promesas, async y await
- 12. Comunicación HTTP con fetch
- 13. Renderizar datos de una API
- 14. Seguridad y límites: validación cliente, CORS y datos remotos
- 15. Proyecto guiado: catálogo interactivo
- 16. Errores frecuentes
- 17. Ejercicios
Unidad didáctica de Desarrollo web en entorno cliente para crear páginas interactivas con DOM, eventos, formularios, validación, promesas, fetch, JSON y consumo básico de APIs.
Introducción
En la unidad anterior aprendiste los fundamentos de JavaScript: variables, tipos, funciones, arrays, objetos, temporizadores, JSON y almacenamiento básico. En esta unidad aplicarás ese conocimiento para construir páginas web interactivas.
El navegador representa cada documento HTML como un árbol de objetos llamado DOM. JavaScript puede consultar ese árbol, modificarlo, crear nuevos elementos y responder a acciones del usuario mediante eventos. Además, los formularios permiten recoger datos y fetch permite comunicarse con APIs sin recargar toda la página.
Esta unidad une cuatro bloques que normalmente aparecen juntos en una aplicación web real:
- DOM para leer y modificar la página.
- Eventos para reaccionar a la interacción del usuario.
- Formularios y validación para recoger datos correctamente.
- Asincronía y
fetchpara obtener o enviar datos mediante HTTP.
El objetivo es que puedas crear pequeñas aplicaciones cliente que no se limiten a mostrar contenido estático, sino que respondan, validen, actualicen la interfaz y consuman datos externos de forma controlada.
Conocimiento previo
- Arquitectura web cliente y herramientas de trabajo
- HTML básico
- JavaScript
- CSS básico: clases, selectores y estilos sencillos.
Referencias
- MDN Web Docs - DOM
- MDN Web Docs - Document
- MDN Web Docs - EventTarget.addEventListener
- MDN Web Docs - Formularios HTML
- MDN Web Docs - Validación de formularios
- MDN Web Docs - FormData
- MDN Web Docs - Fetch API
- MDN Web Docs - Promise
- MDN Web Docs - async function
- MDN Web Docs - CORS
Índice
- Qué es una página interactiva
- El DOM como árbol de nodos
- Seleccionar elementos
- Modificar contenido, atributos y clases
- Crear, insertar y eliminar nodos
- Eventos del navegador
- Delegación de eventos
- Formularios y lectura de datos
- Validación de formularios
- Expresiones regulares en validaciones sencillas
- Asincronía: promesas, async y await
- Comunicación HTTP con fetch
- Renderizar datos de una API
- Seguridad y límites: validación cliente, CORS y datos remotos
- Proyecto guiado: catálogo interactivo
- Errores frecuentes
- Ejercicios
1. Qué es una página interactiva
Una página interactiva no se limita a mostrar contenido. También responde a lo que hace el usuario y cambia su estado sin necesidad de cargar otro documento completo.
Ejemplos habituales:
- Un botón que muestra u oculta una sección.
- Un formulario que avisa si falta un dato obligatorio.
- Una lista de tareas que permite añadir y eliminar elementos.
- Un buscador que filtra resultados mientras escribes.
- Una tabla que carga datos desde una API.
- Un mensaje de error que aparece cuando una petición falla.
Para crear ese tipo de comportamiento necesitas tres piezas:
| Pieza | Función |
|---|---|
| Estado | Datos actuales de la interfaz. Por ejemplo, una lista de tareas. |
| Eventos | Acciones que ocurren en la página. Por ejemplo, un clic o un envío de formulario. |
| Renderizado | Actualización visible de la página a partir del estado. |
En aplicaciones pequeñas puedes gestionar estas piezas con JavaScript y DOM directamente. En aplicaciones más grandes se suelen usar frameworks, pero la base conceptual sigue siendo la misma.
2. El DOM como árbol de nodos
El DOM, o Document Object Model, es la representación que el navegador crea a partir del HTML. Cada etiqueta, texto o atributo forma parte de una estructura de nodos organizada como un árbol.
HTML inicial:
<main> <h1>Lista de tareas</h1> <ul id="lista"> <li>Repasar JavaScript</li> </ul></main>Representación conceptual:
document└─ html └─ body └─ main ├─ h1 └─ ul#lista └─ liJavaScript no modifica directamente el archivo HTML guardado en disco. Modifica la representación que el navegador tiene en memoria. Por eso, si cambias el DOM desde DevTools o desde JavaScript, el archivo original no cambia automáticamente.
3. Seleccionar elementos
Para modificar una página primero necesitas seleccionar los elementos con los que vas a trabajar.
Los métodos más habituales son querySelector() y querySelectorAll().
<h1 id="titulo">Tareas</h1><button class="boton">Añadir</button><button class="boton">Vaciar</button>const titulo = document.querySelector('#titulo');const primerBoton = document.querySelector('.boton');const botones = document.querySelectorAll('.boton');
console.log(titulo);console.log(primerBoton);console.log(botones);querySelector() devuelve el primer elemento que coincide con el selector. Si no encuentra ninguno, devuelve null.
querySelectorAll() devuelve una colección de todos los elementos que coinciden. Puedes recorrerla con forEach() o convertirla en array si necesitas métodos de array más específicos.
const botones = document.querySelectorAll('.boton');
botones.forEach((boton) => { console.log(boton.textContent);});Si vas a trabajar con un elemento obligatorio de la interfaz, comprueba que existe antes de usarlo en ejemplos más complejos.
const formulario = document.querySelector('#formulario');
if (formulario === null) { console.error('No se ha encontrado el formulario');}En ejercicios pequeños puede parecer innecesario, pero en proyectos reales ayuda a localizar errores de selectores y cambios en el HTML.
4. Modificar contenido, atributos y clases
Una vez seleccionado un elemento, puedes modificar su contenido o sus propiedades.
4.1. Texto con textContent
textContent cambia el texto de un elemento de forma segura para contenido textual.
<p id="mensaje">Pendiente de cargar</p>const mensaje = document.querySelector('#mensaje');
mensaje.textContent = 'Datos cargados correctamente';Cuando solo necesites escribir texto, usa textContent.
4.2. HTML interno con cautela
innerHTML permite insertar HTML como texto interpretado por el navegador. Puede ser útil en casos controlados, pero también puede abrir la puerta a problemas de seguridad si se usa con datos externos o introducidos por usuarios.
const contenedor = document.querySelector('#contenedor');
contenedor.innerHTML = '<strong>Mensaje destacado</strong>';4.3. Atributos
Puedes leer y modificar atributos con métodos como setAttribute() y getAttribute(), o mediante propiedades cuando existan.
<img id="avatar" src="avatar.png" alt="Avatar por defecto">const avatar = document.querySelector('#avatar');
avatar.setAttribute('src', 'ana.png');avatar.setAttribute('alt', 'Avatar de Ana');
console.log(avatar.getAttribute('src'));4.4. Clases con classList
classList permite añadir, quitar o alternar clases CSS.
<p id="aviso" class="mensaje">Formulario pendiente</p>const aviso = document.querySelector('#aviso');
aviso.classList.add('mensaje--error');aviso.classList.remove('mensaje--ok');aviso.classList.toggle('visible');Cambiar clases suele ser mejor que cambiar estilos uno a uno desde JavaScript. CSS mantiene la presentación y JavaScript decide cuándo se aplica cada estado.
5. Crear, insertar y eliminar nodos
Modificar texto no siempre es suficiente. Muchas interfaces necesitan crear elementos nuevos: tareas, tarjetas, filas de tabla o mensajes.
Ejemplo de lista:
<ul id="lista"></ul>const lista = document.querySelector('#lista');
const tarea = document.createElement('li');tarea.textContent = 'Repasar DOM';
lista.append(tarea);Métodos útiles:
| Método | Uso |
|---|---|
document.createElement() | Crea un elemento. |
append() | Inserta al final. |
prepend() | Inserta al principio. |
remove() | Elimina un elemento. |
replaceChildren() | Sustituye todos los hijos de un elemento. |
Crear una tarjeta de producto:
const producto = { nombre: 'Teclado', precio: 29.99,};
const tarjeta = document.createElement('article');tarjeta.classList.add('producto');
const titulo = document.createElement('h2');titulo.textContent = producto.nombre;
const precio = document.createElement('p');precio.textContent = `${producto.precio} €`;
tarjeta.append(titulo, precio);Renderizar una lista completa suele hacerse limpiando el contenedor y añadiendo los elementos actuales.
const lista = document.querySelector('#lista');const tareas = ['Repasar DOM', 'Practicar eventos', 'Validar formulario'];
function renderizarTareas() { lista.replaceChildren();
tareas.forEach((texto) => { const item = document.createElement('li'); item.textContent = texto; lista.append(item); });}
renderizarTareas();Este patrón será muy útil en el proyecto guiado: mantener datos en un array y renderizar la interfaz a partir de esos datos.
6. Eventos del navegador
Un evento es algo que ocurre en la página: un clic, una tecla pulsada, la carga de un recurso, un cambio en un campo o el envío de un formulario.
La forma moderna de escuchar eventos es addEventListener().
<button id="boton" type="button">Saludar</button><p id="salida"></p>const boton = document.querySelector('#boton');const salida = document.querySelector('#salida');
boton.addEventListener('click', () => { salida.textContent = 'Hola desde JavaScript';});El segundo argumento de addEventListener() es una función callback. El navegador la ejecutará cuando ocurra el evento.
El objeto evento contiene información sobre lo ocurrido.
boton.addEventListener('click', (event) => { console.log(event.type); console.log(event.target);});Eventos frecuentes:
| Evento | Cuándo ocurre |
|---|---|
click | Al hacer clic. |
input | Al cambiar el valor de un campo mientras se escribe. |
change | Al confirmar un cambio en ciertos controles. |
submit | Al enviar un formulario. |
keydown | Al pulsar una tecla. |
DOMContentLoaded | Cuando el HTML inicial ya se ha procesado. |
Si cargas el script con defer, normalmente no necesitas esperar a DOMContentLoaded para seleccionar elementos del HTML, porque el script se ejecuta cuando el documento ya está preparado.
7. Delegación de eventos
Si una lista se crea dinámicamente, quizá sus elementos no existan cuando registras los eventos. En esos casos puede ser útil escuchar el evento en un contenedor y comprobar qué elemento lo produjo.
HTML:
<ul id="lista-tareas"></ul>JavaScript:
const listaTareas = document.querySelector('#lista-tareas');
listaTareas.addEventListener('click', (event) => { if (event.target.matches('button[data-accion="eliminar"]')) { const item = event.target.closest('li'); item.remove(); }});La delegación de eventos funciona porque muchos eventos se propagan desde el elemento donde ocurren hacia sus contenedores. Así puedes gestionar muchos botones con un solo listener en la lista.
Ejemplo de creación de elementos compatibles con la delegación:
function crearTarea(texto) { const item = document.createElement('li'); const span = document.createElement('span'); const boton = document.createElement('button');
span.textContent = texto; boton.type = 'button'; boton.dataset.accion = 'eliminar'; boton.textContent = 'Eliminar';
item.append(span, boton); return item;}La delegación no siempre es necesaria, pero ayuda cuando tienes listas dinámicas o muchos elementos similares.
8. Formularios y lectura de datos
Los formularios permiten recoger información del usuario. JavaScript puede intervenir para validar, mostrar mensajes o enviar datos de forma asíncrona.
Ejemplo de formulario:
<form id="formulario-producto"> <label for="nombre">Nombre</label> <input id="nombre" name="nombre" type="text" required>
<label for="precio">Precio</label> <input id="precio" name="precio" type="number" min="0" step="0.01" required>
<button type="submit">Guardar</button></form>El evento submit se dispara al enviar el formulario. Si quieres gestionar el envío con JavaScript, usa event.preventDefault().
const formulario = document.querySelector('#formulario-producto');
formulario.addEventListener('submit', (event) => { event.preventDefault();
const datos = new FormData(formulario); const nombre = datos.get('nombre'); const precio = Number(datos.get('precio'));
console.log(nombre, precio);});FormData lee los campos del formulario a partir de su atributo name. Si un campo no tiene name, no aparecerá en FormData.
También puedes leer campos directamente si necesitas trabajar con ellos mientras el usuario escribe.
const inputNombre = document.querySelector('#nombre');
inputNombre.addEventListener('input', () => { console.log(inputNombre.value);});9. Validación de formularios
HTML incluye validación nativa mediante atributos como required, type, min, max, minlength, maxlength y pattern.
<input id="email" name="email" type="email" required><input id="edad" name="edad" type="number" min="16" required>El navegador puede impedir el envío si los campos no cumplen esas restricciones. JavaScript puede complementar esa validación para mostrar mensajes personalizados o aplicar reglas adicionales.
const formulario = document.querySelector('#registro');const email = document.querySelector('#email');const error = document.querySelector('#error-email');
formulario.addEventListener('submit', (event) => { if (!email.validity.valid) { event.preventDefault(); error.textContent = 'Introduce un correo electrónico válido.'; }});Propiedades útiles:
| Propiedad o método | Uso |
|---|---|
element.validity.valid | Indica si el campo es válido. |
element.validity.valueMissing | Falta un valor obligatorio. |
element.validity.typeMismatch | El valor no coincide con el tipo esperado. |
element.checkValidity() | Comprueba si el elemento o formulario es válido. |
element.reportValidity() | Muestra mensajes nativos de validación. |
Ejemplo con mensaje mientras se escribe:
email.addEventListener('input', () => { if (email.validity.valid) { error.textContent = ''; } else { error.textContent = 'El correo todavía no es válido.'; }});10. Expresiones regulares en validaciones sencillas
Una expresión regular es un patrón para comprobar si un texto cumple una forma concreta. En formularios se usan para validaciones simples, como códigos postales, teléfonos o identificadores.
Ejemplo: código postal español de cinco cifras.
const patronCodigoPostal = /^\d{5}$/;
console.log(patronCodigoPostal.test('03001'));console.log(patronCodigoPostal.test('abc'));Ejemplo aplicado a un formulario:
const codigoPostal = document.querySelector('#codigo-postal');const errorCodigoPostal = document.querySelector('#error-codigo-postal');const patronCodigoPostal = /^\d{5}$/;
codigoPostal.addEventListener('input', () => { if (patronCodigoPostal.test(codigoPostal.value)) { errorCodigoPostal.textContent = ''; } else { errorCodigoPostal.textContent = 'El código postal debe tener 5 cifras.'; }});Las expresiones regulares pueden volverse difíciles de leer. Úsalas para patrones razonables y acompáñalas de mensajes claros para el usuario.
En muchos casos es preferible aprovechar primero los tipos y atributos nativos de HTML: type="email", type="number", min, max, required o pattern.
11. Asincronía: promesas, async y await
Muchas operaciones del navegador no se completan inmediatamente. Pedir datos a una API, esperar una respuesta de red o leer ciertos recursos puede tardar. JavaScript representa muchas de estas operaciones mediante promesas.
Una promesa puede estar pendiente, completarse correctamente o fallar.
async y await permiten escribir código asíncrono de forma más legible.
async function saludarDespues() { return 'Hola';}
saludarDespues().then((mensaje) => { console.log(mensaje);});En una función async, puedes usar await para esperar a que una promesa se resuelva.
function esperar(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); });}
async function ejecutar() { console.log('Inicio'); await esperar(1000); console.log('Ha pasado 1 segundo');}
ejecutar();Cuando trabajes con asincronía, piensa siempre en tres estados de interfaz:
- Cargando: la operación aún no ha terminado.
- Éxito: hay datos disponibles.
- Error: la operación ha fallado o la respuesta no es válida.
12. Comunicación HTTP con fetch
fetch() permite realizar peticiones HTTP desde JavaScript. Devuelve una promesa que se resuelve con un objeto Response.
Ejemplo básico:
async function cargarUsuarios() { const response = await fetch('https://jsonplaceholder.typicode.com/users'); const usuarios = await response.json();
console.log(usuarios);}
cargarUsuarios();En código real debes controlar errores. Una respuesta HTTP con estado 404 o 500 no siempre lanza una excepción automáticamente; por eso conviene comprobar response.ok.
async function cargarUsuarios() { try { const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) { throw new Error(`Error HTTP: ${response.status}`); }
const usuarios = await response.json(); console.log(usuarios); } catch (error) { console.error('No se han podido cargar los usuarios:', error); }}
cargarUsuarios();Métodos HTTP habituales:
| Método | Uso habitual |
|---|---|
GET | Obtener datos. |
POST | Crear un recurso o enviar datos. |
PUT | Reemplazar un recurso. |
PATCH | Modificar parte de un recurso. |
DELETE | Eliminar un recurso. |
Ejemplo de envío JSON:
async function crearProducto(producto) { const response = await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(producto), });
if (!response.ok) { throw new Error(`Error HTTP: ${response.status}`); }
return response.json();}En un ejercicio puedes usar APIs públicas de prueba, pero en una aplicación real debes conocer las reglas, autenticación y límites de la API que consumes.
13. Renderizar datos de una API
Consumir una API no termina al recibir JSON. Normalmente hay que transformar esos datos en elementos visibles.
HTML:
<p id="estado" role="status"></p><ul id="usuarios"></ul>JavaScript:
const estado = document.querySelector('#estado');const listaUsuarios = document.querySelector('#usuarios');
function renderizarUsuarios(usuarios) { listaUsuarios.replaceChildren();
usuarios.forEach((usuario) => { const item = document.createElement('li'); item.textContent = `${usuario.name} - ${usuario.email}`; listaUsuarios.append(item); });}
async function cargarUsuarios() { estado.textContent = 'Cargando usuarios...';
try { const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) { throw new Error(`Error HTTP: ${response.status}`); }
const usuarios = await response.json(); renderizarUsuarios(usuarios); estado.textContent = 'Usuarios cargados correctamente.'; } catch (error) { estado.textContent = 'No se han podido cargar los usuarios.'; console.error(error); }}
cargarUsuarios();Fíjate en que la función renderizarUsuarios() no hace la petición. Solo recibe datos y actualiza el DOM. Separar responsabilidades facilita probar y mantener el código.
El atributo role="status" ayuda a comunicar cambios de estado a tecnologías de apoyo sin interrumpir bruscamente la navegación del usuario.
14. Seguridad y límites: validación cliente, CORS y datos remotos
El código cliente no debe ser la única barrera de seguridad. Cualquier persona puede inspeccionar el JavaScript descargado, modificar peticiones desde DevTools o enviar datos directamente a una API.
Reglas básicas:
- Valida en cliente para ayudar al usuario.
- Valida en servidor para proteger la aplicación.
- No confíes en campos ocultos como mecanismo de seguridad.
- No incluyas secretos en JavaScript público.
- No uses
innerHTMLcon datos no confiables. - Muestra errores comprensibles sin exponer detalles internos del servidor.
CORS, Cross-Origin Resource Sharing, es un mecanismo del navegador que controla qué respuestas de otros orígenes puede leer una página. Si una API no permite peticiones desde tu origen, el navegador bloqueará el acceso a la respuesta aunque la petición llegue al servidor.
No se soluciona CORS copiando código del cliente sin entenderlo. Normalmente se configura en el servidor o API que recibe la petición.
También verás referencias antiguas a AJAX con XMLHttpRequest o jQuery. El concepto de comunicación asíncrona sigue siendo importante, pero en código moderno se prioriza fetch() junto con promesas, async y await.
15. Proyecto guiado: catálogo interactivo
En este proyecto crearás un pequeño catálogo de productos que combina DOM, eventos, formulario, validación y renderizado.
HTML base:
<main> <h1>Catálogo</h1>
<form id="formulario-producto"> <label for="nombre">Nombre</label> <input id="nombre" name="nombre" type="text" required minlength="2">
<label for="precio">Precio</label> <input id="precio" name="precio" type="number" min="0" step="0.01" required>
<button type="submit">Añadir producto</button> </form>
<p id="estado" role="status"></p> <ul id="catalogo"></ul></main>JavaScript inicial:
const formulario = document.querySelector('#formulario-producto');const catalogo = document.querySelector('#catalogo');const estado = document.querySelector('#estado');
const productos = [ { id: 1, nombre: 'Teclado', precio: 29.99 }, { id: 2, nombre: 'Ratón', precio: 14.99 },];Función de renderizado:
function renderizarCatalogo() { catalogo.replaceChildren();
productos.forEach((producto) => { const item = document.createElement('li'); const texto = document.createElement('span'); const boton = document.createElement('button');
texto.textContent = `${producto.nombre} - ${producto.precio.toFixed(2)} €`; boton.type = 'button'; boton.dataset.id = producto.id; boton.dataset.accion = 'eliminar'; boton.textContent = 'Eliminar';
item.append(texto, boton); catalogo.append(item); });}Alta desde formulario:
formulario.addEventListener('submit', (event) => { event.preventDefault();
if (!formulario.checkValidity()) { formulario.reportValidity(); return; }
const datos = new FormData(formulario); const nombre = String(datos.get('nombre')).trim(); const precio = Number(datos.get('precio'));
productos.push({ id: Date.now(), nombre, precio, });
formulario.reset(); estado.textContent = 'Producto añadido.'; renderizarCatalogo();});Eliminación con delegación de eventos:
catalogo.addEventListener('click', (event) => { if (!event.target.matches('button[data-accion="eliminar"]')) { return; }
const id = Number(event.target.dataset.id); const posicion = productos.findIndex((producto) => producto.id === id);
if (posicion !== -1) { productos.splice(posicion, 1); estado.textContent = 'Producto eliminado.'; renderizarCatalogo(); }});
renderizarCatalogo();Este proyecto todavía no guarda datos en servidor. Sirve para practicar el patrón básico: datos en memoria, eventos, validación y actualización del DOM. Como ampliación, podrías sustituir el array inicial por datos obtenidos con fetch().
16. Errores frecuentes
| Error | Causa habitual | Solución |
|---|---|---|
Cannot read properties of null | El selector no encuentra el elemento. | Revisar id, clase, carga del script y uso de defer. |
| El evento no se ejecuta | Listener registrado sobre un elemento inexistente o selector incorrecto. | Comprobar el elemento en consola. |
| El formulario recarga la página | No se ha usado event.preventDefault(). | Prevenir el envío si JavaScript gestiona el formulario. |
FormData no devuelve un campo | El control no tiene atributo name. | Añadir name al campo. |
Se muestran datos inseguros con innerHTML | Se inserta contenido no confiable como HTML. | Usar textContent o crear nodos. |
fetch parece funcionar pero no hay datos | No se ha usado await response.json() o la respuesta no es JSON. | Revisar respuesta en Network. |
| No se detecta un error HTTP | No se comprueba response.ok. | Lanzar error si response.ok es falso. |
| Problema de CORS | La API no permite leer la respuesta desde ese origen. | Configurar la API o usar una API que permita el origen. |
La consola y el panel Network son herramientas obligatorias en esta unidad. Úsalas antes de cambiar código al azar.
17. Ejercicios
Ejercicio 1: Selección y texto
Crea una página con un título, un párrafo y un botón. Al pulsar el botón, cambia el texto del párrafo usando textContent.
Ejercicio 2: Lista dinámica
Crea un array de tareas y renderízalo dentro de una lista ul. Usa createElement(), textContent, append() y replaceChildren().
Ejercicio 3: Alternar clases
Crea una caja con una clase CSS inicial. Añade un botón que use classList.toggle() para alternar una clase visible, destacada o activa.
Ejercicio 4: Delegación de eventos
Crea una lista de tareas donde cada tarea tenga un botón de eliminar. Usa un único listener en el contenedor de la lista para eliminar tareas.
Ejercicio 5: Formulario con FormData
Crea un formulario de producto con nombre, precio y categoría. Al enviarlo, evita la recarga de la página, lee los datos con FormData y muestra el objeto resultante por consola.
Ejercicio 6: Validación nativa y personalizada
Añade restricciones HTML a un formulario y complementa la validación con JavaScript para mostrar un mensaje propio cuando un campo sea inválido.
Ejercicio 7: Expresión regular sencilla
Valida un código postal español de cinco cifras usando una expresión regular. Muestra un mensaje diferente para valores válidos e inválidos.
Ejercicio 8: Cargar datos con fetch
Usa fetch() y async/await para cargar usuarios desde https://jsonplaceholder.typicode.com/users. Comprueba response.ok, transforma la respuesta con json() y muestra nombre y correo en una lista.
Ejercicio 9: Estados de carga y error
Amplía el ejercicio anterior mostrando un mensaje de carga antes de la petición, un mensaje de éxito al terminar y un mensaje de error si la petición falla.
Ejercicio 10: Proyecto integrador
Crea un pequeño catálogo interactivo con formulario de alta, lista renderizada desde un array, eliminación mediante delegación de eventos y una carga inicial opcional desde un archivo JSON o una API pública de prueba.