Ir al contenido
wannacode

Una lista a partir de datos

En resumen: Renderizar desde los datos es una función render() que construye la lista de la página a partir de un array, para que cada sitio muestre lo mismo.

Problema

El carrito miente

Una compradora añade pan y leche. Las líneas del carrito son correctas, el total es correcto, y arriba dice «Carrito (0)». Pulsa los dos botones «Añadir» y mira el contador.

HTML, CSS, JS

La consola está vacía. Cada sitio de la página lo actualiza una línea de código distinta en add, y para el contador no hay esa línea. El navegador no sabe que los tres sitios muestran lo mismo. Así que nadie nota el error, salvo la compradora.

La idea

De un array a una lista

Los productos llegan como datos: un array de objetos con un nombre y un precio. La página tiene que mostrarlos como elementos de una lista. El camino es el mismo para cualquier lista:

function productItem(product) {
const li = document.createElement('li');
li.textContent = `${product.name}, ${money(product.price)}`;
return li;
}
list.replaceChildren(...products.map(productItem));
  • document.createElement('li') crea un elemento nuevo. Todavía no está en la página.
  • textContent pone el texto. El nombre del producto es texto, no HTML, así que ningún carácter suyo se convierte en una etiqueta.
  • products.map(productItem) hace un <li> de cada producto: un array de elementos.
  • list.replaceChildren(...) quita los hijos viejos de la lista y pone los nuevos. ... reparte el array en argumentos sueltos.

Esto se llama renderizar una lista a partir de datos: los datos viven en JavaScript, y la página la construye una función a partir de ellos.

Míralo

El catálogo

HTML, CSS, JS

Cambia en el array el precio del pan y pulsa «Ejecutar». No tocas el HTML: la lista sigue a los datos. Añade un quinto producto al array y aparece como una quinta línea.

Pruébalo

Solo lo que hay en stock

Muestra en la lista solo los productos que hay en stock (stock es mayor que 0). Debajo de la lista, en #count, escribe cuántos son: «Disponibles: 3 de 5».

HTML, CSS, JS

La idea

Un conjunto de datos, tres sitios

El carrito es un array cart. La página lo muestra tres veces: la cantidad de arriba, las líneas y el total. Cada vez que cart cambia, los tres sitios tienen que cambiar juntos.

A mano esto significa: cada función que cambia cart toca cada sitio que lo muestra. add toca las líneas y el total. remove toca las líneas, el total y el contador. Mañana llega un cuarto sitio, por ejemplo «Envío gratis a partir de 30 €», y todas las funciones tienen que acordarse.

Cuando se olvida un sitio, los datos y la pantalla se separan: cart tiene dos productos y la página dice cero. No hay ningún error. Hay una página que muestra algo falso.

Míralo

Mira cómo se separan

const cart = [];
function addByHand(product) {
cart.push(product);
cartList.append(cartItem(product));
total.textContent = `Total: ${money(sum())}`;
}
function render() {
badge.textContent = `Carrito (${cart.length})`;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Total: ${money(sum())}`;
}
function add(product) {
cart.push(product);
render();
}
Paso a paso, en palabras
  1. A la izquierda están los datos: el array cart. A la derecha están los tres sitios de la página que lo muestran: el contador, las líneas y el total.
  2. addByHand(pan). Primero cambian los datos: el array tiene un producto.
  3. Después la función toca la pantalla a mano: una línea nueva y un total nuevo. Cada sitio es una línea de código distinta.
  4. Para el contador nadie escribió una línea. Los datos dicen 1, el contador dice 0. No hay ningún error en la consola: la página simplemente muestra algo falso.
  5. Un producto más, otra vez a mano. La diferencia crece: 2 frente a 0.
  6. Ahora add(): solo cambia los datos. Por un momento toda la pantalla va por detrás, porque todavía no se ha dibujado nada.
  7. render() dibuja otra vez los tres sitios a partir de cart: el contador, cada línea y el total. No le importa lo que hubiera antes en la pantalla.
  8. La regla: cada cambio es primero los datos, después render(). La pantalla se deduce de los datos y un sitio olvidado es imposible.

addByHand toca dos sitios de tres. render toca los tres, cada vez, a partir de los datos. No pregunta qué había antes en la pantalla, así que no puede dejarse engañar por eso.

La idea

Un solo render()

La regla es corta: las funciones que cambian los datos no tocan la pantalla. Cambian los datos y llaman a render(). Solo render() escribe en la página, y lo escribe todo a partir de los datos:

function render() {
badge.textContent = `Carrito (${cart.length})`;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Total: ${money(sum())}`;
}

