TaskFlow, en acabar la lliçó anterior, ja és una aplicació completa i funcional: cinc components muntats, un context compartit, un controlador reactiu, un mixin i un servei simulat, tots coordinats exactament com es va planejar a la primera lliçó d'aquest mòdul. Falta, abans de poder dir que el projecte està realment acabat, comprovar amb proves automatitzades les peces que s'han completat en aquest mòdul i que el mòdul 9 no va poder cobrir perquè encara no existien, empaquetar l'aplicació per al seu desplegament, i decidir si alguna de les seves peces mereix publicar-se per separat com a llibreria reutilitzable. Aquesta és l'última lliçó del curs: no introdueix cap concepte nou de Lit, i tanca, a la seva darrera secció, el recorregut complet iniciat a la lliçó "Què són els Web Components i per què Lit?".

Contingut

  1. Què queda per tancar abans d'acabar
  2. Checklist final de proves: el que 09-01 i 09-02 no van arribar a cobrir
  3. Nous tests: <task-filter> i el flux complet d'un esdeveniment
  4. Build final amb Vite: comandes i estructura de dist/
  5. Publicant <task-card> com a llibreria npm reutilitzable
  6. Checklist de desplegament: lloc estàtic davant de llibreria
  7. TaskFlow, de principi a fi: un repàs mòdul a mòdul
  8. Propers passos, fora d'aquest curs
  9. Tancament del curs

  1. Què queda per tancar abans d'acabar

El mòdul 9 va construir la bateria de proves de TaskFlow (09-01), la seva accessibilitat (09-02), el seu rendiment (09-03) i el seu catàleg de patrons i antipatrons (09-04) sobre l'aplicació tal com existia aleshores: sense <task-filter> ben establert, amb <task-board> encara en una versió anterior de la seva càrrega inicial. Aquest mòdul ha completat aquestes peces, i aquesta lliçó tanca el cercle: estén la bateria de proves al que faltava, deixa l'aplicació preparada per publicar-se, i amb això dona per acabat el curs complet.

  1. Checklist final de proves: el que 09-01 i 09-02 no van arribar a cobrir

Abans d'escriure cap test nou, convé fer un inventari honest de quina part de TaskFlow ja té cobertura i quina no, utilitzant exactament les mateixes eines de la lliçó "Proves Unitàries amb Web Test Runner" (09-01):

Component o flux Té test des del mòdul 9? Què falta cobrir en aquesta lliçó
<task-card>: títol, insígnia d'estat, clic que expandeix Sí (09-01) Res
<task-card>: role, aria-expanded Sí (09-02) Res
<task-filter>: aria-label, aria-pressed, canvi d'estat No Un test que comprovi que prémer un botó actualitza el context de filtre
<task-list>: filtratge segons el context No Un test d'integració que comprovi que el filtre redueix les targetes visibles
tarea-cambiada: l'esdeveniment complet, de <task-card> fins a <task-board> No Un test d'integració que comprovi el cicle esdeveniment amunt / propietat avall en dos salts

Les dues primeres files ja estaven resoltes; les tres últimes són exactament les peces que aquest mòdul ha afegit o completat, i són les que es cobreixen a l'apartat següent.

  1. Nous tests: <task-filter> i el flux complet d'un esdeveniment

El primer test nou comprova <task-filter> de forma aïllada, però per exercitar realment el seu comportament cal embolcallar-la en un proveïdor de context de prova, ja que <task-filter> per si sola, sense cap ancestre que publiqui filtroContext, només veuria el valor de reserva definit en el seu propi valorActual:

// test/task-filter.test.js
import { fixture, html, expect } from '@open-wc/testing';
import { ContextProvider } from '@lit/context';
import { LitElement } from 'lit';
import { filtroContext } from '../src/context/filtro-context.js';
import '../src/components/task-filter.js';

