El tancament del mòdul anterior deixava una promesa pendent: diverses vegades, al llarg d'aquest curs, s'ha mencionat "de passada" una eina de Lit sense aturar-se a explicar-la, prometent que el seu moment arribaria al mòdul 7. Aquest moment és ara. Aquesta lliçó presenta el concepte de directiva i les tres primeres directives incorporades del catàleg de Lit —classMap, styleMap i ifDefined—, aplicant-les directament sobre codi real de TaskFlow escrit en mòduls anteriors, perquè la millora es note de forma immediata i no quede com una idea abstracta.
Contingut
- Què és una directiva a Lit
classMap: alternar classes des d'un objecte- Reescrivint la insígnia d'estat de
<task-card>ambclassMap - Combinar
classMapamb classes estàtiques: el que sí i el que no styleMap: estils inline dinàmicsifDefined: ometre un atribut quan el seu valor ésundefined- Aplicant
ifDefineda<user-avatar> - Quan NO fa falta una directiva incorporada
- Què és una directiva a Lit
Durant tot aquest curs, les expressions ${...} dins d'una plantilla html s'han limitat a produir valors corrents de JavaScript: cadenes, números, booleans, arrays de plantilles, o una altra plantilla html niada. Lit sap què fer amb cadascun d'aquests tipus perquè els reconeix de forma nativa (un array es recorre i s'insereix cada element, una plantilla niada s'insereix com a fragment de DOM, i així successivament, tal com es va explicar al mòdul 2).
Una directiva és un tipus especial de valor, reconeixible pel motor de plantilles de Lit, que en lloc de convertir-se directament en text o en un node, li indica a Lit un comportament concret per gestionar aquella posició de la plantilla. Visualment, una directiva s'usa exactament igual que qualsevol altre valor interpolat —com el resultat de cridar una funció dins de ${}—, però internament el resultat d'aquesta crida no és una cadena ni un array: és un objecte especial que Lit identifica i tracta de forma diferenciada.
import { classMap } from 'lit/directives/class-map.js';
html`<div class="${classMap({ activo: true })}"></div>`;classMap({ activo: true }) no retorna la cadena "activo"; retorna un objecte directiva que, col·locat en posició d'atribut class, li diu a Lit: "gestiona tu l'atribut class d'aquest element a partir d'aquest objecte, activant o desactivant cada classe segons el seu valor booleà, en cada actualització". Cada directiva incorporada de Lit viu al seu propi mòdul dins de lit/directives/, i cal importar-la explícitament abans d'usar-la; no formen part del nucli de lit que s'importa habitualment (LitElement, html, css).
classMap: alternar classes des d'un objecte
classMap: alternar classes des d'un objecteclassMap rep un únic argument: un objecte les claus del qual són noms de classe CSS i els valors expressions booleanes. Per cada clau amb valor veritable, la classe corresponent s'afegeix a l'element; per cada clau amb valor fals, s'elimina (o simplement no s'afegeix, si mai hi va ser present).
import { classMap } from 'lit/directives/class-map.js';
const clases = {
tarjeta: true,
'tarjeta--urgente': this.urgente,
'tarjeta--expandida': this.expandida,
};
html`<article class="${classMap(clases)}">...</article>`;Davant l'alternativa manual —construir la cadena de classes a mà, alguna cosa com `tarjeta ${this.urgente ? 'tarjeta--urgente' : ''} ${this.expandida ? 'tarjeta--expandida' : ''}`.trim()—, classMap elimina per complet la gestió d'espais en blanc, de condicionals niats i del risc de deixar classes "fantasma" actives quan la condició ja no es compleix. La lògica es redueix a un objecte pla, fàcil de llegir d'un cop d'ull: cada línia és "aquesta classe, si aquesta condició".
- Reescrivint la insígnia d'estat de
<task-card> amb classMap
<task-card> amb classMapLa lliçó 02-03 va introduir renderInsigniaEstado(), una funció auxiliar amb tres branques if que decidia tant el text com la classe CSS de la insígnia segons this.estado:
// Versió de la lliçó 02-03, sense classMap
renderInsigniaEstado() {
if (this.estado === 'hecha') {
return html`<span class="insignia insignia--hecha">✓ Hecha</span>`;
}
if (this.estado === 'progreso') {
return html`<span class="insignia insignia--progreso">⏳ En progreso</span>`;
}
return html`<span class="insignia insignia--pendiente">○ Pendiente</span>`;
}Aquesta versió funciona perfectament bé i no hi ha cap urgència real per substituir-la —de fet, quan el text visible també canvia segons la condició (com aquí, "✓ Hecha" davant "⏳ En progreso"), unes branques if explícites solen seguir sent l'opció més clara—. Però serveix com a exemple perfecte per veure classMap en acció, i el patró es torna clarament superior en el moment en què l'element en si no canvia, només la seua combinació de classes:
// Versió amb classMap
renderInsigniaEstado() {
const clases = {
insignia: true,
'insignia--hecha': this.estado === 'hecha',
'insignia--progreso': this.estado === 'progreso',
'insignia--pendiente': this.estado === 'pendiente',
};
const texto = {
hecha: '✓ Hecha',
progreso: '⏳ En progreso',
pendiente: '○ Pendiente',
}[this.estado];
return html`<span class="${classMap(clases)}">${texto}</span>`;
}Ací classMap substitueix les tres branques que decidien la classe, i un objecte literal a part (texto) substitueix les que decidien el contingut textual. El resultat té una línia més de codi que la versió original, així que no és automàticament "millor" en aquest cas concret; l'interessant és que totes dues responsabilitats —quina classe aplicar i quin text mostrar— queden separades i declaratives, en lloc de mesclades dins de tres blocs if/return que repeteixen la mateixa condició dues vegades (una per a la classe, una altra per al text). Quan al mòdul 10 s'afegisquen més estats possibles a TaskFlow, estendre aquesta versió implicarà afegir una entrada a cadascun dels dos objectes, sense tocar cap branca condicional existent.
- Combinar
classMap amb classes estàtiques: el que sí i el que no
classMap amb classes estàtiques: el que sí i el que noUn dubte habitual en començar a usar classMap és si es pot combinar amb classes que no depenen de cap condició. La resposta és sí, sempre que s'escriguen com a text normal junt amb l'expressió:
Això és perfectament vàlid: insignia és text estàtic de l'atribut, i classMap(...) aporta la resta de classes condicionals. El que no és vàlid és combinar classMap amb una segona expressió dinàmica independent dins del mateix atribut class:
// Incorrecte: dues expressions dinàmiques al mateix atribut class
html`<span class="${this.claseExtra} ${classMap({...})}"></span>`;classMap (igual que styleMap, que es veu al següent apartat) ha de ser l'única expressió dinàmica de l'atribut, encara que pot convivir amb text literal fix al voltant. Aquesta restricció no és arbitrària: classMap necessita comparar-se a si mateixa amb l'execució anterior de la mateixa directiva per saber quines classes afegir i quines llevar, cosa que només pot fer amb garanties si és l'única peça dinàmica de la qual depèn aquell atribut.
styleMap: estils inline dinàmics
styleMap: estils inline dinàmicsstyleMap segueix exactament el mateix patró que classMap, però per a l'atribut style: rep un objecte les claus del qual són propietats CSS (en camelCase, com en JavaScript, o com a cadena entre cometes per a variables CSS personalitzades) i els valors les quantitats o cadenes CSS a aplicar.
import { styleMap } from 'lit/directives/style-map.js';
const estilos = {
opacity: this.expandida ? '1' : '0.85',
borderLeftWidth: this.urgente ? '4px' : '2px',
'--color-avatar-tamano': this.compacta ? '32px' : '48px',
};
html`<article style="${styleMap(estilos)}">...</article>`;styleMap resol el mateix problema que classMap, traslladat a estils en línia: en lloc de construir a mà una cadena com `opacity: ${...}; border-left-width: ${...}`, amb el risc d'oblidar un punt i coma o un guionet, es declara un objecte pla on cada propietat CSS es correspon amb una clau. Igual que amb classMap, styleMap ha de ser l'única expressió dinàmica dins de l'atribut style en el qual s'use, encara que pot combinar-se amb estils estàtics escrits com a text al voltant.
Convé aclarir quan styleMap aporta valor davant el que ja es coneix des del mòdul 4: les variables CSS personalitzades continuen sent l'eina principal per al theming (colors, mides configurables des de fora del component, com --color-avatar-tamano en l'exemple). styleMap no substitueix les variables CSS; les complementa en el cas concret en què un valor d'estil necessita calcular-se dinàmicament des de JavaScript, en cada renderitzat, en lloc de configurar-se una vegada des de fora del component.
ifDefined: ometre un atribut quan el seu valor és undefined
ifDefined: ometre un atribut quan el seu valor és undefinedEl tercer problema que resolen les directives d'aquest catàleg és distint dels dos anteriors. Quan s'interpola una expressió directament en un atribut normal (no class ni style, sinó qualsevol altre, com title, alt o un atribut personalitzat), Lit converteix el valor a cadena de la manera habitual de JavaScript. El problema apareix quan aquest valor és exactament undefined:
Si this.descripcion val undefined (per exemple, perquè encara no ha arribat cap valor des de fora), el resultat en el DOM no és l'absència de l'atribut alt, sinó un atribut alt="undefined" literal, amb aquesta paraula visible per a qualsevol lector de pantalla o eina que l'inspeccione. Això rarament és el que es vol: normalment, si no hi ha un valor real a oferir, el desitjable és que l'atribut ni tan sols existisca en el DOM.
ifDefined resol exactament aquest cas:
import { ifDefined } from 'lit/directives/if-defined.js';
html`<img alt="${ifDefined(this.descripcion)}" />`;Amb ifDefined, si this.descripcion és undefined, Lit elimina per complet l'atribut alt de l'element (o no l'afegeix, si mai hi va ser present); si té qualsevol altre valor —inclosa una cadena buida '', o fins i tot null—, l'atribut s'estableix amb normalitat usant aquest valor. És important fixar-se en aquest matís: ifDefined reacciona únicament a undefined, no a qualsevol valor "fals" en el sentit de JavaScript (0, '' o null continuen establint l'atribut amb normalitat); si es necessitara ometre l'atribut també per a una cadena buida, caldria comprovar-ho explícitament abans de cridar ifDefined, per exemple amb ifDefined(this.descripcion || undefined).
- Aplicant
ifDefined a <user-avatar>
ifDefined a <user-avatar><user-avatar>, construït a la lliçó 04-04, rep des de <task-card> una propietat asignadoImagen opcional: quan la tasca té una imatge de la persona assignada, <task-card> distribueix un <img> dins de <user-avatar>; quan no, deixa el slot buit perquè apareguen les inicials de reserva. Aquella solució usava una branca if completa en JavaScript, a renderAvatar(), per decidir si construir o no l'etiqueta <img> sencera:
// Versió de la lliçó 04-04
renderAvatar() {
if (this.asignadoImagen) {
return html`
<user-avatar nombre="${this.asignadoA}">
<img src="${this.asignadoImagen}" alt="${this.asignadoA}" />
</user-avatar>
`;
}
return html`<user-avatar nombre="${this.asignadoA}"></user-avatar>`;
}Aquesta solució continua sent perfectament vàlida, i de fet és preferible quan la diferència entre els dos casos no és només un atribut, sinó la presència o absència d'un element complet (ací, la mateixa etiqueta <img>). Però convé conèixer l'alternativa amb ifDefined, útil en un cas lleugerament distint: quan el que es necessita ometre no és un element sencer, sinó un únic atribut dins d'un element que sí es vol mantenir sempre present. Suposem, per exemple, que <user-avatar> volgués acceptar directament una propietat imagenUrl opcional i decidir ella mateixa, internament, si mostrar una imatge o les inicials, en lloc que siga <task-card> qui decidisca distribuint o no un <img>:
// src/components/user-avatar.js (variant amb imagenUrl interna)
import { ifDefined } from 'lit/directives/if-defined.js';
render() {
return html`
<div class="avatar" title="${this.nombre}">
<img
src="${ifDefined(this.imagenUrl)}"
alt="${this.nombre}"
class="${classMap({ oculta: !this.imagenUrl })}"
/>
${!this.imagenUrl ? html`<span>${this.iniciales()}</span>` : ''}
</div>
`;
}Si this.imagenUrl és undefined (el seu valor per defecte, en lloc d'una cadena buida), ifDefined evita que l'<img> acabe amb un src="undefined" literal, que el navegador interpretaria com una petició de xarxa real a una URL invàlida, generant un error de càrrega visible a les eines de desenvolupador sense cap motiu real. Notem que ací es combinen, en un mateix fragment, dues de les tres directives d'aquesta lliçó: ifDefined per a l'atribut src, i classMap per amagar visualment l'<img> buit mentre es mostren les inicials de reserva en el seu lloc.
- Quan NO fa falta una directiva incorporada
Cap de les tres directives d'aquesta lliçó substitueix per complet les tècniques ja conegudes del curs; cadascuna resol un problema concret, i usar-les fora d'aquell problema afig una importació i una capa d'indirecció sense guanyar res a canvi.
| Situació | Tècnica recomanada |
|---|---|
| Mostrar o no un element complet segons una condició | Ternari o && (mòdul 2), no classMap |
| Alternar dues o més classes CSS en un mateix element que sempre és present | classMap |
| Un únic valor d'estil calculat dinàmicament en JavaScript | styleMap |
Un atribut que a vegades no hauria d'existir en absolut, amb un valor undefined |
ifDefined |
Un atribut booleà natiu (disabled, checked, hidden) |
El prefix ? de Lit (?disabled="${...}"), no ifDefined |
L'última fila mereix una aclariment: Lit ofereix, des de la seua sintaxi principal (no com a directiva del catàleg de lit/directives/), un prefix ? per a atributs booleans natius, que afig o lleva l'atribut segons que el valor siga veritable o fals, sense necessitat de ifDefined ni de cap altra directiva. ifDefined està pensat per a atributs amb un valor real (una cadena, com alt o src) que a vegades no està disponible, no per a atributs purament booleans.
Errors Comuns i Consells
- Combinar
classMapostyleMapamb una segona expressió dinàmica en el mateix atribut: com s'ha explicat a l'apartat 4, cadascuna ha de ser l'única expressió de l'atributclassostyleen el qual s'use; si es necessita lògica addicional, cal incorporar-la dins del mateix objecte que se li passa a la directiva, no com una expressió germana en el mateix atribut. - Oblidar que
ifDefinednomés reacciona aundefined: com s'ha vist a l'apartat 6, ninullni''ni0provoquen l'eliminació de l'atribut; si el valor per defecte d'una propietat és''en lloc deundefined(com passava ambasignadoImagena la lliçó 04-04),ifDefinedno tindrà cap efecte sense canviar primer aquest valor per defecte. - Usar
styleMapper a valors que haurien de ser variables CSS configurables des de fora: si un valor d'estil no depèn d'un càlcul fet en JavaScript en cada renderitzat, sinó que és simplement un valor que qui use el component hauria de poder personalitzar, una variable CSS (mòdul 4) continua sent l'eina correcta;styleMapno és un substitut general destatic stylesni de les variables CSS. - Reescriure codi que ja funciona bé només per usar una directiva: com s'ha comentat a l'apartat 3,
renderInsigniaEstado()amb tres branquesifcontinuava sent perfectament legítima;classMapaporta més valor com més classes condicionals independents calga combinar sobre un mateix element, no en casos amb una única branca de tres alternatives mútuament excloents.
Exercicis
- Afig a
<task-card>una classetarjeta--compactaque s'active mitjançant una nova propietat booleanacompacta, combinant-la ambclassMapjunt amb la classe basetarjetai ambtarjeta--urgente(ja existent per l'avís d'urgència). Escriu l'objecte complet que li passaries aclassMap. - Reprén l'exemple de
styleMapde l'apartat 5 i modifica'l perquèborderLeftWidthvalga'4px'únicament quanthis._contadorTiempo.cercaDeVencersigatrue(el controlador reactiu de la lliçó 06-03), en lloc dethis.urgente. Explica amb les teues pròpies paraules per què això continua sent una única expressió dinàmica vàlida dins de l'atributstyle. - Un company d'equip escriu
<user-avatar imagen-url="${this.asignadoImagen}">, ambasignadoImagenper defecte en''(cadena buida, com a la lliçó 04-04), i se sorprén queifDefined"no servisca per a res" en intentar usar-lo sobre aquesta propietat. Explica, basant-te en l'apartat 6, per què passa això i quin canvi faria falta perquèifDefinedcomencés a tenir efecte.
Solucions
const clases = {
tarjeta: true,
'tarjeta--urgente': this.urgente,
'tarjeta--compacta': this.compacta,
};
html`<article class="${classMap(clases)}">...</article>`;const estilos = {
borderLeftWidth: this._contadorTiempo.cercaDeVencer ? '4px' : '2px',
};
html`<article style="${styleMap(estilos)}">...</article>`;Continua sent una única expressió dinàmica vàlida perquè, encara que la condició interna canvie de font (de this.urgente a this._contadorTiempo.cercaDeVencer), l'atribut style complet continua rebent exactament un únic valor: el resultat de la crida a styleMap(estilos). Lit no distingeix d'on procedeix la condició usada dins de l'objecte; només li importa que la mateixa crida a la directiva siga l'única peça dinàmica de l'atribut.
ifDefinednomés omet l'atribut quan el valor que rep és exactamentundefined; una cadena buida''és un valor perfectament definit des del punt de vista de JavaScript, així queifDefined('')deixa passar la cadena buida tal com és, i l'atributimagen-url=""s'estableix amb normalitat (buit, però present). PerquèifDefinedtinga l'efecte desitjat,asignadoImagenhauria de valerundefinedquan no hi ha imatge disponible, en lloc de''; una manera ràpida d'aconseguir-ho sense canviar el valor per defecte de la propietat seria escriureifDefined(this.asignadoImagen || undefined), convertint explícitament la cadena buida enundefinedjust abans de passar-la a la directiva.
Conclusió
Aquesta lliçó ha tancat tres mencions pendents des dels mòduls 2 i 4: classMap i styleMap com a alternatives més còmodes que construir cadenes de classes o estils a mà, i ifDefined com a manera d'evitar atributs amb el valor literal "undefined" quan una dada opcional encara no està disponible. Les tres comparteixen la mateixa naturalesa de fons, presentada a l'apartat 1: són directives, un tipus especial de valor que el motor de plantilles de Lit reconeix i tracta de forma diferenciada, més enllà de les cadenes, números i plantilles niades ja conegudes.
Aquestes tres directives incorporades resolen problemes puntuals i concrets dins d'una plantilla, però totes comparteixen una limitació: no donen accés directe al node del DOM que Lit gestiona en aquella posició, ni permeten mantenir un estat propi entre renderitzats successius més enllà de l'objecte que se'ls passa com a argument. La lliçó següent presenta l'eina que sí ofereix aquest nivell de control: les directives personalitzades, amb les quals es podrà escriure una lògica de renderitzat reutilitzable, amb accés real a la part del DOM afectada, per a casos que classMap o styleMap no poden cobrir per si sols.
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