Así cada sitio se describe una sola vez. Un sitio nuevo es una línea en render(), y funciona para add, para remove y para cualquier función futura. Las líneas se dibujan otra vez por completo: replaceChildren quita las viejas y pone nuevas a partir de cart.map(cartItem).

Pruébalo

Un solo render()

add y remove tocan la pantalla a mano, y cada una olvida un sitio. Escribe render() para que dibuje el contador, las líneas y el total a partir de cart, y haz que add y remove solo cambien cart y llamen a render().

HTML, CSS, JS

Pruébalo

Encuentra el error

El contador siempre va un producto por detrás: después del primer clic dice 0, después del segundo 1.

Elige la línea con el error.

La idea

El precio de «todo otra vez»

render() no sabe qué cambió, así que lo reemplaza todo. Normalmente no se nota. A veces sí. En el carrito de abajo cada línea tiene un campo de cantidad, y cada cambio del campo llama a render(). La compradora quiere 15 leches: borra el 2, escribe 1 y 5.

HTML, CSS, JS

Después del dígito 1, render() quita el campo viejo y pone uno nuevo con el valor 1. El foco estaba en el campo viejo y ya no está, así que el 5 no va a ninguna parte. Lo mismo le pasa al cursor en un campo de texto, a la posición del scroll en una lista larga y a un menú desplegable abierto. Y con 1000 líneas, el navegador construye 1000 elementos nuevos en cada clic.

Los dos caminos son malos: a mano se olvidan sitios, y «todo otra vez» pierde el foco y es lento con una lista grande. La respuesta es un tercer camino: escribes una función que dice cómo se ve la pantalla para estos datos, como render(). Otra cosa compara el resultado nuevo con el viejo y cambia en la página solo la diferencia: el texto del contador, una línea, nada del campo. Esto es lo que hacen React y Vue, y es el módulo siguiente: componentes.

Más precisamente, para curiosos

innerHTML en lugar de createElement. list.innerHTML = products.map((p) => `<li>${p.name}</li>`).join('') es más corto, pero el nombre del producto entra como HTML. Un nombre con <img onerror=…> se convierte en código en la página (XSS). textContent y createElement mantienen el texto como texto.

replaceChildren. Sin argumentos vacía el elemento: list.replaceChildren(). Antes se escribía list.innerHTML = '' y luego append.

Los listeners. Cada botón nuevo recibe un listener nuevo en productItem. Los botones viejos desaparecen junto con sus listeners. Otro camino es la delegación: un listener en el <ul> y event.target.closest('li'), con el propio elemento diciendo de qué producto se trata (data-id).

hidden. element.hidden = true es como el atributo hidden en HTML: el elemento no se muestra. Si una regla CSS le da al elemento un display, la regla gana a hidden y el elemento vuelve a mostrarse.

Conservar el foco a mano. Puedes recordar qué campo tenía el foco y dónde estaba el cursor, y devolverlos después de render(). Funciona, pero es un sitio más que se puede olvidar. Las bibliotecas lo hacen por ti, porque no reemplazan el campo en absoluto.

Ejercicio

El carrito

Construye el catálogo y el carrito a partir de los datos. Cada producto es una línea con un botón «Añadir» (el nombre para un lector de pantalla: «Añadir Pan blanco»). Cada línea del carrito tiene un botón «Quitar» («Quitar Pan blanco»). El contador de arriba es «Carrito (2)», el total es «Total: 10,00 €», y «El carrito está vacío» solo se ve cuando el carrito está vacío (la propiedad hidden).

HTML, CSS, JS

Test

Ponte a prueba

1. El carrito muestra el número de productos en tres sitios. Cuando actualizas a mano, ¿cuál es el error más común?
2. ¿Qué hace list.replaceChildren(...items)?
3. ¿Dónde, dentro de add, va render()?
4. ¿Por qué «dibujar todo otra vez» pierde el foco en un campo dentro de la lista?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·