TaskFlow no té molt sentit si només pot mostrar una tasca alhora. Un tauler Kanban real necessita mostrar col·leccions senceres de tasques: totes les pendents, totes les que estan en progrés, totes les d'una persona concreta. Aquesta lliçó explica com recórrer un array de dades dins d'una plantilla Lit utilitzant Array.map, quins problemes d'identitat poden aparèixer en renderitzar llistes que canvien amb el temps, i per què en aquests casos convé una directiva específica que es detallarà més endavant al curs. Amb aquesta tècnica construiràs <task-list>, el primer component de TaskFlow capaç de mostrar diverses tasques a partir d'una col·lecció de dades.
Contingut
- Llistes de plantilles: un array és un valor interpolable
Array.mapcom a tècnica principal- El problema de la identitat en actualitzar llistes
- La directiva
repeati el concepte dekey: una menció de passada - Quan n'hi ha prou amb
mapi quan convérepeat - Construint
<task-list>
- Llistes de plantilles: un array és un valor interpolable
A la lliçó "El Motor de Plantillas de Lit" es va mencionar, de passada, que un array de valors és un contingut vàlid per interpolar dins d'una plantilla html. És moment d'aturar-se en aquesta idea: quan el resultat d'una interpolació és un array, Lit no el converteix a text (no apareixerà una cosa com "tarea1,tarea2,tarea3" a pantalla); en lloc d'això, renderitza cada element de l'array com si fos una interpolació independent, col·locant cadascun a continuació de l'anterior.
Aquest exemple, encara que poc útil tal com és, ja demostra el comportament: els tres textos de l'array apareixerien un darrere l'altre dins de l'<ul>, sense cap etiqueta <li> que els envolti individualment, perquè l'array conté només text pla. El cas realment útil apareix quan, en lloc d'un array de textos solts, s'interpola un array de plantilles html, una per cada element de les dades originals. Aquí és on entra Array.map.
Array.map com a tècnica principal
Array.map com a tècnica principalEl patró estàndard per renderitzar una llista a Lit consisteix a utilitzar el mètode map dels arrays, ja disponible en JavaScript estàndard sense cap dependència de Lit, per transformar un array de dades en un array de plantilles html:
import { LitElement, html } from 'lit';
class ListaDeTareas extends LitElement {
constructor() {
super();
this.tareas = ['Comprar pan', 'Revisar el PR', 'Llamar al cliente'];
}
render() {
return html`
<ul>
${this.tareas.map((tarea) => html`<li>${tarea}</li>`)}
</ul>
`;
}
}Analitzem l'expressió clau: this.tareas.map((tarea) => html\
. El mètode maprecorre cada element de l'arraythis.tareasi, per cada un, executa la funció indicada, que en aquest cas retorna una plantillahtmlamb unque conté aquell text. El resultat demapno és ja l'array de textos original, sinó un **nou array de plantilleshtml**, una per cada tasca. Aquest nou array és exactament el tipus de valor descrit a l'apartat 1: un array el contingut del qual són plantilles, així que Lit el renderitza col·locant cada un darrere l'altre dins de l'- `.
Aquest patró és tan habitual a Lit (i, en general, a qualsevol framework modern d'interfícies basat en JavaScript) que convé memoritzar-lo com una construcció d'una sola línia: array.map((elemento) => html\...`)`. Es pot utilitzar en qualsevol punt d'una plantilla on tingui sentit repetir una estructura per cada element d'una col·lecció: files d'una taula, opcions d'un desplegable, targetes d'un tauler, elements d'un menú.
Si els objectes de la col·lecció són més complexos que simples cadenes de text, el patró funciona exactament igual, accedint a les propietats de cada objecte dins de la funció:
render() {
return html`
<ul>
${this.tareas.map((tarea) => html`
<li>${tarea.titulo} — ${tarea.estado}</li>
`)}
</ul>
`;
}
- El problema de la identitat en actualitzar llistes
Mentre una llista es renderitza una única vegada i mai més torna a canviar, map no presenta cap problema. Els inconvenients apareixen quan la llista canvia amb el temps: s'afegeixen tasques, s'eliminen, o es reordenen (per exemple, en arrossegar una targeta d'una columna a una altra en un tauler Kanban, una cosa que TaskFlow farà en mòduls posteriors).
Quan Lit torna a executar render() amb un array diferent (encara que només hagi canviat lleugerament respecte a l'anterior), i aquest array es processa amb map com a l'apartat 2, Lit compara, per defecte, la posició de cada plantilla resultant amb la posició que ocupava al renderitzat anterior, no el contingut lògic de cada element. És a dir: "la plantilla que estava a la posició 3 l'última vegada" es compara amb "la plantilla que està a la posició 3 aquesta vegada", sense tenir en compte si es tracta realment de la mateixa tasca original o d'una completament diferent que simplement ha caigut en aquesta posició després de reordenar l'array.
A la pràctica, això pot produir dos tipus de problemes:
- Ineficiència: si s'insereix una tasca nova al principi d'una llista de cent elements, totes les posicions es desplacen una cap avall. Comparant per posició, Lit pot acabar actualitzant el contingut de les cent targetes (perquè "l'element a la posició N" ha canviat a les cent posicions), en lloc de reconèixer que noranta-nou targetes són exactament les mateixes d'abans i només cal inserir-ne una nova al principi.
- Pèrdua d'estat del DOM: si algun node dins d'una d'aquestes plantilles tenia estat propi del navegador (per exemple, el focus d'un camp de text, una animació en marxa, el text seleccionat per l'usuari), reordenar per posició pot fer que aquest estat "salti" a un element lògic diferent del que el tenia originalment, perquè Lit, en no saber quin element és "el mateix d'abans", pot reutilitzar el node físic d'una posició per representar ara dades diferents.
- La directiva
repeat i el concepte de key: una menció de passada
repeat i el concepte de key: una menció de passadaPer resoldre aquest problema, Lit ofereix una directiva anomenada repeat, pensada específicament per a llistes que canvien amb el temps (s'insereixen, eliminen o reordenen elements). La seva forma bàsica és la següent:
import { repeat } from 'lit/directives/repeat.js';
render() {
return html`
<ul>
${repeat(
this.tareas,
(tarea) => tarea.id,
(tarea) => html`<li>${tarea.titulo}</li>`
)}
</ul>
`;
}repeat rep tres arguments: l'array de dades, una funció que calcula una clau única (key) per cada element —normalment un identificador estable, com tarea.id—, i una funció que genera la plantilla per cada element, igual que es faria amb map. Gràcies a aquesta clau, Lit pot identificar cada element per la seva identitat lògica real, no per la seva posició a l'array: si una tasca amb id: 'abc' es mou de la posició 3 a la posició 0, repeat reconeix que continua sent la mateixa tasca i mou el node DOM existent, en lloc de destruir-lo i recrear-ne un de nou amb contingut diferent.
Aquesta directiva, juntament amb la resta del catàleg de directives incorporades a Lit, s'estudia en profunditat al mòdul 7, "Directives i Funcionalitats Avançades de Plantilles". Es menciona aquí, de forma anticipada, únicament perquè sàpigues que existeix una solució concreta al problema descrit a l'apartat 3, i perquè reconeguis la sintaxi si la trobes abans d'arribar a aquest mòdul.
- Quan n'hi ha prou amb
map i quan convé repeat
map i quan convé repeatNo totes les llistes necessiten repeat; utilitzar-lo "per si de cas" en qualsevol llista afegeix una dependència i una capa de complexitat que de vegades no aporta res. Una guia pràctica per decidir:
| Situació | Tècnica recomanada |
|---|---|
| La llista es renderitza una vegada i no torna a canviar durant la vida del component | Array.map és suficient |
| La llista canvia, però sempre es reconstrueix sencera des de zero (per exemple, se substitueix completament l'array a cada actualització, sense conservar elements) | Array.map continua sent raonable |
| La llista pateix insercions, eliminacions o reordenacions freqüents, i conservar l'estat del DOM de cada element importa (focus, animacions, entrades de formulari) | repeat, amb una clau estable com id |
| Els elements de la llista són complexos o costosos de tornar a renderitzar, i es vol evitar recalcular els que no han canviat de posició lògica | repeat |
A TaskFlow, per exemple, quan més endavant al curs s'implementi arrossegar targetes entre columnes del tauler Kanban, aquest serà exactament l'escenari en el qual repeat aportarà una diferència notable: les targetes es mouran de posició sense perdre la seva identitat ni el seu estat intern. De moment, amb dades d'exemple estàtiques que no canvien durant l'execució del component, Array.map és l'eina adequada i la que s'utilitzarà durant la resta d'aquest mòdul.
- Construint
<task-list>
<task-list>Amb la tècnica de Array.map ja explicada, és el moment de construir el segon component real de TaskFlow: <task-list>. La seva responsabilitat és senzilla: rebre una col·lecció de tasques i renderitzar un <task-card> per cadascuna. Igual que a les lliçons anteriors, la col·lecció es declararà com un camp d'instància simple, no com una propietat reactiva (això arriba al mòdul 3).
Primer, assegura't que task-card.js exporta o almenys registra el seu element com a les lliçons anteriors (s'assumeix aquí la versió de la lliçó "Renderizado Condicional", amb insígnia d'estat). Després, crea src/components/task-list.js:
import { LitElement, html } from 'lit';
import './task-card.js';
class TaskList extends LitElement {
constructor() {
super();
// Array d'instància simple, encara sense reactivitat real (mòdul 3).
this.tareas = [
{ id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso' },
{ id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente' },
{ id: 3, titulo: 'Desplegar a producción', estado: 'hecha' },
];
}
render() {
return html`
<section>
<h2>Mis tareas</h2>
<div class="lista">
${this.tareas.map(
(tarea) => html`<task-card></task-card>`
)}
</div>
</section>
`;
}
}
customElements.define('task-list', TaskList);Hi ha un detall important que convé assenyalar explícitament en aquest punt del curs: a la línia html\dins delmap, s'està creant una instància de per cada tasca de l'array, però **encara no se li està passant cap dada concreta d'aquesta tasca**. Com quecontinua sense tenir propietats reactives reals, encara no existeix una forma neta de dir-li "mostra el títol d'*aquesta* tasca en concret"; per això, en aquest punt del curs, les tres targetes mostrades peres veuran totes idèntiques, amb els valors fixos que
Aquesta limitació, precisament, és l'argument més clar per justificar per què el mòdul 3 és imprescindible: en quant <task-card> declari propietats reactives reals, es podrà escriure una cosa com html\<task-card .titulo="${tarea.titulo}" .estado="${tarea.estado}">`(utilitzant la sintaxi de propietats amb punt vista a la lliçó anterior), i cada targeta mostrarà llavors les dades de la seva pròpia tasca. És important que quedi clara aquesta limitació ara, en lloc de deixar-la com una sorpresa confusa:
Per utilitzar el nou component, actualitza index.html:
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <title>TaskFlow</title> <script type="module" src="/src/components/task-list.js"></script> </head> <body> <h1>TaskFlow</h1> <task-list></task-list> </body> </html>
Fixa't que index.html ja no necessita importar task-card.js directament: n'hi ha prou que task-list.js l'importi (import './task-card.js';), perquè aquest import registra igualment l'element <task-card> al navegador, i <task-list> l'utilitza internament a la seva pròpia plantilla. En recarregar la pàgina, hauries de veure el títol "Mis tareas" seguit de tres targetes, una per cada element de l'array this.tareas, encara que les tres mostrin, de moment, el mateix contingut d'exemple.
Errors Comuns i Consells
- Oblidar l'
importdel component fill: si<task-list>no importatask-card.js(directament o de forma indirecta, a través d'un altre fitxer que sí ho faci), el navegador no sabrà què fer amb l'etiqueta<task-card>i la tractarà com un element desconegut, sense cap error visible més enllà que no es vegi res a dins. - Esperar que cada
<task-card>mostri dades diferents ja en aquest mòdul: com s'ha explicat a l'apartat 6, sense propietats reactives encara no existeix una forma neta de passar dades diferents a cada instància; les targetes es veuran iguals fins al mòdul 3, i això és intencionat. - Utilitzar l'índex de
mapcom si fos una clau estable: és temptador escriurethis.tareas.map((tarea, indice) => ...)i pensar enindicecom un identificador únic de cada tasca. No ho és: l'índex depèn de la posició actual a l'array, que canvia si es reordena o s'insereix un element al principi. Per identificar de forma estable un element (per exemple, en utilitzarrepeatmés endavant al curs), cal utilitzar un identificador propi de les dades, comtarea.id. - Renderitzar llistes enormes sense cap estratègia de paginació o virtualització:
Array.mapfunciona bé per a llistes de mida raonable (desenes o fins i tot uns pocs centenars d'elements), però renderitzar milers d'elements de cop pot tornar-se lent independentment de la biblioteca utilitzada. Aquesta consideració de rendiment a gran escala es reprèn al mòdul 9, "Proves i Bones Pràctiques".
Exercicis
- Afegeix una quarta tasca a l'array
this.tareasde<task-list>i comprova que apareix una quarta targeta en recarregar la pàgina. - Modifica
render()de<task-list>perquè, abans de la llista de targetes, mostri un paràgraf amb el número total de tasques, utilitzantthis.tareas.lengthinterpolat directament (sense necessitat demap, ja que és un únic valor). - Investiga (consultant la documentació oficial de Lit sobre la directiva
repeat, o l'apartat 4 d'aquesta lliçó) què passaria de diferent, en termes de quins nodes del DOM es reutilitzen, si s'utilitzésrepeatambtarea.idcom a clau en lloc deArray.map, en inserir una nova tasca al principi de l'array. Redacta la resposta amb les teves paraules, sense necessitat de programar-ho encara.
Solucions
this.tareas = [
{ id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso' },
{ id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente' },
{ id: 3, titulo: 'Desplegar a producción', estado: 'hecha' },
{ id: 4, titulo: 'Actualizar la documentación', estado: 'pendiente' },
];En recarregar la pàgina, Array.map genera automàticament una quarta plantilla <task-card>, sense necessitat de tocar la resta de render().
render() {
return html`
<section>
<h2>Mis tareas</h2>
<p>Total: ${this.tareas.length} tareas</p>
<div class="lista">
${this.tareas.map((tarea) => html`<task-card></task-card>`)}
</div>
</section>
`;
}- Amb
Array.map, en inserir una tasca nova al principi de l'array, totes les tasques existents passen a ocupar una posició diferent de la que tenien (la que era la posició 0 passa a ser la 1, i així successivament); com que Lit compara per posició en aquest cas, és possible que reconstrueixi o actualitzi el contingut de totes les targetes existents, no només que insereixi una nova al principi. Ambrepeatitarea.idcom a clau, Lit identifica cada tasca pel seu identificador, independentment de la posició que ocupi a l'array; en inserir una tasca nova al principi, reconeixeria que les altres tasques són les mateixes d'abans (mateixid) i simplement inseriria un nou node DOM al principi, sense tocar ni reconstruir els nodes ja existents de les altres targetes.
Conclusió
En aquesta lliçó has aprés a renderitzar col·leccions de dades dins d'una plantilla Lit combinant Array.map amb interpolació d'arrays de plantilles html, la tècnica estàndard per a llistes que no canvien de forma complexa durant la vida del component. També has entès per què les llistes que es reordenen o modifiquen amb freqüència poden patir problemes d'identitat si es comparen només per posició, i per què existeix la directiva repeat amb clau (key) per a aquests casos, el detall de la qual s'estudiarà al mòdul 7. Amb aquesta base, TaskFlow ja compta amb <task-list>, capaç de recórrer un array de tasques d'exemple i generar un <task-card> per cadascuna, encara que encara sense poder-li passar dades diferents a cada targeta: aquesta peça queda pendent, de forma conscient, per al mòdul 3.
A l'última lliçó d'aquest mòdul, "El Ciclo de Renderizado", faràs un pas enrere per entendre una cosa que has estat donant per fet fins ara: com i quan decideix Lit exactament quan tornar a executar render(), per què aquest procés és asíncron, i per què mai s'ha de modificar el DOM a mà dins d'aquest mètode, tancant així el mòdul abans de fer el salt a les propietats reactives de veritat al mòdul 3.
Curs de Lit
Mòdul 1: Introducció a Lit i Web Components
- Què són els Web Components i per què Lit?
- Configuració de l'Entorn de Desenvolupament
- El Teu Primer Component Lit
- Anatomia d'un Component Lit
Mòdul 2: Plantilles Reactives i Renderitzat
- El Motor de Plantilles de Lit
- Expressions i Interpolació en Plantilles
- Renderitzat Condicional
- Renderitzat de Llistes
- El Cicle de Renderitzat
Mòdul 3: Propietats i Estat Reactiu
- Propietats Reactives
- Estat Intern amb @state
- Tipus de Propietats i Conversors Personalitzats
- Atributs vs Propietats i Reflexió
Mòdul 4: Estils en Components Lit
- CSS Encapsulat amb Shadow DOM
- Estils Compartits entre Components
- Variables CSS Personalitzades i Theming
- Slots i Estilitzat de Contingut Distribuït
Mòdul 5: Esdeveniments i Comunicació entre Components
- Gestió d'Esdeveniments DOM en Plantilles
- Esdeveniments Personalitzats: Comunicació de Fill a Pare
- Comunicació de Pare a Fill amb Propietats
- Patrons de Comunicació entre Components Germans
Mòdul 6: Cicle de Vida i Comportament Avançat
- Callbacks del Cicle de Vida
- Hooks Reactius: willUpdate, updated i firstUpdated
- Controladors Reactius
- Mixins i Composició de Comportament
Mòdul 7: Directives i Funcionalitats Avançades de Plantilles
- Directives Incorporades: classMap, styleMap i ifDefined
- Directives Personalitzades
- Renderitzat Asíncron amb until
- Context Compartit amb @lit/context
Mòdul 8: Integració, Interoperabilitat i Desplegament
- Utilitzar Components Lit en HTML Pla
- Integrar Lit amb React, Vue i Angular
- Renderitzat al Servidor amb @lit-labs/ssr
- Empaquetatge, Publicació i TypeScript
Mòdul 9: Proves i Bones Pràctiques
- Proves Unitàries amb Web Test Runner
- Accessibilitat en Web Components
- Rendiment i Optimització
- Patrons i Antipatrons Comuns
