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

  1. Què és una directiva a Lit
  2. classMap: alternar classes des d'un objecte
  3. Reescrivint la insígnia d'estat de <task-card> amb classMap
  4. Combinar classMap amb classes estàtiques: el que sí i el que no
  5. styleMap: estils inline dinàmics
  6. ifDefined: ometre un atribut quan el seu valor és undefined
  7. Aplicant ifDefined a <user-avatar>
  8. Quan NO fa falta una directiva incorporada

  1. 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).

  1. classMap: alternar classes des d'un objecte

classMap 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ó".

  1. Reescrivint la insígnia d'estat de <task-card> amb classMap

La 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.

  1. Combinar classMap amb classes estàtiques: el que sí i el que no

Un 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ó:

html`<span class="insignia ${classMap({ 'insignia--hecha': this.estado === 'hecha' })}"></span>`;

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.

  1. styleMap: estils inline dinàmics

styleMap 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.

  1. ifDefined: ometre un atribut quan el seu valor és undefined

El 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:

html`<img alt="${this.descripcion}" />`;

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).

  1. Aplicant 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.

  1. 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 classMap o styleMap amb 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'atribut class o style en 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 ifDefined només reacciona a undefined: com s'ha vist a l'apartat 6, ni null ni '' ni 0 provoquen l'eliminació de l'atribut; si el valor per defecte d'una propietat és '' en lloc de undefined (com passava amb asignadoImagen a la lliçó 04-04), ifDefined no tindrà cap efecte sense canviar primer aquest valor per defecte.
  • Usar styleMap per 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; styleMap no és un substitut general de static styles ni 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 branques if continuava sent perfectament legítima; classMap aporta 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

  1. Afig a <task-card> una classe tarjeta--compacta que s'active mitjançant una nova propietat booleana compacta, combinant-la amb classMap junt amb la classe base tarjeta i amb tarjeta--urgente (ja existent per l'avís d'urgència). Escriu l'objecte complet que li passaries a classMap.
  2. Reprén l'exemple de styleMap de l'apartat 5 i modifica'l perquè borderLeftWidth valga '4px' únicament quan this._contadorTiempo.cercaDeVencer siga true (el controlador reactiu de la lliçó 06-03), en lloc de this.urgente. Explica amb les teues pròpies paraules per què això continua sent una única expressió dinàmica vàlida dins de l'atribut style.
  3. Un company d'equip escriu <user-avatar imagen-url="${this.asignadoImagen}">, amb asignadoImagen per defecte en '' (cadena buida, com a la lliçó 04-04), i se sorprén que ifDefined "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è ifDefined comencé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.

  1. ifDefined només omet l'atribut quan el valor que rep és exactament undefined; una cadena buida '' és un valor perfectament definit des del punt de vista de JavaScript, així que ifDefined('') deixa passar la cadena buida tal com és, i l'atribut imagen-url="" s'estableix amb normalitat (buit, però present). Perquè ifDefined tinga l'efecte desitjat, asignadoImagen hauria de valer undefined quan 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 escriure ifDefined(this.asignadoImagen || undefined), convertint explícitament la cadena buida en undefined just 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

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