El mòdul 3 ha deixat <task-card> i <task-list> completament resoltes pel que fa a dades: propietats reactives, estat intern, conversors personalitzats i una llista que passa a cada targeta filla les dades de la seva pròpia tasca. Però, com es va assenyalar explícitament en tancar l'última lliçó, tota aquesta feina s'ha fet sobre HTML sense cap estil visual: sense vores, sense tipografia acurada, sense cap color que distingeixi una targeta urgent d'una que no ho és. Aquesta lliçó comença a resoldre aquesta mancança explicant primer un tret fonamental dels Web Components —l'encapsulació d'estils que proporciona el Shadow DOM— i aplicant-lo de seguida per donar a <task-card> la seva primera fulla d'estils pròpia amb static styles i la funció css.
Contingut
- Què afegeix el Shadow DOM al CSS: dues fronteres, no una
static stylesi la funciócss: la manera d'escriure CSS a Lit- Per què no funciona un
<link>o un<style>normal dins del shadow root - Alternatives per a CSS realment extern:
adoptedStyleSheets - Els primers estils propis de
<task-card> - El selector especial
:host
- Què afegeix el Shadow DOM al CSS: dues fronteres, no una
Abans d'escriure una sola línia de CSS per a <task-card>, convé entendre amb precisió què fa el Shadow DOM amb els estils, perquè és un comportament diferent del de qualsevol element HTML normal i és la base de tot el que es construeix a partir d'aquesta lliçó.
Quan un element no usa Shadow DOM —un <div>, un <article> qualsevol d'una pàgina normal—, el CSS de la pàgina s'aplica sobre ell sense cap barrera: qualsevol regla de qualsevol fulla d'estils carregada al document pot afectar-lo, i qualsevol estil que aquest element defineixi (per exemple, mitjançant una classe) pot afectar altres elements si el selector és prou ampli. És exactament el motiu pel qual, en projectes grans sense Web Components, és habitual acabar amb convencions de nomenclatura elaborades (BEM i similars) només per evitar que el CSS d'una part de l'aplicació "s'escoli" en una altra.
El Shadow DOM canvia això d'arrel, aixecant dues fronteres simultànies:
- El CSS de fora no entra: les fulles d'estils globals del document, els
<link>externs, qualsevol reglap { color: red; }escrita al CSS general de la pàgina, no afecten els elements que viuen dins del shadow root de<task-card>. Per dins,<task-card>comença sempre des d'una fulla en blanc, tret de les propietats CSS heretables per naturalesa (comfont-familyocolor, que sí que travessen la frontera per herència normal de CSS, no per cap característica especial de Lit) i de les variables CSS personalitzades, que es tracten a la següent lliçó d'aquest mòdul. - El CSS de dins no surt: qualsevol regla que s'escrigui dins del shadow root de
<task-card>—per exemple,article { border: 1px solid; }— només afecta els elements<article>que viuen dins d'aquell shadow root concret. No es filtra cap al document principal ni cap a altres components, ni tan sols cap a altres instàncies de<task-card>amb el seu propi shadow root independent.
Aquesta doble frontera és la raó de fons per la qual els Web Components resolen, de forma nativa i sense cap convenció manual, el problema de col·lisió de noms de classes CSS que arrossega el desenvolupament web des de fa anys. Es pot escriure amb total llibertat una regla com .detalle { padding: 1rem; } dins de <task-card> sense preocupar-se en absolut de si existeix una altra classe .detalle a qualsevol altra part del lloc: viuen en mons d'estils completament separats.
static styles i la funció css: la manera d'escriure CSS a Lit
static styles i la funció css: la manera d'escriure CSS a LitLit ofereix un mecanisme dedicat per declarar el CSS d'un component: un camp estàtic anomenat styles, anàleg en esperit a static properties vist al mòdul anterior, el valor del qual es construeix amb una funció especial anomenada css, importada també de lit.
import { LitElement, html, css } from 'lit';
class TaskCard extends LitElement {
static styles = css`
article {
border: 1px solid #ccc;
padding: 1rem;
}
`;
render() {
return html`<article><h3>Ejemplo</h3></article>`;
}
}Fixa't en el paral·lelisme amb html, ja conegut des del mòdul 2: css, igual que html, és una funció que s'usa com a tagged template (una plantilla de JavaScript precedida directament pel nom de la funció, sense parèntesis) i retorna un valor especial que Lit sap interpretar; no és una simple cadena de text, encara que el contingut entre els backticks s'escrigui exactament igual que en un fitxer .css normal. Aquesta semblança de sintaxi entre html i css no és casualitat: totes dues formen part del mateix sistema de plantilles etiquetades de Lit, pensat perquè l'editor de codi pugui oferir ressaltat de sintaxi (amb les extensions adequades) tant per al marcatge com per als estils, directament dins del fitxer JavaScript.
static styles es declara, igual que static properties, una única vegada, fora de qualsevol mètode, com a camp estàtic de la classe. Lit el processa quan es defineix el component i, en temps d'execució, insereix aquest CSS dins del propi shadow root de cada instància, de manera que les dues fronteres descrites a l'apartat anterior s'apliquin automàticament sense cap treball addicional per la teva part.
- Per què no funciona un
<link> o un <style> normal dins del shadow root
<link> o un <style> normal dins del shadow rootÉs raonable preguntar-se, sobretot si es ve d'escriure HTML i CSS de forma tradicional, per què no n'hi ha prou amb incloure una etiqueta <style> (o un <link rel="stylesheet"> apuntant a un fitxer .css) directament dins de la plantilla render(), en lloc d'usar static styles.
// Funciona, però amb un problema de rendiment important
render() {
return html`
<style>
article { border: 1px solid #ccc; padding: 1rem; }
</style>
<article><h3>Ejemplo</h3></article>
`;
}Tècnicament, una etiqueta <style> escrita així dins de render() sí que queda encapsulada pel Shadow DOM de la mateixa manera que la resta del contingut: les seves regles només afectaran aquest shadow root. El problema no és d'encapsulació, sinó de rendiment: com es va explicar a la lliçó del cicle de renderitzat del mòdul 2, render() es torna a executar cada vegada que canvia una propietat reactiva, i amb aquest plantejament l'etiqueta <style> completa —i, en molts navegadors, el CSS que conté— es recrearia i es tornaria a processar en cadascuna d'aquestes actualitzacions, un cost completament innecessari per a un CSS que gairebé sempre és el mateix en cada renderitzat.
static styles, en canvi, es processa una sola vegada per definició de classe (no per instància, ni per renderitzat): Lit construeix internament una representació optimitzada del CSS i la reutilitza en totes les actualitzacions i, fins i tot, en totes les instàncies del mateix component que coexisteixin a la pàgina. És, de llarg, la manera recomanada de declarar estils a Lit, i l'única que s'usa a la resta d'aquest curs.
Pel que fa a un <link rel="stylesheet" href="..."> normal apuntant a un fitxer .css extern, col·locat dins del render(): a la pràctica funciona en navegadors moderns, però té un problema afegit de temps: el shadow root es renderitza abans que el navegador acabi de descarregar la fulla d'estils externa, així que es produeix gairebé sempre un parpelleig visible de contingut sense estil (el fenomen conegut com FOUC, flash of unstyled content) durant el primer renderitzat, cada vegada que es crea una nova instància del component. Per aquest motiu, i per l'avantatge de rendiment ja explicat, static styles amb css és la via recomanada per la mateixa documentació de Lit per al cas normal d'un component amb el seu CSS conegut de bestreta.
- Alternatives per a CSS realment extern:
adoptedStyleSheets
adoptedStyleSheetsExisteix un escenari legítim en el qual sí que interessa carregar CSS des d'una font veritablement externa al propi mòdul del component: per exemple, una fulla d'estils generada dinàmicament, o compartida mitjançant una API de baix nivell del navegador anomenada adoptedStyleSheets, que permet construir un objecte CSSStyleSheet mitjançant JavaScript i "adoptar-lo" en un o diversos shadow roots sense necessitat que cadascun tingui la seva pròpia còpia del CSS en memòria.
Lit usa precisament adoptedStyleSheets per sota, de forma transparent, quan el navegador ho suporta (amb una alternativa automàtica per als pocs navegadors que no ho fan), com a mecanisme d'implementació de static styles. No cal, en l'ús normal de Lit, tocar adoptedStyleSheets directament: s'esmenta aquí únicament perquè quedi clar que static styles no és una limitació del framework, sinó una capa còmoda construïda sobre una API estàndard de la plataforma web, pensada exactament per a aquest propòsit de compartir CSS entre shadow roots de forma eficient. La següent lliçó d'aquest mòdul, "Estils Compartits entre Components", torna a aquesta idea de compartir CSS, però treballant sempre al nivell de static styles i css, que és la via pràctica del dia a dia amb Lit.
- Els primers estils propis de
<task-card>
<task-card>Amb la teoria ja coberta, és el moment de donar a <task-card> la seva primera aparença visual real. Recupera el fitxer src/components/task-card.js tal com va quedar al final del mòdul 3 —amb les seves propietats reactives, el seu estat intern expandida i la seva insígnia d'estat— i afegeix static styles:
import { LitElement, html, css } from 'lit';
const conversorDeFecha = {
fromAttribute(valorDelAtributo) {
if (!valorDelAtributo) {
return null;
}
return new Date(valorDelAtributo);
},
toAttribute(valorDeLaPropiedad) {
if (!valorDeLaPropiedad) {
return null;
}
return valorDeLaPropiedad.toISOString().split('T')[0];
},
};
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
estado: { type: String },
prioridad: { type: Number },
urgente: { type: Boolean },
expandida: { state: true },
fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
};
static styles = css`
article {
border: 1px solid #d0d5dd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
font-family: system-ui, sans-serif;
background-color: #ffffff;
}
h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: #1f2933;
}
p {
margin: 0.25rem 0;
font-size: 0.9rem;
color: #52606d;
}
.insignia {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 999px;
font-size: 0.8rem;
margin-bottom: 0.5rem;
}
.aviso {
color: #b42318;
font-weight: bold;
}
`;
constructor() {
super();
this.titulo = 'Tarea sin título';
this.estado = 'pendiente';
this.prioridad = 3;
this.urgente = false;
this.expandida = false;
this.fechaLimite = null;
}
alternarExpandida() {
this.expandida = !this.expandida;
}
renderInsigniaEstado() {
if (this.estado === 'hecha') {
return html`<span class="insignia insignia--hecha">✓ Hecha</span>`;
}
if (this.estado === 'en-progreso') {
return html`<span class="insignia insignia--progreso">◐ En progreso</span>`;
}
return html`<span class="insignia insignia--pendiente">○ Pendiente</span>`;
}
renderFechaLimite() {
if (!this.fechaLimite) {
return '';
}
return html`<p>Fecha límite: ${this.fechaLimite.toLocaleDateString('es-ES')}</p>`;
}
render() {
return html`
<article @click="${this.alternarExpandida}">
<h3>${this.titulo}</h3>
${this.renderInsigniaEstado()}
<p>Prioridad: ${this.prioridad}</p>
${this.renderFechaLimite()}
${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
${this.expandida
? html`
<div class="detalle">
<p>Estado interno: la tarjeta está expandida.</p>
</div>
`
: ''}
</article>
`;
}
}
customElements.define('task-card', TaskCard);El bloc static styles conté regles CSS completament normals —selectors d'etiqueta (article, h3, p) i de classe (.insignia, .aviso)—, exactament igual que en qualsevol fulla d'estils tradicional. L'únic diferent és on viu aquest CSS i a què afecta: com es va explicar a l'apartat 1, la regla article { border: 1px solid #d0d5dd; ... } només pot afectar l'<article> que la pròpia plantilla de <task-card> genera, sense risc de xocar amb cap altre <article> que pogués existir a qualsevol altra part de TaskFlow (per exemple, dins de <task-list>, que a la següent lliçó d'aquest mòdul també usarà el seu propi element d'estructura). Nota, a més, que les classes .insignia--hecha, .insignia--progreso i .insignia--pendiente, encara que generades per renderInsigniaEstado(), no tenen encara cap regla pròpia en aquest primer pas: es recuperen amb els seus colors concrets a la lliçó "Variables CSS Personalitzades i Theming" d'aquest mateix mòdul.
- El selector especial
:host
:hostAbans de tancar aquesta primera lliçó, convé presentar un selector que no existeix en el CSS tradicional i que resultarà imprescindible a la resta del mòdul: :host. Dins d'una fulla d'estils declarada amb static styles, :host selecciona el propi element personalitzat, és a dir, l'etiqueta <task-card> tal com es veu des de fora del shadow root, no cap element de dins de la plantilla.
Aquesta regla :host { display: block; } és, de fet, una pràctica habitual i recomanada en començar a donar estil a qualsevol component Lit: per defecte, un element personalitzat sense cap CSS associat es comporta com un element inline (igual que un <span>), el que pot produir comportaments de maquetació poc intuïtius si s'espera que ocupi tot l'ample disponible o que respecti correctament els marges; declarar :host { display: block; } fa que <task-card> es comporti, de cara al document que el conté, com un bloc normal. :host és també la peça clau per poder escriure selectors com :host([estado="hecha"]), esmentats a l'última lliçó del mòdul 3 a propòsit de reflect: true, encara que aquest ús concret —amb variables CSS i selecció per atribut— es desenvolupa amb més detall a les lliçons següents d'aquest mòdul.
Errors Comuns i Consells
- Esperar que una classe CSS global de la pàgina afecti un component Lit: com es va explicar a l'apartat 1, el CSS extern al shadow root no hi entra sota cap circumstància (llevat d'herència de propietats heretables i variables CSS, que es veuen a la següent lliçó). Si una regla escrita en una fulla d'estils general del lloc no sembla aplicar-se a
<task-card>, la causa gairebé sempre és aquesta frontera, no un error de sintaxi. - Escriure el CSS amb una etiqueta
<style>dins derender()"perquè funciona": com es va detallar a l'apartat 3, aquesta tècnica encapsula correctament el CSS, però el reprocessa en cada actualització del component, un cost innecessari questatic stylesevita per disseny. Excepte casos molt concrets de CSS generat dinàmicament,static stylesés sempre l'opció a preferir. - Oblidar
:host { display: block; }i sorprendre's pel comportament de maquetació del component: un element personalitzat sense estil propi es comporta cominlineper defecte; si<task-card>sembla no respectar l'ample o els marges esperats dins de<task-list>, convé revisar primer si s'ha declarat undisplayexplícit a:host. - Intentar usar un
<link rel="stylesheet">dins del shadow root esperant que carregui de forma instantània: com es va apuntar a l'apartat 3, un<link>extern introdueix una espera de xarxa que pot provocar un parpelleig de contingut sense estil a cada nova instància del component; per al CSS propi d'un component, conegut de bestreta,static stylesno té aquest problema perquè el CSS ja viatja inclòs en el propi mòdul JavaScript.
Exercicis
- Afegeix a la fulla d'estils de
<task-card>una regla:host { display: block; }i comprova, col·locant dues o més<task-card>seguides en una pàgina de prova sense cap altre CSS extern, que cadascuna ocupa la seva pròpia línia en lloc d'aparèixer una al costat de l'altra. - Escriu, en una pàgina HTML de prova fora del shadow root de
<task-card>, una reglaarticle { border: 5px solid red; }en una fulla d'estils normal del document, i comprova al navegador que la vora vermella no afecta en absolut l'<article>intern de<task-card>. Explica amb les teves pròpies paraules, recolzant-te en l'apartat 1, per què passa això. - Afegeix una nova regla a
static stylesque doni a la classe.detalle(el bloc que apareix quanexpandidaéstrue) un fons gris clar i un padding propi, i comprova que aquest estil només s'aplica quan la targeta està expandida, sense haver de tocar res de la lògica derender().
Solucions
static styles = css`
:host {
display: block;
}
article {
border: 1px solid #d0d5dd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
}
`;Sense :host { display: block; }, dues <task-card> seguides a l'HTML tendirien a col·locar-se a la mateixa línia, com passa amb qualsevol element inline (per exemple, dos <span> consecutius); amb la regla afegida, cada <task-card> passa a comportar-se com un bloc independent i les targetes s'apilen verticalment, una per línia, tal com caldria esperar d'una llista de tasques.
-
La vora vermella no apareix a l'
<article>de dins de<task-card>perquè, com s'explica a l'apartat 1, el Shadow DOM aixeca una frontera que impedeix que el CSS del document exterior entri al shadow root del component. La reglaarticle { border: 5px solid red; }, en estar definida fora d'aquest shadow root, només podria afectar elements<article>que visquin igualment fora de qualsevol shadow root (o dins de shadow roots que hagin declarat explícitament aquesta mateixa regla), mai a l'<article>intern de<task-card>, que només obeeix al CSS declarat en el seu propistatic styles.
static styles = css`
/* ...regles anteriors... */
.detalle {
background-color: #f2f4f7;
padding: 0.5rem;
border-radius: 4px;
margin-top: 0.5rem;
}
`;Com .detalle només apareix a l'HTML generat per render() quan this.expandida és true (segons la lògica ja existent del mòdul 3), la regla d'estil s'aplica automàticament només en aquest moment, sense cap necessitat de tocar la condició del render(): el CSS descriu com es veu un element quan existeix, i és la pròpia plantilla la que decideix quan aquell element existeix.
Conclusió
En aquesta lliçó has entès la doble frontera que aixeca el Shadow DOM entre el CSS d'un component i la resta del document, has après a declarar estils amb static styles i la funció css, i has vist per què aquesta via és preferible a una etiqueta <style> dins de render() o a un <link> extern dins del shadow root. Amb tot això, <task-card> té ja la seva primera aparença visual pròpia: una targeta amb vora, tipografia acurada i un :host correctament configurat com a bloc.
No obstant això, en el moment que es doni estil també a <task-list> (que necessitarà el seu propi contenidor, el seu propi títol, potser la seva pròpia tipografia base), apareixerà de seguida un problema pràctic: molt d'aquest CSS —colors base, tipografia, espaiats— té sentit compartir-lo entre diversos components, no repetir-lo copiat i enganxat a cadascun. Aquest és exactament el contingut de la següent lliçó, "Estils Compartits entre Components", on aprendràs a extraure una fulla d'estils comuna reutilitzable entre <task-card> i <task-list>.
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
