classMap, styleMap i ifDefined, vistes a la lliçó anterior, són directives escrites pel mateix equip de Lit per resoldre problemes d'ús molt general. Però el mecanisme que les fa possibles no és exclusiu de l'equip de Lit: està disponible públicament, i qualsevol projecte pot escriure les seues pròpies directives quan la lògica de renderitzat que necessita repetir en diverses plantilles no encaixa bé en una simple funció auxiliar. Aquesta lliçó explica quan té sentit fer aquest pas, com es construeix una directiva pròpia amb la funció directive() i la classe base Directive, i l'aplica a un cas real de TaskFlow: una directiva resaltarSiUrgente que cap de les eines ja vistes al curs pot resoldre igual de bé.

Contingut

  1. Què pot fer una directiva que una funció auxiliar no pot
  2. La funció directive() i la classe base Directive
  3. El cicle render() / update()
  4. Part: accés a la part real del DOM
  5. Construint resaltarSiUrgente
  6. Usant la directiva a <task-card>
  7. Neteja amb disconnected()
  8. noChange: evitar treball innecessari

  1. Què pot fer una directiva que una funció auxiliar no pot

Aquest curs ha usat, des del mòdul 2, funcions auxiliars com renderInsigniaEstado() per encapsular lògica de renderitzat reutilitzable dins d'un mateix component. Una funció auxiliar corrent és suficient sempre que el seu treball es limite a decidir quina plantilla retornar, a partir de les dades que rep com a argument. És una solució excel·lent, i de fet continua sent la primera opció per defecte per a la immensa majoria de la lògica de renderitzat de TaskFlow.

Hi ha, però, un tipus de necessitat que una funció auxiliar no pot cobrir per si sola: quan la lògica necessita mantenir estat propi entre un renderitzat i el següent, o necessita accés directe al node real del DOM que ocupa aquella posició de la plantilla, més enllà del valor que es mostra en pantalla. Una funció auxiliar s'executa de zero en cada crida, sense memòria de la crida anterior, i no rep cap referència al DOM; simplement retorna un valor i acaba. Quan la lògica necessita recordar alguna cosa entre renderitzats (per exemple, "aquesta tasca ja estava urgent en el renderitzat anterior, o just ara acaba d'entrar en aquest estat?") o necessita tocar el DOM directament (per exemple, afegir una classe temporal i programar la seua eliminació amb un setTimeout), fa falta una peça amb cicle de vida propi: una directiva personalitzada.

  1. La funció directive() i la classe base Directive

Una directiva personalitzada es construeix combinant dues peces del mateix Lit, ambdues importades de lit/directive.js:

import { directive, Directive } from 'lit/directive.js';

class MiDirectiva extends Directive {
  render(...args) {
    // Lògica que decideix què es mostra, similar a una funció auxiliar normal
    return 'algo';
  }
}

export const miDirectiva = directive(MiDirectiva);

