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
- Què pot fer una directiva que una funció auxiliar no pot
- La funció
directive()i la classe baseDirective - El cicle
render()/update() Part: accés a la part real del DOM- Construint
resaltarSiUrgente - Usant la directiva a
<task-card> - Neteja amb
disconnected() noChange: evitar treball innecessari
- 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.
- La funció
directive() i la classe base Directive
directive() i la classe base DirectiveUna 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.
- El cicle
render() / update()
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.
Part: accés a la part real del DOM
Part: accés a la part real del DOMQuan 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()}> - Un
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.
- Construint
resaltarSiUrgente
resaltarSiUrgenteTaskFlow 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 mentreurgentecontinue senttrue), i_idTimeout, l'identificador retornat persetTimeout, necessari per poder cancel·lar-lo si calguera.partInfoes rep i es reenvia asuper()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 cridarresaltarSiUrgente(...)en la plantilla; com que ací només se li passa un argument (urgente), es desestructura directament en la signatura del mètode.part.elementdona 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 cridenclassList.add/classList.removeamb 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 queurgenteés veritable després d'haver sigut fals (o després de la inicialització), evitant reiniciar elsetTimeouten cada renderitzat posterior mentre la tasca continue sent urgent. render()es limita a retornarnothing(importat delit, 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().
- Usant la directiva a
<task-card>
<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.
- Neteja amb
disconnected()
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.
noChange: evitar treball innecessari
noChange: evitar treball innecessariLit 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; exportarResaltarSiUrgenteDirectivea soles i usar-la comnew ResaltarSiUrgenteDirective()dins d'una plantilla no funcionaria, perquè el motor de plantilles de Lit reconeix directives per la forma especial quedirective()els dona, no per herència deDirectiveen 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 enupdate()(un interval, una promesa pendent amb una acció associada, un listener afegit manualment sobrepart.element) s'hauria de netejar endisconnected(). - 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 apart.elementnomés està disponible dins d'update()(o dedisconnected()/reconnected()), mai com a argument derender(). - Crear una directiva personalitzada per a un cas que
classMap,styleMapo 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
- Afig un paràmetre de configuració a
resaltarSiUrgente, de manera que s'use comresaltarSiUrgente(this._contadorTiempo.cercaDeVencer, { duracionMs: 3000, clase: 'resaltada-larga' }), substituint els valors fixos1500i'resaltada'de l'apartat 5 pels valors rebuts (ambduracionMs: 1500iclase: 'resaltada'com a valors per defecte si no s'especifiquen). - Explica, basant-te en l'apartat 2, què passaria si
resaltarSiUrgentes'usara dins d'una llista renderitzada ambrepeat(lliçó 02-04) i una tasca concreta canviara de posició en l'array: es conserva la instància deResaltarSiUrgenteDirectiveassociada a aquella tasca, o se'n crea una nova? Recolza't en el fet querepeat, gràcies a la seua clau (key), reutilitza el mateix node DOM per al mateix element lògic encara que canvie de posició. - Un company d'equip proposa substituir
resaltarSiUrgenteper una funció auxiliar corrent,resaltarSiUrgente(urgente), que retorne la classe'resaltada'o''segons el valor d'urgente, combinada ambclassMap. 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);- 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,repeatmou 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 deResaltarSiUrgenteDirective, amb el seu_yaResaltadai el seu_idTimeoutintactes, continua associada a aquella mateixa tasca encara que canvie de posició visual en la llista. Si en canvi s'usaraArray.mapper 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ó. - La classe
'resaltada'combinada ambclassMap, sense més, aplicaria la classe durant tot el temps queurgentesigatrue, no només durant el primer segon i mig després de la transició: en el moment en queurgentetorne afalseen un renderitzat posterior (per exemple, perquè la data límit s'actualitza), la classe desapareixeria, però mentreurgentecontinue senttruede forma continuada, la classe continuaria activa indefinidament, sense l'efecte de "destellament temporal que es retira sol" que sí aconsegueixsetTimeoutdins 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_yaResaltadade la directiva) i programara el seu propisetTimeoutper 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
- 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