// Un petit component de prova que només publica el context,
// exactament el mateix paper que <task-board> compleix en l'aplicació real.
class ProveedorDePrueba extends LitElement {
  constructor() {
    super();
    this.valor = { texto: '', estado: 'todas', actualizar: (cambios) => this._actualizar(cambios) };
    this._provider = new ContextProvider(this, { context: filtroContext, initialValue: this.valor });
  }
  _actualizar(cambios) {
    this._provider.value = { ...this._provider.value, ...cambios };
    this.valor = this._provider.value;
  }
  render() {
    return html`<slot></slot>`;
  }
}
customElements.define('proveedor-de-prueba', ProveedorDePrueba);

describe('task-filter', () => {
  it('expone aria-pressed="true" solo en el botón del estado activo', async () => {
    const contenedor = await fixture(
      html`<proveedor-de-prueba><task-filter></task-filter></proveedor-de-prueba>`
    );
    const filtro = contenedor.querySelector('task-filter');
    const botones = filtro.shadowRoot.querySelectorAll('button');

    expect(botones[0].getAttribute('aria-pressed')).to.equal('true'); // "todas", actiu per defecte
    expect(botones[1].getAttribute('aria-pressed')).to.equal('false');
  });

  it('actualiza el contexto compartido al pulsar un botón de estado', async () => {
    const contenedor = await fixture(
      html`<proveedor-de-prueba><task-filter></task-filter></proveedor-de-prueba>`
    );
    const proveedor = contenedor;
    const filtro = contenedor.querySelector('task-filter');
    const botonPendientes = filtro.shadowRoot.querySelectorAll('button')[1];

    botonPendientes.click();
    await filtro.updateComplete;

    expect(proveedor.valor.estado).to.equal('pendiente');
  });
});

El segon test comprova el flux complet de l'apartat 2, tercera fila: que un canvi d'estat disparat dins del shadow root d'una <task-card>, diversos nivells per sota, acaba modificant de veritat l'array tareas de <task-board>, exactament el recorregut descrit al mapa de la lliçó 10-01.

// test/task-board.test.js
import { fixture, html, expect, aTimeout } from '@open-wc/testing';
import '../src/components/task-board.js';

describe('task-board: flujo completo de tarea-cambiada', () => {
  it('propaga un cambio de estado desde una task-card hasta el array tareas', async () => {
    const tablero = await fixture(html`<task-board></task-board>`);

    // La càrrega inicial de tareas-service.js tarda 1200 ms, simulats amb
    // setTimeout; aquí s'espera el mateix que s'esperaria al navegador.
    await aTimeout(1300);
    await tablero.updateComplete;

    const lista = tablero.shadowRoot.querySelector('task-list');
    const primeraTarjeta = lista.shadowRoot.querySelector('task-card');
    const selector = primeraTarjeta.shadowRoot.querySelector('select');

    selector.value = 'hecha';
    selector.dispatchEvent(new Event('change'));
    await primeraTarjeta.updateComplete;
    await lista.updateComplete;
    await tablero.updateComplete;

    expect(tablero.tareas[0].estado).to.equal('hecha');
  });
});

aTimeout, importada també de @open-wc/testing, és una petita utilitat que retorna una promesa que es resol després del nombre de mil·lisegons indicat; s'utilitza aquí perquè la càrrega simulada de tareas-service.js (lliçó 07-03, consolidada a 10-03) tarda un temps real, no artificial, i el test necessita esperar aquest mateix temps abans que existeixi cap <task-card> sobre la qual actuar. La resta del test encadena tres updateComplete diferents —de la targeta, de la llista, del tauler— perquè, com es va explicar a la lliçó "Comunicació de Pare a Fill amb Propietats" (05-03) i a "Patrons de Comunicació entre Components Germans" (05-04), l'esdeveniment travessa dos reenviaments abans d'arribar a <task-board>, i cada nivell necessita completar la seva pròpia actualització abans que el següent la reflecteixi.

  1. Build final amb Vite: comandes i estructura de dist/