Directive és la classe base de la qual ha d'heretar qualsevol directiva personalitzada: defineix el contracte mínim (el mètode render) i la resta de la maquinària interna que connecta la directiva amb el motor de plantilles de Lit. directive() és una funció de fàbrica que rep aquesta classe i retorna la funció que realment s'usa dins de les plantilles (miDirectiva(...), en l'exemple); cada crida a aquesta funció retorna un objecte directiva nou, llest per inserir-se en una posició d'una plantilla html, de la mateixa manera en què s'ha usat classMap(...) o until(...) en la resta d'aquest mòdul.

Un detall important, que distingeix les directives de les funcions auxiliars corrents: Lit crea i reutilitza una única instància de la classe Directive per cada posició fixa de la plantilla en la qual s'usa, no una instància nova per cada renderitzat. Si resaltarSiUrgente(...) s'usa dins de render() de <task-card>, cada instància de <task-card> té la seua pròpia instància de ResaltarSiUrgenteDirective, i aquesta mateixa instància persisteix, amb el seu propi estat intern, mentre aquella instància de <task-card> continue existint i renderitzant-se en aquella posició concreta de la seua plantilla. Aquesta persistència és exactament el que permet recordar alguna cosa entre un renderitzat i el següent, cosa impossible amb una funció auxiliar corrent.

  1. El cicle render() / update()

Una directiva pot implementar dos mètodes, tots dos opcionals excepte render, que Lit invoca en cada renderitzat de la posició on la directiva està col·locada:

Mètode Quan s'executa Per a què serveix
render(...args) Sempre, excepte que update() decidisca ometre'l Calcular quin valor s'ha de mostrar, igual que faria una funció auxiliar
update(part, args) Abans de render(), amb accés directe a la part del DOM Llegir o modificar el DOM directament, decidir si cal tornar a executar render()

Si una directiva no necessita tocar el DOM directament ni comparar-lo amb el renderitzat anterior, n'hi ha prou amb implementar render(), i ignorar update() per complet (Lit cridarà update() amb la seua implementació per defecte, que simplement delega en render()). update() només fa falta quan la lògica necessita el segon element de la taula: accés a la part real del DOM abans de decidir què mostrar, tal com es veurà a l'apartat 5 amb resaltarSiUrgente.

  1. Part: accés a la part real del DOM

Quan s'implementa update(part, args), el primer paràmetre (part) és un objecte que representa la posició concreta de la plantilla on la directiva està col·locada, amb una forma distinta segons en quin tipus de posició s'use la directiva:

  • Un ChildPart, si la directiva ocupa la posició d'un node fill (html\

    ${miDirectiva()}

    ``).
  • Un AttributePart, si ocupa el valor d'un atribut (html\
    ``).
  • Un ElementPart, si la directiva es col·loca directament sobre l'etiqueta de l'element, sense associar-la a cap atribut concret (html\<div ${miDirectiva()}>
``).

Els tres tipus de Part exposen una propietat element, que dona accés directe al node del DOM afectat: el mateix element en el cas d'un ElementPart, o l'element al qual pertany l'atribut o el node fill en els altres dos casos. Aquesta referència directa al DOM és exactament el que cap funció auxiliar pot oferir per si sola, i és la peça que fa possible l'exemple de la secció següent.

  1. Construint resaltarSiUrgente

TaskFlow necessita un efecte visual concret que ni classMap ni cap tècnica ja vista al curs resol bé: quan una tasca entra en estat "a punt de vèncer" (el cercaDeVencer calculat per ContadorTiempoRestanteController, de la lliçó 06-03), la seua targeta hauria de resaltar-se breument amb una animació d'un segon i mig, per cridar l'atenció, i després tornar al seu aspecte normal sense necessitat que l'usuari faça res. classMap podria aplicar una classe mentre cercaDeVencer siga true, però això mantindria la classe activa tot el temps que dure aquell estat, no només en l'instant de la transició; el que cal és detectar el canvi de false a true, i reaccionar amb un efecte temporal que es retire sol, amb un setTimeout.

// src/directives/resaltar-si-urgente.js
import { directive, Directive } from 'lit/directive.js';
import { nothing } from 'lit';

class ResaltarSiUrgenteDirective extends Directive {
  constructor(partInfo) {
    super(partInfo);
    this._yaResaltada = false;
    this._idTimeout = null;
  }

  update(part, [urgente]) {
    const elemento = part.element;

    if (urgente && !this._yaResaltada) {
      elemento.classList.add('resaltada');
      this._idTimeout = setTimeout(() => {
        elemento.classList.remove('resaltada');
      }, 1500);
      this._yaResaltada = true;
    }

    if (!urgente) {
      this._yaResaltada = false;
    }

    return this.render(urgente);
  }

  render(urgente) {
    return nothing;
  }

  disconnected() {
    clearTimeout(this._idTimeout);
  }
}

export const resaltarSiUrgente = directive(ResaltarSiUrgenteDirective);

