La darrera lliçó del mòdul anterior es tancava amb una promesa: amb TaskFlow funcionalment complet —<task-board> com a orquestrador i proveïdor de context, <task-list> filtrant i renderitzant amb repeat, <task-card> amb les seves insígnies, el seu avatar i la seva urgència calculada, i <task-filter> llegint i escrivint el context compartit—, toca veure com aquesta mateixa aplicació s'integra amb la resta del món: HTML pla, altres frameworks, renderitzat al servidor i empaquetatge. Aquesta lliçó comença pel cas més senzill de tots, i precisament per això el més revelador: utilitzar un component Lit dins d'una pàgina HTML corrent, sense cap projecte Vite al darrere, sense cap pas de compilació, sense cap framework d'aplicació. Si alguna cosa d'això sorprèn, és el senyal de que convé repassar per què és possible abans de veure com es fa.
Contingut
- Per què un component Lit funciona en qualsevol HTML
- Carregar Lit i un component sense cap pas de compilació
<script type="module">des d'un CDN: unpkg i esm.sh- La limitació dels atributs: per què no n'hi ha prou d'escriure
tareas="..." - Establint propietats complexes des de JavaScript pur
- Exemple complet:
<task-card>en una pàgina HTML estàtica - Quan encara fa falta un projecte amb build
- Per què un component Lit funciona en qualsevol HTML
La lliçó 01-01 ja va avançar la idea, sense desenvolupar-la encara: un component Lit, un cop definit amb customElements.define(...), és un Custom Element estàndard, exactament el mateix tipus d'objecte que <video> o <details>. Lit no inventa un mecanisme propi de registre de components ni exigeix cap entorn d'execució especial perquè un element personalitzat funcioni; es basa, en tot moment, en l'API customElements que qualsevol navegador modern implementa de forma nativa. La conseqüència pràctica és tan directa com sorprenent per a qui ve d'un framework d'aplicació: qualsevol pàgina HTML que carregui el codi JavaScript d'un component Lit pot utilitzar l'etiqueta d'aquest component, sense necessitat de React, sense necessitat de Vue, sense necessitat de cap compilador ni empaquetador.
Això no és una casualitat de disseny, sinó l'objectiu explícit de Lit des de la primera lliçó d'aquest curs: produir components que siguin, per damunt de tot, estàndards de la plataforma web, i que la capa de conveniència que Lit hi afegeix per sobre (plantilles reactives, actualitzacions eficients, static properties) desaparegui en temps d'execució, deixant només el Custom Element resultant. Tot el projecte Vite utilitzat durant el curs fins ara ha estat una comoditat per al desenvolupament —recàrrega en calent, resolució de mòduls, servidor local—, no un requisit del propi Lit per funcionar en un navegador.
- Carregar Lit i un component sense cap pas de compilació
El projecte Vite de TaskFlow, utilitzat en totes les lliçons anteriors, resol les importacions (import { LitElement, html, css } from 'lit') a través de node_modules, processant-les amb un empaquetador abans de servir-les al navegador. Una pàgina HTML plana, sense cap servidor de desenvolupament al darrere, no té aquest pas intermedi: el navegador necessita rebre directament una URL vàlida per a cada mòdul que importa. Els navegadors moderns ja saben executar mòduls ES natius amb <script type="module">, i ja saben resoldre importacions amb ruta relativa o absoluta, però no saben, per si mateixos, quin fitxer correspon al nom 'lit' sense més context; aquest és exactament el treball que fa un empaquetador amb node_modules, i que falta per complet en una pàgina sense build.
Hi ha dues formes de resoldre aquesta absència: apuntar directament a un CDN que serveixi Lit ja empaquetat amb una URL completa, o generar un bundle propi del component (incloent-hi Lit dins) amb les eines que es veuran a la lliçó 08-04. Aquesta lliçó se centra en la primera opció, la més immediata per a un cas d'ús puntual —una demo, una pàgina de documentació, un widget aïllat— que no justifica muntar un projecte complet de build.
<script type="module"> des d'un CDN: unpkg i esm.sh
<script type="module"> des d'un CDN: unpkg i esm.shEls dos CDN més habituals per consumir paquets npm directament com a mòduls ES, sense cap pas d'instal·lació local, són unpkg i esm.sh. Tots dos agafen un paquet publicat a npm (com lit) i el serveixen ja convertit en un mòdul ES que un navegador pot importar directament per URL:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>TaskFlow — Demo sin build</title>
</head>
<body>
<task-card titulo="Revisar propuesta" estado="pendiente" prioridad="2"></task-card>
<script type="module">
import { LitElement, html, css } from 'https://esm.sh/lit@3';
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
estado: { type: String },
prioridad: { type: Number },
};
render() {
return html`
<article>
<h3>${this.titulo}</h3>
<p>Estado: ${this.estado} · Prioridad: ${this.prioridad}</p>
</article>
`;
}
static styles = css`
article {
border: 1px solid #ccc;
border-radius: 8px;
padding: 1rem;
}
`;
}
customElements.define('task-card', TaskCard);
</script>
</body>
</html>Aquest fitxer, desat com index.html i obert directament en un navegador (o servit per qualsevol servidor HTTP mínim, inclòs el propi python -m http.server), renderitza <task-card> sense cap package.json, sense cap instal·lació de dependències i sense cap pas de compilació previ. El navegador descarrega https://esm.sh/lit@3 en el moment en què executa l'import, exactament com descarregaria qualsevol altre recurs extern, i a partir d'aquí LitElement, html i css estan disponibles per a la resta de l'script, igual que en el projecte Vite del curs.
La diferència pràctica entre unpkg i esm.sh és sobretot de format d'URL i d'opcions de resolució: esm.sh tendeix a generar mòduls ES més directament utilitzables sense paràmetres addicionals, mentre que unpkg serveix el contingut tal com està publicat al paquet npm, que a vegades requereix apuntar explícitament al fitxer del build ja en format de mòdul ES (per exemple, https://unpkg.com/lit@3/index.js?module). Per a un cas d'ús puntual, qualsevol dels dos compleix; l'important és fixar sempre una versió concreta (lit@3, no lit sense més), perquè la pàgina no canviï de comportament sense avís si el paquet publica una versió nova amb canvis incompatibles.
- La limitació dels atributs: per què no n'hi ha prou d'escriure
tareas="..."
tareas="..."L'exemple de l'apartat anterior utilitza només atributs de tipus cadena o número (titulo, estado, prioridad), i per això funciona sense cap matís addicional: HTML és, per definició, un llenguatge d'atributs de text, i Lit converteix automàticament aquestes cadenes al tipus declarat a static properties (com es va explicar a la lliçó 03-03 sobre conversors de tipus). El problema apareix tan bon punt cal establir una propietat complexa —un array, un objecte— directament des del marcat HTML, cosa que TaskFlow necessita constantment: <task-list> rep un array complet de tasques a través de la seva propietat tareas.
<!-- Esto NO funciona como cabría esperar -->
<task-list tareas="[{"id":"t1","titulo":"Diseñar"}]"></task-list>Res no impedeix, tècnicament, escriure un atribut amb una cadena que contingui JSON serialitzat a mà, amb les cometes escapades com a entitats HTML ("), però és una solució fràgil i incòmoda de mantenir, i a més exigeix que el propi component s'encarregui d'invocar JSON.parse(...) sobre el valor de l'atribut abans de poder-lo utilitzar com un array real, un pas que cap dels components de TaskFlow implementa (tareas es declara { type: Array }, i el conversor per defecte de Lit per a Array no fa aquesta feina de deserialitzar JSON de forma automàtica i segura per a qualsevol estructura arbitrària). La limitació de fons és la mateixa que ja va aparèixer, des d'un altre angle, a la lliçó 03-04: els atributs HTML són sempre cadenes de text; les propietats de JavaScript poden ser qualsevol valor del llenguatge, inclosos objectes i arrays, però només són accessibles des de codi, no des del marcat HTML en si mateix.
- Establint propietats complexes des de JavaScript pur
La solució, exactament igual que dins del propi Lit, és establir la propietat directament en JavaScript, no com un atribut de marcat:
<task-list id="lista-principal"></task-list>
<script type="module">
const lista = document.querySelector('#lista-principal');
lista.tareas = [
{ id: 't1', titulo: 'Diseñar la base de datos', estado: 'hecha', prioridad: 2 },
{ id: 't2', titulo: 'Implementar autenticación', estado: 'progreso', prioridad: 3 },
{ id: 't3', titulo: 'Escribir pruebas de integración', estado: 'pendiente', prioridad: 1 },
];
</script>document.querySelector('#lista-principal') retorna la instància real de l'element <task-list> ja connectat al DOM, i lista.tareas = [...] assigna directament la propietat reactiva tareas, exactament el mateix mecanisme que dispara Lit quan una plantilla utilitza .tareas="${...}" amb el prefix de punt (vist per primera vegada a la lliçó 05-03). No hi ha cap diferència de fons entre assignar una propietat des d'una plantilla Lit dins d'un altre component i assignar-la directament des d'una línia de JavaScript solta en una pàgina HTML plana: en tots dos casos, Lit detecta el canvi de valor a través del mateix mecanisme de propietats reactives estudiat des del mòdul 3, i programa un nou renderitzat si correspon.
Aquest és, en definitiva, el criteri general per decidir entre atribut i propietat en integrar un component Lit en HTML pla: qualsevol valor que sigui de forma natural una cadena, un número o un booleà simple es pot escriure directament com a atribut al marcat; qualsevol valor que sigui un objecte, un array, una funció o qualsevol estructura que no tingui una representació textual raonable necessita assignar-se com a propietat des de JavaScript, després d'obtenir una referència a l'element amb document.querySelector o equivalent.
- Exemple complet:
<task-card> en una pàgina HTML estàtica
<task-card> en una pàgina HTML estàticaUnint les dues tècniques anteriors, així es veuria una pàgina HTML completament estàtica, sense cap projecte Vite ni pas de build, mostrant una única <task-card> de TaskFlow amb dades mixtes —unes com a atributs, una altra (una funció de callback) com a propietat assignada des de JavaScript—:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>TaskFlow — Tarjeta suelta</title>
</head>
<body>
<task-card
id="tarjeta-demo"
titulo="Revisar propuesta de cliente"
estado="progreso"
prioridad="3"
></task-card>
<script type="module">
import 'https://esm.sh/lit@3';
import './task-card.js';
const tarjeta = document.querySelector('#tarjeta-demo');
// asignadoImagen es una URL opcional; se establece como propiedad
// porque, aunque sea una cadena, en este caso concreto conviene
// decidir su valor dinámicamente en JavaScript antes de mostrarla.
tarjeta.asignadoImagen = obtenerImagenDeUsuarioActual();
function obtenerImagenDeUsuarioActual() {
return localStorage.getItem('avatar-url') ?? undefined;
}
</script>
</body>
</html>./task-card.js és aquí un fitxer propi del projecte (no un component de tercers), que defineix <task-card> exactament com s'ha construït al llarg del curs, amb l'única diferència que les seves pròpies importacions de lit també s'han de resoldre contra una URL de CDN si el fitxer se serveix tal com és, sense cap pas de build previ (cosa que la lliçó 08-04 explicarà com evitar, generant en el seu lloc un bundle autocontingut). L'import 'https://esm.sh/lit@3'; solt, sense capturar cap valor, no és necessari si task-card.js ja importa Lit pel seu compte des de la mateixa URL; s'inclou aquí només per deixar explícita la dependència en l'exemple.
- Quan encara fa falta un projecte amb build
Res del que precedeix invalida el projecte Vite utilitzat durant la resta del curs: per a un desenvolupament seriós de TaskFlow, amb diversos components que s'importen entre si, amb recàrrega en calent durant el desenvolupament, amb comprovació de tipus (si s'opta per TypeScript, com es veurà a la lliçó 08-04) i amb un procés de construcció que optimitzi el resultat final, un projecte amb build continua sent l'opció correcta. El cas d'aquesta lliçó —HTML pla carregant components des d'un CDN— té el seu lloc en escenaris més acotats: una demo ràpida, una pàgina de documentació que mostra un component aïllat, un widget que un tercer ha de poder incrustar en la seva pròpia web sense dependre de la cadena de build de TaskFlow, o simplement una forma de comprovar que un component exportat funciona igual de bé fora del projecte que el va originar. Reconèixer en quin escenari s'està, en cada cas, evita tant muntar un projecte de build innecessari per a una demo puntual com intentar mantenir una aplicació complexa sencera a base de <script type="module"> solts sense cap eina de per mig.
Errors Habituals i Consells
- Intentar passar un array o un objecte com a atribut de text: com s'ha explicat a l'apartat 4, els atributs HTML són sempre cadenes; una propietat complexa s'ha d'assignar des de JavaScript (
elemento.propiedad = valor), mai escriure-la serialitzada a mà dins d'un atribut, tret que el propi component implementi explícitament aquesta deserialització. - Oblidar fixar una versió concreta en importar des d'un CDN: escriure
https://esm.sh/litsense cap versió pot servir, en un moment donat, una versió diferent de la que es va provar, amb canvis de comportament inesperats; fixar semprelit@3(o la versió exacta que correspongui) evita aquest risc. - Intentar accedir a la propietat abans que l'element estigui definit: si l'script que assigna
elemento.tareas = [...]s'executa abans quecustomElements.define('task-list', TaskList)hagi corregut (per exemple, per un ordre de scripts incorrecte), l'assignació continua funcionant gràcies al mecanisme d'upgrade de Custom Elements (Lit recorda els valors assignats abans de la definició i els aplica tan bon punt l'element es registra), però convé no dependre d'aquest detall per a codi nou i mantenir sempre l'ordre lògic: definir el component abans de manipular les seves instàncies. - Servir la pàgina amb
file://en lloc d'un servidor HTTP mínim: alguns navegadors restringeixen l'importde mòduls ES quan la pàgina s'obre directament des del sistema de fitxers, sense cap servidor al darrere; un servidor HTTP mínim comnpx serveopython -m http.serverresol aquest problema en segons i evita errors de CORS confusos.
Exercicis
- Escriu una pàgina HTML plana, sense cap projecte Vite, que carregui Lit des de
https://esm.sh/lit@3, defineixi un component<contador-simple>amb una propietat d'estatvalor(inicialitzada a0) i un botó que la incrementi a cada clic, i la mostri a la seva plantilla. - Un company d'equip, integrant
<task-list>en una pàgina HTML plana, escriu<task-list tareas="3"></task-list>esperant mostrar tres tasques, i es sorprèn que no funcioni. Explica, basant-te en l'apartat 4, per què aquesta expressió no aconsegueix l'efecte esperat i què caldria escriure en el seu lloc. - Retorna a l'exemple de l'apartat 6 i modifica l'script perquè, en lloc d'assignar
asignadoImagende forma síncrona, l'assigni després d'una crida afetch(...)que tarda un temps a resoldre's. Continua funcionant el mecanisme de propietats reactives de Lit igual de bé en aquest cas? Raona la teva resposta basant-te en el que s'ha estudiat sobre el cicle de renderitzat a la lliçó 02-05.
Solucions
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Contador simple</title>
</head>
<body>
<contador-simple></contador-simple>
<script type="module">
import { LitElement, html } from 'https://esm.sh/lit@3';
class ContadorSimple extends LitElement {
static properties = {
valor: { state: true },
};
constructor() {
super();
this.valor = 0;
}
incrementar() {
this.valor++;
}
render() {
return html`
<p>Valor: ${this.valor}</p>
<button @click="${this.incrementar}">Incrementar</button>
`;
}
}
customElements.define('contador-simple', ContadorSimple);
</script>
</body>
</html>tareas="3"estableix un atribut de text amb el valor"3", no una propietat amb un array de tres elements; com s'ha explicat a l'apartat 4, HTML no té cap sintaxi per expressar directament "un array amb tres tasques" dins d'un atribut, i el conversor de tipusArrayde Lit no interpreta"3"com una instrucció per generar tres tasques de cap tipus. Per aconseguir l'efecte esperat cal obtenir una referència a l'element des de JavaScript i assignar la propietat directament, com a l'apartat 5:document.querySelector('task-list').tareas = [tarea1, tarea2, tarea3].- Sí, continua funcionant exactament igual: el mecanisme de propietats reactives de Lit, estudiat des del mòdul 3, no depèn de que l'assignació passi de forma síncrona tot just carregar la pàgina; reacciona a qualsevol assignació a la propietat en qualsevol moment de la vida del component, ja estigui connectat des del principi o es connecti més endavant. Tan bon punt la promesa de
fetch(...)es resolgui i l'script executitarjeta.asignadoImagen = ..., Lit detecta el canvi de valor i programa un nou renderitzat pel mateix cicle asíncron per microtasca explicat a la lliçó 02-05, exactament com si aquesta mateixa assignació hagués passat dins d'un altre component Lit en lloc d'en un script solt d'una pàgina HTML plana.
Conclusió
Aquesta lliçó ha mostrat que un component Lit no necessita cap entorn especial per funcionar: en ser, en última instància, un Custom Element estàndard, n'hi ha prou de carregar el seu codi —des d'un CDN o des d'un fitxer propi— en qualsevol pàgina HTML per utilitzar-lo amb normalitat, amb l'únic matís que les propietats complexes (arrays, objectes) s'han d'assignar des de JavaScript, no escriure's com a atributs de marcat. Aquest és el cas més simple d'interoperabilitat, però no l'únic: la lliçó següent fa un pas més enllà i examina com s'integra un component Lit dins d'aplicacions construïdes amb altres frameworks —React, Vue i Angular—, cadascun amb el seu propi conjunt de friccions en tractar amb Custom Elements que els seus propis models de components no anticipen del tot.
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
