Tres horas. Media hora para entender el patrón de render y cómo se leen y se validan los datos de un formulario, y dos horas y media haciendo que tu catálogo salga de los datos y que tus errores se puedan oír.
Hoy · Hoja de ruta
- 1. Aprende: El patrón de render —una función que recibe datos y deja la página como esos datos digan—, cómo se leen los valores de un formulario y qué hace que un error sea perceptible sin ver la pantalla.
- 2. Haz: Pinta todo tu catálogo de la UD3, lee el formulario entero y valídalo con mensajes accesibles.
- 3. Comprueba: Borras el HTML escrito a mano y la página sigue igual; el foco va al primer campo que falla.
Antes de empezar · 5 minutos, sin apuntes
- ¿Cuántos productos tienes escritos a mano en el HTML? ¿Y si fueran cuatrocientos?
- Un campo numérico, ¿devuelve número?
- Un error escrito en rojo junto al campo, ¿lo percibe quien no ve la pantalla?
Se explica
25 minutos · conceptos y demostración
Hoy la página deja de ser algo que se escribe y pasa a ser algo que se calcula a partir de unos datos. Esa idea gobierna la sesión, y también la siguiente.
La función de render
import { crearTarjeta } from "./render.js";
export function pintarCatalogo(productos, contenedor) {
if (productos.length === 0) {
contenedor.replaceChildren(mensajeVacio());
return;
}
const fragmento = document.createDocumentFragment();
for (const producto of productos) {
fragmento.append(crearTarjeta(producto));
}
contenedor.replaceChildren(fragmento);
}
Tiene tres propiedades que conviene nombrar, porque son las que la hacen fiable:
- Recibe lo que pinta. No consulta variables globales ni pregunta a la página.
- Deja el contenedor completo. No añade sobre lo que hubiera: lo sustituye.
- Se puede llamar mil veces. El resultado depende solo de los datos que reciba.
Idempotente
Que ejecutarla dos veces con los mismos datos deje el mismo resultado. Sin esa propiedad, cada nuevo filtrado duplicaría el catálogo, que es el fallo con el que casi todo el mundo se estrena en esta sesión.
Una lista vacía sin mensaje se percibe como un error de la aplicación, y tampoco basta con «No hay resultados». Un buen estado vacío dice qué ha pasado y qué se puede hacer:
function mensajeVacio() {
const parrafo = document.createElement("p");
parrafo.className = "vacio";
parrafo.textContent = "No hay productos que coincidan. Prueba a quitar algún filtro.";
return parrafo;
}
A partir de aquí el catálogo vive en los datos, y en el HTML solo queda el contenedor:
<ul class="catalogo" data-js="catalogo"></ul>
Con una salvedad, que es la condición 2 de la unidad: si tu web debe seguir mostrando contenido sin JavaScript, el HTML conserva los productos y el código los sustituye al arrancar. Decide cuál de las dos opciones eliges, y escríbelo en tus notas.
Leer un formulario
const buscador = document.querySelector("[data-js='buscador']");
buscador.value // siempre texto
buscador.value.trim()
Number(precioMaximo.value) // convertir en el borde, como en la UD3
casilla.checked // true / false
seleccion.value // el valor de la opción elegida
Todo campo devuelve texto
Incluso los numéricos y los de fecha. Es exactamente el "10" + 5 de la sesión 1 de la UD3, que ahora deja de ser un ejercicio y pasa a ser tu bug.
Convierte al leer, y comprueba: un campo numérico vacío da cadena vacía, y Number("") es 0, no NaN. Ese cero silencioso ha estropeado muchos filtros.
Para leerlo entero de una vez:
formulario.addEventListener("submit", (evento) => {
evento.preventDefault();
const datos = Object.fromEntries(new FormData(evento.currentTarget));
console.log(datos);
// { nombre: "Ana", email: "ana@…", mensaje: "Hola" }
});
FormData recoge los campos que tengan atributo name —de ahí la insistencia de la UD1 en ponerlo— y Object.fromEntries los convierte en un objeto normal, listo para validarlo con las funciones que escribiste en la UD3. Las casillas no marcadas no aparecen, y los grupos de casillas con el mismo nombre requieren getAll.
Con el evento input ya tienes la búsqueda en vivo, usando buscar de la UD3 sin un solo cambio:
buscador.addEventListener("input", (evento) => {
const termino = evento.target.value.trim();
pintarCatalogo(buscar(catalogo, termino), contenedor);
});
Tres validaciones, y ninguna sobra
En la UD1 escribiste campos obligatorios, tipos de dato y patrones. Eso sigue funcionando y es la primera línea de defensa. JavaScript no viene a sustituirla:
campo.validity.valueMissing; // obligatorio y vacío
campo.validity.typeMismatch; // no parece un correo
campo.checkValidity(); // true / false
formulario.noValidate = true; // asumo yo la presentación de los errores
Las tres capas de validación
- Nativa: inmediata y gratis, funciona sin JavaScript.
- Con JavaScript: mensajes mejores, reglas que el HTML no expresa, avisos mientras se escribe.
- En el servidor (UD6): la única obligatoria, porque las dos anteriores se pueden saltar.
Un cliente que valida bien mejora la experiencia. Un servidor que no valida es un agujero.
Un error que se ve y se oye
<label for="email">Correo electrónico</label>
<input type="email" id="email" name="email" required
aria-describedby="error-email">
<p id="error-email" class="error" role="alert"></p>
function mostrarError(campo, mensaje) {
const destino = document.querySelector(`#error-${campo.id}`);
destino.textContent = mensaje;
campo.setAttribute("aria-invalid", "true");
}
Tres piezas que hacen el error perceptible para todo el mundo: aria-describedby ata el mensaje al campo, aria-invalid marca el campo como erróneo, y role="alert" hace que el lector de pantalla lo anuncie al aparecer. El color rojo, por sí solo, no informa a quien no distingue colores. Igual que en la UD2: el color acompaña, no comunica.
Avisar con cada tecla mientras alguien escribe su correo es molesto y aparece en rojo antes de que haya terminado. El criterio habitual:
- Al salir del campo: primera comprobación
- Al enviar: todos los campos
- Mientras se escribe: solo para quitar un error ya mostrado
Al enviar, por último, el foco va al primer campo que falla, que es lo que permite corregir sin buscar:
if (errores.length > 0) {
errores.forEach(({ campo, mensaje }) => mostrarError(campos[campo], mensaje));
campos[errores[0].campo].focus();
return;
}
Se trabaja
150 minutos · práctica sobre tu propio proyecto
Hoy se cierra el circuito que abriste en la UD3: aquellos datos y aquellas funciones se convierten en la página que se ve. Los dos últimos pasos comprueban lo que se rompe cuando se quitan las muletas.
Paso 1 · El catálogo pintado · 35 min
- Escribe
pintarCatalogo(productos, contenedor)enjs/render.js. - Píntalo al arrancar desde
main.js, importando los datos de la UD3. - Añade el estado vacío con un mensaje que diga qué hacer.
- Llama a la función dos veces seguidas y comprueba que no se duplica. Si se duplica, no era idempotente.
- Pinta un subconjunto usando una de tus funciones de filtrado de la UD3, sin modificarla.
- Muestra en un párrafo cuántos resultados se están viendo.
Antes de continuar: borra el catálogo escrito a mano del HTML. Si la página sigue igual, el render funciona.
Paso 2 · Leer y usar · 35 min
- Búsqueda en vivo con
inputsobre tu catálogo pintado. - Un filtro de precio máximo con conversión en el momento de leer.
- Lee el formulario de contacto entero con
FormDataal enviarlo. - Muestra en consola el objeto resultante y comprueba el tipo de cada valor con
typeof. - Combina búsqueda y precio máximo en una sola consulta.
- Añade un botón de limpiar que vacíe los filtros y devuelva el foco al buscador.
Consultar · otras cosas útiles de un formulario
formulario.reset(); // vuelve a los valores iniciales
buscador.focus(); // pone el foco
buscador.select(); // selecciona el contenido
campo.disabled = true; // deshabilita mientras se envía
Paso 3 · Formulario validado · 45 min
Es el trabajo central de la sesión.
- Añade a cada campo su párrafo de error con
role="alert"yaria-describedby. - Valida al salir de cada campo y al enviar.
- Muestra todos los errores a la vez y lleva el foco al primero.
- Quita el error en cuanto el campo se corrige.
- Reutiliza tu función de validación de la UD3: recibe un objeto y devuelve la lista de errores. Si has tenido que reescribirla, mira por qué.
- Comprueba que el mensaje de error no depende solo del color: quítale el color en DevTools y comprueba que se sigue entendiendo.
Paso 4 · Quita las muletas · 20 min
Dos pruebas que casi nadie hace, y que descubren cosas distintas.
Sin JavaScript. Desactívalo en DevTools y recarga.
| Pregunta | Qué ocurre | ¿Es aceptable? |
|---|---|---|
| ¿Se ve algún producto? | ||
| ¿El formulario sigue validando lo básico? | ||
| ¿El formulario se puede enviar? |
La respuesta depende de la decisión que anotaste sobre el catálogo. Lo que no es aceptable es que no la hayas tomado.
Solo con el teclado. Recorre el formulario entero de principio a fin, provoca los errores y corrígelos sin tocar el ratón. Anota dónde se queda el foco después de mostrar los errores y después de corregir el último.
Paso 5 · El campo vacío que vale cero · 15 min
Tu filtro de precio máximo tiene un fallo que no da error. Compruébalo:
- Deja el campo de precio máximo vacío y observa qué productos se muestran.
- Escribe qué está pasando:
""convertido a número no esNaN, es0. - Corrígelo tratando el campo vacío como «sin límite» en lugar de como cero.
- Prueba estos cinco valores en el campo y anota qué hace tu filtro con cada uno: vacío,
0,-5,abcy un número mayor que todos tus precios. - Dos de los cinco necesitan una decisión que no es evidente. Identifícalos y documenta qué has decidido.
Ampliación si has completado el trabajo
Primero termina y comprueba los cinco pasos. El primer reto mide; el segundo mejora una interfaz que ya funciona.
Reto 1 · Cuatrocientos productos
Genera un catálogo de cuatrocientos productos con un bucle y mide de verdad, en lugar de suponer.
- Usa
console.timeyconsole.timeEndalrededor del render. Anota el tiempo. - Reescribe
pintarCatalogopara que inserte las tarjetas una a una conappendsobre el contenedor, sin fragmento. Vuelve a medir. - Compara los dos tiempos con 20, 400 y 4.000 productos. Anota los seis números en una tabla. La diferencia no es proporcional, y ese es el hallazgo.
- Abre la pestaña Performance y graba un render de los lentos. Busca las barras de Layout y cuenta cuántas hay en cada versión.
- Explica en tres líneas por qué el fragmento cambia el número de recálculos, con el vocabulario de la UD2: qué obliga al navegador a rehacer el layout.
- Con 4.000 productos, ni siquiera el fragmento es suficiente. Escribe qué harías: no hace falta que lo implementes, pero sí que nombres la técnica.
Reto 2 · El resumen de errores
Un formulario largo con cinco errores obliga a recorrerlo entero buscando cuáles fallaron. Las interfaces bien hechas ponen un resumen arriba.
- Al enviar con errores, genera un bloque al principio del formulario con la lista de los que han fallado.
- Cada elemento de la lista es un enlace al campo correspondiente, usando su
id. Comprueba que al pulsarlo el foco llega al campo, y no solo la vista. - El resumen debe anunciarse: decide entre
role="alert"y llevarle el foco directamente. Prueba las dos y quédate con una, justificando la elección. - Escribe mensajes distintos para «este campo está vacío» y «el formato no es correcto» en el mismo campo. La diferencia importa: no se corrigen igual.
- Cuando todos los errores queden resueltos, el resumen tiene que desaparecer. Decide qué pasa entonces con el foco de quien estaba dentro de él.
- Prueba el resultado entero con el teclado y, si tienes acceso a un lector de pantalla, con él. Anota qué se oye al enviar un formulario con tres errores.
Cierre
5 minutos · comprobación y recuerdo
Lista de verificación de la sesión
- Tu render recibe los datos que pinta y los sustituye en lugar de añadirlos.
- El caso de lista vacía tiene un mensaje que dice qué hacer.
- Reutilizas sin cambios las funciones de la UD3.
- Conviertes cada valor del formulario al leerlo, y tratas el campo vacío.
- Los errores se ven, se oyen y llevan el foco donde toca.
- La validación nativa sigue funcionando sin JavaScript.
Antes de cerrar · 3 minutos, sin mirar
- ¿Qué significa que un render sea idempotente, y qué se ve si no lo es?
- ¿Qué debe decir un buen estado vacío?
- ¿De qué tipo es el valor de un campo numérico, y cuánto vale
Number("")? - ¿Qué campos recoge
FormData? - Nombra los tres atributos que hacen perceptible un error sin ver la pantalla.
- ¿Cuáles son las tres capas de validación, y cuál es obligatoria?
Ver respuestas
1 · Que llamarla varias veces con los mismos datos deja siempre el mismo resultado. Si no lo es, el catálogo se duplica en cada filtrado.
2 · Qué ha ocurrido y qué puede hacer la persona a continuación.
3 · Texto siempre; y Number("") vale cero, que es justo lo que hay que detectar antes de usarlo como límite.
4 · Los que tienen atributo name; las casillas sin marcar no aparecen.
5 · aria-describedby, aria-invalid y role="alert".
6 · Nativa, en el cliente con JavaScript y en el servidor; la del servidor es la obligatoria.
Microprueba semanal 3 · 5–10 minutos
Individual, sin IA y sin apuntes.
- ¿Qué significa que un render sea idempotente, y qué se ve si no lo es?
- Escribe cómo leerías un campo numérico de formulario para poder sumarlo.
- Nombra los tres atributos que hacen que un error de formulario se perciba sin ver la pantalla.