Diversos detalls mereixen explicació un per un:

  • El constructor(partInfo) inicialitza dos camps propis de cada instància de la directiva: _yaResaltada, que recorda si la targeta ja està enmig de l'efecte de resaltat (per no reiniciar-lo en cada renderitzat mentre urgente continue sent true), i _idTimeout, l'identificador retornat per setTimeout, necessari per poder cancel·lar-lo si calguera. partInfo es rep i es reenvia a super() sense usar-lo directament en aquest exemple; conté metadades sobre la posició de la plantilla en la qual la directiva s'ha instanciat per primera vegada.
  • update(part, [urgente]) rep com a segon argument un array amb els arguments exactes amb els quals es va cridar resaltarSiUrgente(...) en la plantilla; com que ací només se li passa un argument (urgente), es desestructura directament en la signatura del mètode.
  • part.element dona accés a l'element real del DOM sobre el qual està col·locada la directiva (com es veurà a l'apartat 6, el mateix <article> de <task-card>), i sobre ell es criden classList.add/classList.remove amb normalitat, exactament igual que es faria amb JavaScript del DOM sense cap framework pel mig.
  • La condició urgente && !this._yaResaltada és la que detecta la transició de "no urgent" a "urgent", no simplement l'estat actual: només entra en aquesta branca la primera vegada que urgente és veritable després d'haver sigut fals (o després de la inicialització), evitant reiniciar el setTimeout en cada renderitzat posterior mentre la tasca continue sent urgent.
  • render() es limita a retornar nothing (importat de lit, ja usat a la lliçó 02-03 per al cas "no renderitzar res" amb l'operador &&), perquè aquesta directiva no necessita mostrar cap valor en la posició de la plantilla; el seu efecte és purament imperatiu, sobre el DOM, a través d'update().

  1. Usant la directiva a <task-card>

Amb la directiva ja escrita, s'aplica sobre <task-card> com un ElementPart, col·locada directament sobre l'etiqueta <article> sense associar-la a cap atribut concret:

// src/components/task-card.js
import { resaltarSiUrgente } from '../directives/resaltar-si-urgente.js';

render() {
  return html`
    <article ${resaltarSiUrgente(this._contadorTiempo.cercaDeVencer)} @click="${this.alternarExpandida}">
      <h3>${this.titulo}</h3>
      ${this.renderInsigniaEstado()}
      ${this._contadorTiempo.cercaDeVencer ? html`<p class="aviso">⏰ Está a punto de vencer</p>` : ''}
    </article>
  `;
}

La sintaxi ${resaltarSiUrgente(...)} col·locada directament entre el nom de l'etiqueta i els seus atributs, sense anar precedida de cap nom d'atribut ni de =, és justament el que converteix aquesta directiva en un ElementPart: no està associada a class, ni a style, ni a cap altre atribut concret, sinó al mateix element <article> en la seua totalitat. En cada renderitzat d'aquesta instància de <task-card>, Lit crida update() sobre la mateixa instància de ResaltarSiUrgenteDirective (recordada des del primer renderitzat, com s'ha explicat a l'apartat 2), passant-li el valor actual de this._contadorTiempo.cercaDeVencer; la directiva compara aquest valor amb el seu propi record de la transició anterior i decideix, ella mateixa, si cal afegir la classe resaltada i programar la seua retirada.

  1. Neteja amb disconnected()

A més de render() i update(), la classe Directive ofereix dos mètodes addicionals, disconnected() i reconnected(), que Lit invoca quan la part del DOM associada a la directiva es desconnecta o es reconnecta del document (per exemple, si <task-card> s'elimina del DOM, o si forma part d'una llista gestionada amb repeat que decideix reutilitzar o descartar nodes, com s'ha vist a la lliçó 02-04). resaltarSiUrgente implementa disconnected() per cancel·lar qualsevol setTimeout pendent amb clearTimeout(this._idTimeout), exactament pel mateix motiu, ja conegut des de disconnectedCallback a la lliçó 06-01 i des de hostDisconnected a la lliçó 06-03, pel qual qualsevol temporitzador actiu s'ha de netejar quan la peça que el va programar deixa d'estar en ús: sense aquesta neteja, si una targeta s'elimina del DOM just quan _idTimeout està pendent, el setTimeout continuaria executant-se igualment 1500 ms més tard, intentant modificar classList d'un element que ja no forma part de la pàgina, un malbaratament de treball que, a més, podria, en casos més complexos amb referències creuades, contribuir a una fuga de memòria.

  1. noChange: evitar treball innecessari

Lit ofereix, també importable de lit, un valor especial anomenat noChange, que una directiva pot retornar des d'update() (o des de render()) per indicar-li a Lit "no ha canviat res en aquesta posició, no cal tocar el DOM en absolut". És distint de retornar nothing (usat a l'apartat 5 per a "no mostrar cap valor real", però que sí actualitza el DOM per reflectir aquesta absència si abans hi havia alguna cosa); noChange significa, literalment, "deixa el DOM exactament com està, sense cap comparació ni actualització".

import { noChange } from 'lit';

update(part, [urgente]) {
  if (urgente === this._ultimoValorVisto) {
    return noChange;
  }
  this._ultimoValorVisto = urgente;
  // ... resta de la lògica ...
  return this.render(urgente);
}

En l'exemple de resaltarSiUrgente, aquesta optimització no aporta res rellevant perquè render() ja retorna sempre nothing (una operació trivial), així que s'ha omès per no complicar l'exemple principal; però convé conèixer noChange per a directives amb un render() que sí produïsca un valor costós de calcular o d'aplicar al DOM, on evitar un treball repetit quan el valor d'entrada no ha canviat pot suposar una diferència real de rendiment.

Errors Comuns i Consells

  • Exportar la classe directament en lloc del resultat de directive(): export const resaltarSiUrgente = directive(ResaltarSiUrgenteDirective) és imprescindible; exportar ResaltarSiUrgenteDirective a soles i usar-la com new ResaltarSiUrgenteDirective() dins d'una plantilla no funcionaria, perquè el motor de plantilles de Lit reconeix directives per la forma especial que directive() els dona, no per herència de Directive en si mateixa.
  • Oblidar disconnected() quan la directiva programa temporitzadors o subscripcions: exactament el mateix risc assenyalat a l'apartat 7 i ja vist dues vegades al mòdul 6; qualsevol recurs que la directiva arranque en update() (un interval, una promesa pendent amb una acció associada, un listener afegit manualment sobre part.element) s'hauria de netejar en disconnected().
  • Intentar modificar el DOM directament des de render() en lloc d'update(): render() està pensat per retornar un valor que Lit insereix pel seu compte en la posició corresponent, no per fer manipulació imperativa del DOM; l'accés a part.element només està disponible dins d'update() (o de disconnected()/reconnected()), mai com a argument de render().
  • Crear una directiva personalitzada per a un cas que classMap, styleMap o una funció auxiliar ja resolen: com s'ha explicat a l'apartat 1, el criteri de decisió és concret: només fa falta una directiva pròpia quan la lògica necessita recordar estat entre renderitzats o tocar el DOM directament; si n'hi ha prou amb decidir quina plantilla o quin objecte retornar a partir de les dades actuals, una funció auxiliar o una directiva incorporada són sempre l'opció més simple.

Exercicis

  1. Afig un paràmetre de configuració a resaltarSiUrgente, de manera que s'use com resaltarSiUrgente(this._contadorTiempo.cercaDeVencer, { duracionMs: 3000, clase: 'resaltada-larga' }), substituint els valors fixos 1500 i 'resaltada' de l'apartat 5 pels valors rebuts (amb duracionMs: 1500 i clase: 'resaltada' com a valors per defecte si no s'especifiquen).
  2. Explica, basant-te en l'apartat 2, què passaria si resaltarSiUrgente s'usara dins d'una llista renderitzada amb repeat (lliçó 02-04) i una tasca concreta canviara de posició en l'array: es conserva la instància de ResaltarSiUrgenteDirective associada a aquella tasca, o se'n crea una nova? Recolza't en el fet que repeat, gràcies a la seua clau (key), reutilitza el mateix node DOM per al mateix element lògic encara que canvie de posició.
  3. Un company d'equip proposa substituir resaltarSiUrgente per una funció auxiliar corrent, resaltarSiUrgente(urgente), que retorne la classe 'resaltada' o '' segons el valor d'urgente, combinada amb classMap. Explica per què aquesta alternativa no reprodueix el comportament real de la directiva (el resaltat temporal de 1500 ms que es retira sol), encara que a primera vista semble una simplificació raonable.

Solucions

class ResaltarSiUrgenteDirective extends Directive {
  constructor(partInfo) {
    super(partInfo);
    this._yaResaltada = false;
    this._idTimeout = null;
  }

  update(part, [urgente, opciones = {}]) {
    const { duracionMs = 1500, clase = 'resaltada' } = opciones;
    const elemento = part.element;

    if (urgente && !this._yaResaltada) {
      elemento.classList.add(clase);
      this._idTimeout = setTimeout(() => elemento.classList.remove(clase), duracionMs);
      this._yaResaltada = true;
    }

    if (!urgente) {
      this._yaResaltada = false;
    }

    return this.render(urgente);
  }

  render() {
    return nothing;
  }

  disconnected() {
    clearTimeout(this._idTimeout);
  }
}

export const resaltarSiUrgente = directive(ResaltarSiUrgenteDirective);
  1. Com s'ha explicat a l'apartat 2, Lit associa una instància de la directiva a cada posició fixa de la plantilla, i repeat (a diferència d'Array.map) identifica cada element per la seua clau lògica (tarea.id), no per la seua posició en l'array; en conseqüència, si una tasca canvia de posició dins de la llista, repeat mou el mateix node DOM existent a la nova posició en lloc de destruir-lo i recrear-ne un de nou, i aquesta continuïtat del node DOM es trasllada també a la directiva col·locada sobre ell: la mateixa instància de ResaltarSiUrgenteDirective, amb el seu _yaResaltada i el seu _idTimeout intactes, continua associada a aquella mateixa tasca encara que canvie de posició visual en la llista. Si en canvi s'usara Array.map per a aquella mateixa llista, un reordenament podria fer que Lit reutilitzara el node de la posició N per representar ara una tasca lògica distinta, i amb ell la instància de la directiva, que podria arrossegar per error l'estat (_yaResaltada) de la tasca que abans ocupava aquella posició.
  2. La classe 'resaltada' combinada amb classMap, sense més, aplicaria la classe durant tot el temps que urgente siga true, no només durant el primer segon i mig després de la transició: en el moment en que urgente torne a false en un renderitzat posterior (per exemple, perquè la data límit s'actualitza), la classe desapareixeria, però mentre urgente continue sent true de forma continuada, la classe continuaria activa indefinidament, sense l'efecte de "destellament temporal que es retira sol" que sí aconsegueix setTimeout dins de la directiva. Reproduir el comportament real exigiria, com a mínim, que la funció auxiliar recordara si ja havia mostrat el resaltat abans (exactament l'estat _yaResaltada de la directiva) i programara el seu propi setTimeout per retirar-la, cosa que ja no és possible amb una funció auxiliar corrent, sense estat propi entre crides, tal com s'ha explicat a l'apartat 1.

Conclusió

Aquesta lliçó ha presentat les directives personalitzades com l'eina de Lit per a lògica de renderitzat que necessita alguna cosa més que decidir quina plantilla retornar: estat propi persistent entre renderitzats, i accés directe al node del DOM afectat a través de part.element. resaltarSiUrgente, construïda amb directive() i la classe base Directive, ha resolt un efecte visual —el destellament temporal en entrar en estat urgent— que cap de les eines ja vistes al curs, incloses les directives incorporades de la lliçó anterior, podia cobrir igual de bé, reutilitzable a més en qualsevol altra plantilla de TaskFlow que necessite el mateix avís.

Amb les directives ja dominades tant en la seua forma incorporada com personalitzada, queda un últim problema de renderitzat per resoldre en aquest mòdul, distint de tot el vist fins ara: què fer quan el valor que cal mostrar encara no existeix, perquè depèn d'una operació asíncrona en marxa, com carregar les tasques de TaskFlow des d'una font de dades externa. La lliçó següent presenta la directiva until per a exactament aquest cas.

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