Amb les proves ja completes, el build de producció de TaskFlow com a aplicació segueix exactament el criteri fixat a la lliçó "Empaquetatge, Publicació i TypeScript" (08-04, apartats 1 i 4): Vite, no Rollup de forma directa, perquè TaskFlow és una aplicació autosuficient, no una llibreria pensada perquè un altre bundler la torni a processar.

npm run build

El resultat, a dist/, queda amb aquesta forma:

dist/
├── index.html
└── assets/
    ├── index-a1b2c3d4.js
    ├── index-e5f6g7h8.css
    └── task-board-i9j0k1l2.js

Cada nom de fitxer inclou un fragment hash derivat del seu contingut (a1b2c3d4, en l'exemple), exactament el mecanisme de control de memòria cau explicat a 08-04: si el contingut d'un fitxer no canvia entre dos desplegaments successius, el seu nom tampoc no canvia, i el navegador de qui ja va visitar TaskFlow pot continuar utilitzant la còpia que ja té a la memòria cau sense tornar-la a descarregar. index.html, regenerat per Vite en cada build, ja apunta als noms de fitxer correctes d'aquesta build concreta, sense que calgui editar manualment cap ruta.

  1. Publicant <task-card> com a llibreria npm reutilitzable

TaskFlow, com a aplicació completa, es desplega amb el build de l'apartat anterior. Però hi cap una pregunta diferent, ja plantejada en abstracte a la lliçó 08-04: val la pena publicar <task-card> per separat, perquè altres projectes —no només TaskFlow— la puguin instal·lar com a dependència d'npm? Si la resposta fos afirmativa (una cosa raonable, atès que <task-card> no depèn de cap dada específica de TaskFlow excepte les que rep com a propietats), el package.json d'aquest paquet independent hauria de declarar lit com a dependència externa, no inclosa en el propi bundle, exactament pel motiu explicat a 08-04 (apartat 3): evitar que dos components de tercers, ambdós dependents de Lit, carreguin dues còpies completes de la biblioteca en la mateixa aplicació.

{
  "name": "@taskflow/task-card",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/task-card.js",
  "module": "dist/task-card.js",
  "files": ["dist"],
  "peerDependencies": {
    "lit": "^3.0.0"
  },
  "devDependencies": {
    "lit": "^3.0.0",
    "@rollup/plugin-node-resolve": "^15.0.0",
    "rollup": "^4.0.0"
  }
}

peerDependencies és la peça que formalitza aquesta decisió: declara que el paquet espera que qui l'instal·li aporti la seva pròpia còpia de lit, en lloc de portar la seva pròpia inclosa. devDependencies manté una còpia de lit disponible només durant el desenvolupament del propi paquet (per poder executar les seves proves o el seu build), sense que aquesta còpia viatgi dins del paquet final publicat. La configuració de Rollup, idèntica a la de la lliçó 08-04, produeix el dist/task-card.js que aquest package.json referencia:

// rollup.config.js (del paquet @taskflow/task-card, no de l'aplicació TaskFlow)
import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/task-card.js',
  external: ['lit', /^lit\//],
  output: {
    file: 'dist/task-card.js',
    format: 'esm',
  },
  plugins: [resolve()],
};

external: ['lit', /^lit\//] és el detall que faltava a l'exemple de la lliçó 08-04: indica a Rollup que no inclogui, al bundle final, ni el propi paquet lit ni cap dels seus submòduls (lit/directives/..., lit/decorators.js), deixant aquests import intactes a la sortida perquè el bundler de qui instal·li @taskflow/task-card els resolgui amb la seva pròpia còpia de Lit, coherent amb la declaració de peerDependencies del package.json.

  1. Checklist de desplegament: lloc estàtic davant de llibreria

Pas Com a aplicació completa (dist/ de l'apartat 4) Com a llibreria (@taskflow/task-card de l'apartat 5)
Eina de build Vite (vite build) Rollup, amb lit com a dependència externa
Inclou una còpia de Lit? Sí, el bundle és autosuficient No, es declara com a peerDependency
On es publica Un servidor de fitxers estàtics o una CDN El registre d'npm
Qui ho consumeix Persones que visiten TaskFlow directament al seu navegador Altres projectes, com una dependència més del seu propi package.json
Quines proves han de passar abans Tota la bateria de test/*.test.js (mòdul 9 i aquesta lliçó) Els mateixos tests, executats contra el propi paquet abans de publicar cada versió

Els dos camins són legítims i no s'exclouen mútuament: res impedeix que TaskFlow es despleguí com a aplicació completa (apartat 4) mentre, en paral·lel, <task-card> es publiqui també com a paquet independent (apartat 5) perquè altres equips la reutilitzin fora de TaskFlow, exactament el mateix component en tots dos casos, només empaquetat de dues formes diferents segons a qui va dirigit cada resultat.

  1. TaskFlow, de principi a fi: un repàs mòdul a mòdul

Amb TaskFlow ja provat, empaquetat i llest per desplegar-se, val la pena mirar enrere i recórrer, d'un cop d'ull, el camí complet d'aquest curs:

Mòdul Què va aportar a TaskFlow
1. Introducció a Lit i Web Components El primer component Lit, sense cap dada ni interacció encara.
2. Plantilles Reactives i Renderitzat El motor de plantilles, condicionals, llistes i el cicle de renderitzat.
3. Propietats i Estat Reactiu titulo, estado, prioridad, expandida, i el conversor de fechaLimite.
4. Estils en Components Lit Shadow DOM, estils compartits, variables CSS de theming, i <user-avatar> amb slots.
5. Esdeveniments i Comunicació entre Components tarea-cambiada, el patró d'elevar l'estat, i el naixement de <task-board>.
6. Cicle de Vida i Comportament Avançat ContadorTiempoRestanteController i el mixin ConEstadoCarga.
7. Directives i Funcionalitats Avançades de Plantilles classMap, resaltarSiUrgente, until, i <task-filter> amb @lit/context.
8. Integració, Interoperabilitat i Desplegament Ús en HTML pla i en altres frameworks, SSR, empaquetatge amb Rollup i Vite.
9. Proves i Bones Pràctiques La primera bateria de tests, accessibilitat, rendiment, i el catàleg d'antipatrons.
10. Projecte: Construint TaskFlow Arquitectura consolidada, components unificats, i el tancament final d'aquesta lliçó.

Cap fila d'aquesta taula és una sorpresa a aquesta alçada del curs; s'inclou aquí, a l'última lliçó, precisament perquè el recorregut complet es vegi d'un sol cop, una cosa que cap lliçó anterior, centrada en el seu propi mòdul, podia oferir.

  1. Propers passos, fora d'aquest curs

Aquest curs ha cobert Lit de forma exhaustiva per al tipus d'aplicació que representa TaskFlow, però l'ecosistema al voltant de Lit continua creixent, i convé deixar assenyalats alguns camins concrets per a qui vulgui continuar més enllà d'aquesta última lliçó:

  • @lit-labs/virtualizer: la lliçó "Rendiment i Optimització" (09-03, apartat 9) va esmentar la virtualització com a tècnica per a llistes de desenes de milers d'elements, fora de l'abast pràctic d'aquest curs, i va assenyalar que TaskFlow no la necessitava. És, literalment, el següent pas natural si <task-list> hagués de créixer molt més enllà dels volums de dades gestionats al mòdul 9.
  • @lit-labs/motion: un paquet experimental del propi ecosistema de Lit per a animacions declaratives entre estats, un terreny que aquesta lliçó només ha tocat de forma manual amb l'animació CSS de resaltarSiUrgente (lliçó 10-02).
  • @lit-labs/observers: controladors reactius ja escrits per l'equip de Lit per embolcallar ResizeObserver, IntersectionObserver i altres API d'observació del navegador, seguint exactament el mateix patró de ReactiveController estudiat a la lliçó 06-03 amb ContadorTiempoRestanteController.
  • Explorar Lit amb TypeScript i decoradors més a fons: la lliçó 08-04 va presentar l'equivalència sintàctica entre static properties i decoradors; migrar progressivament TaskFlow, component a component, és un exercici pràctic natural per consolidar aquesta lliçó amb un projecte real.
  • La documentació oficial a lit.dev: en particular la seva secció de receptes (Playground) i les seves notes de cada versió, per seguir de prop qualsevol canvi futur de l'API a mesura que Lit evolucioni més enllà de la versió estudiada en aquest curs.

  1. Tancament del curs

Aquest curs va començar, a la lliçó "Què són els Web Components i per què Lit?", amb una pregunta sobre estàndards de la plataforma web i una promesa: aprendre a construir interfícies reactives amb components propis del navegador, sense dependre d'un framework d'aplicació complet. Deu mòduls i trenta-nou lliçons després, aquesta promesa s'ha complert amb una aplicació real i completa, TaskFlow, construïda peça a peça, mòdul a mòdul, sense deixar cap cap solt: cada concepte presentat es va aplicar de seguida sobre el propi projecte, i cada peça esmentada sense resoldre del tot —des de <task-filter> al mòdul 5 fins a l'animació de resaltarSiUrgente en aquesta última lliçó— va trobar el seu lloc abans que el curs acabés.

Qui hagi seguit el curs complet, escrivint i provant cada exemple, ja té el criteri necessari per abordar un projecte real amb Lit: quan cal elevar l'estat a un ancestre comú i quan recórrer a un context compartit; quan un controlador reactiu i quan un mixin; quan until i quan una propietat de càrrega explícita; i, sobretot, l'hàbit de preguntar-se, davant de cada decisió de disseny, quina de les dues alternatives evita millor els antipatrons catalogats al mòdul 9. Aquest criteri, més que la memorització d'una API concreta, és el que sobreviu un cop tancat un curs.

Enhorabona per haver arribat fins aquí. TaskFlow queda acabat, provat i llest per desplegar-se; la resta del camí, amb Lit o amb qualsevol altra eina, queda ja a les teves mans.

Errors Comuns i Consells

  • Publicar una llibreria sense declarar lit com a dependència externa: com es va explicar a l'apartat 5, i ja s'havia advertit a la lliçó 08-04, oblidar external a la configuració de Rollup inclouria una còpia completa de Lit dins del paquet publicat, amb el risc de duplicació ja conegut.
  • Executar vite build esperant el mateix resultat que per a una llibreria: com recorda la taula de l'apartat 6, cada camí de desplegament té una eina i una configuració diferents; mesclar tots dos objectius en la mateixa configuració de build sol produir un resultat que no serveix bé per a cap dels dos casos.
  • Donar per bona una bateria de tests incompleta només perquè el mòdul 9 ja la va començar: com s'ha vist a l'apartat 2, <task-filter> i el flux complet de tarea-cambiada no tenien cap test fins a aquesta lliçó; convé revisar, abans de donar cap projecte per provat, quines peces es van afegir després de l'última vegada que es van escriure tests.
  • Tractar aquesta lliçó com el final de l'aprenentatge, no del curs: com assenyala l'apartat 8, l'ecosistema de Lit continua viu més enllà d'aquestes quaranta lliçons; el criteri après aquí és la base per continuar explorant, no un punt final.

Exercicis

  1. Afegeix un test a test/task-board.test.js que comprovi que, després de canviar el text de cerca a l'<input> de <task-filter> (accessible des de tablero.shadowRoot.querySelector('task-filter').shadowRoot.querySelector('input')), el nombre de <task-card> visibles dins de <task-list> es redueix en conseqüència. Recorda esperar updateComplete a cada nivell implicat, com al test de l'apartat 3.
  2. Completa el package.json de l'apartat 5 afegint un script "build": "rollup -c" i explica, basant-te en la lliçó 08-04 (apartat 2), per què aquest paquet utilitza Rollup mentre que l'aplicació TaskFlow completa, a l'apartat 4 d'aquesta lliçó, utilitza Vite per al mateix verb "fer el build".
  3. Repassa la taula de l'apartat 7 i, per al mòdul que consideris que va aportar el canvi més important en l'arquitectura general de TaskFlow (no en una tècnica concreta, sinó en com s'organitzen els components entre si), justifica la teva elecció amb un exemple concret del propi projecte.

Solucions

it('reduce las tarjetas visibles al escribir en el filtro de texto', async () => {
  const tablero = await fixture(html`<task-board></task-board>`);
  await aTimeout(1300);
  await tablero.updateComplete;

  const filtro = tablero.shadowRoot.querySelector('task-filter');
  const lista = tablero.shadowRoot.querySelector('task-list');
  const input = filtro.shadowRoot.querySelector('input');

  const tarjetasAntes = lista.shadowRoot.querySelectorAll('task-card').length;

  input.value = 'demo';
  input.dispatchEvent(new Event('input'));
  await filtro.updateComplete;
  await lista.updateComplete;

  const tarjetasDespues = lista.shadowRoot.querySelectorAll('task-card').length;
  expect(tarjetasDespues).to.be.lessThan(tarjetasAntes);
});
  1. Aquest paquet utilitza Rollup perquè el seu objectiu, tal com es va explicar a la lliçó 08-04 (apartat 1) i es va recordar a l'apartat 6 d'aquesta lliçó, és produir una llibreria reutilitzable per altres bundlers, no una aplicació autosuficient llesta per servir-se directament; Rollup genera una sortida neta i propera al codi d'entrada, pensada precisament perquè un bundler extern (el de qui instal·li @taskflow/task-card) la torni a processar segons les seves pròpies necessitats. L'aplicació TaskFlow completa, en canvi, no té cap consumidor extern que la torni a empaquetar: el seu objectiu és el resultat final ja optimitzat per al navegador, amb divisió en fragments i gestió de memòria cau per hash, que Vite ofereix de fàbrica (utilitzant Rollup per sota, però amb una configuració orientada a aplicacions, no a llibreries).
  2. Una resposta raonable assenyala el mòdul 5, amb l'aparició de <task-board>: abans d'aquesta lliçó, <task-list> mantenia el seu propi array de tasques d'exemple i no existia cap component capaç de coordinar més de dos nivells de la jerarquia; a partir de "Patrons de Comunicació entre Components Germans" (05-04), tota l'arquitectura de TaskFlow va passar a organitzar-se al voltant d'un ancestre comú que reparteix propietats cap avall i recull esdeveniments cap amunt, el mateix patró que, ampliat amb el context de filtre al mòdul 7, continua vigent sense canvis estructurals fins a la versió final consolidada en aquest mòdul 10. Altres respostes igualment vàlides podrien assenyalar el mòdul 7, per la introducció de @lit/context com a alternativa al reenviament manual de propietats, o el mòdul 4, per l'aparició de <user-avatar> com a primer exemple de descomposició en un component fill reutilitzable.

Curs de Lit

Mòdul 1: Introducció a Lit i Web Components

Mòdul 2: Plantilles Reactives i Renderitzat

Mòdul 3: Propietats i Estat Reactiu

Mòdul 4: Estils en Components Lit

Mòdul 5: Esdeveniments i Comunicació entre Components

Mòdul 6: Cicle de Vida i Comportament Avançat

Mòdul 7: Directives i Funcionalitats Avançades de Plantilles

Mòdul 8: Integració, Interoperabilitat i Desplegament

Mòdul 9: Proves i Bones Pràctiques

Mòdul 10: Projecte: Construint TaskFlow

© Copyright 2026. Tots els drets reservats