<task-card> ja té la seva pròpia fulla d'estils, declarada amb static styles a la lliçó anterior. Però TaskFlow és una aplicació amb diversos components, i a la següent lliçó d'aquest curs <task-list> necessitarà també els seus propis estils: un contenidor per al tauler, un títol, potser una tipografia base coherent amb la de les targetes. Copiar i enganxar el mateix CSS de colors, tipografia i espaiats a cada component nou seria repetir feina i, pitjor encara, generaria inconsistències quan algú actualitzés un color en un component sense recordar-se d'actualitzar-lo també a la resta. Aquesta lliçó resol aquest problema extraient un CSS reutilitzable a un mòdul compartit i combinant-lo amb els estils propis de cada component, aplicant-lo a <task-card> i a una nova versió amb estil de <task-list>.
Contingut
- El problema: CSS duplicat entre diversos components
- Un mòdul d'estils compartits amb
css static stylescom a array: combinar diverses fulles- Creant
shared-styles.jsper a TaskFlow - Aplicant la fulla compartida a
<task-card> - Donant estil a
<task-list>: layout de columna per a les targetes - Quin CSS convé compartir i quin convé deixar local
- El problema: CSS duplicat entre diversos components
Imagina que, seguint el mateix patró que es va usar a la lliçó anterior per a <task-card>, s'escrivís també un static styles complet i autocontingut per a <task-list>, repetint-hi els mateixos valors de color de text, la mateixa família tipogràfica i el mateix tipus de vora arrodonida que ja existeixen a <task-card>:
// task-card.js
static styles = css`
article {
font-family: system-ui, sans-serif;
color: #1f2933;
border-radius: 8px;
}
`;
// task-list.js — mismo CSS, copiado y pegado
static styles = css`
section {
font-family: system-ui, sans-serif;
color: #1f2933;
border-radius: 8px;
}
`;Aquest plantejament funciona, però té un cost que es paga a mesura que TaskFlow creix: si més endavant es decideix canviar la família tipogràfica de tota l'aplicació, o ajustar el to del color de text principal, caldria localitzar i actualitzar aquesta mateixa declaració a cada component que l'hagi copiat, amb el risc real d'oblidar-se'n algun i acabar amb una aplicació visualment inconsistent. És exactament el mateix problema que la duplicació de codi planteja sempre en programació, aplicat ara al CSS.
La solució, igual que amb qualsevol altre fragment de codi repetit, és extreure aquesta part comuna a un únic lloc i reutilitzar-la des d'on calgui.
- Un mòdul d'estils compartits amb
css
cssCom css és simplement una funció de JavaScript que s'importa de lit, el valor que retorna —igual que el valor que retorna html— es pot guardar en una variable, exportar-la des d'un mòdul, i importar-la a qualsevol altre fitxer, exactament igual que es faria amb qualsevol altra constant o funció de JavaScript.
// src/styles/shared-styles.js
import { css } from 'lit';
export const estilosCompartidos = css`
:host {
font-family: system-ui, sans-serif;
color: #1f2933;
}
`;Aquest fitxer no defineix cap component ni cap classe: és, simplement, un mòdul de JavaScript normal el contingut únic del qual és una constant amb un fragment de CSS ja "embolicat" per la funció css. No hi ha res específic de Lit en la manera d'organitzar-lo més enllà d'usar la pròpia funció css; és el mateix patró d'"extreure a un mòdul i exportar" que ja s'haurà usat en qualsevol projecte de JavaScript modern per compartir constants o funcions entre diversos fitxers.
static styles com a array: combinar diverses fulles
static styles com a array: combinar diverses fullesAmb el CSS compartit ja extret al seu propi mòdul, cada component necessita combinar-lo amb el seu propi CSS específic (la vora i el padding de <task-card>, per exemple, que no té sentit compartir amb <task-list>). Per a això, static styles accepta, a més d'un únic valor css, un array de diversos valors css, que Lit combina automàticament en una sola fulla d'estils aplicada al shadow root del component:
import { LitElement, html, css } from 'lit';
import { estilosCompartidos } from '../styles/shared-styles.js';
class TaskCard extends LitElement {
static styles = [
estilosCompartidos,
css`
article {
border: 1px solid #d0d5dd;
border-radius: 8px;
padding: 1rem;
}
`,
];
// ...
}L'ordre dins de l'array importa, igual que en qualsevol fulla d'estils CSS normal: les regles del segon element de l'array s'apliquen després de les del primer, així que, en cas que dues regles competeixin exactament per la mateixa especificitat sobre el mateix selector, guanya la que apareix més tard a l'array. A la pràctica, amb fulles compartides pensades per no solapar-se amb el CSS específic de cada component (com es recomana a l'apartat 7), aquest matís d'ordre rarament arriba a notar-se, però convé tenir-lo present si mai apareix un comportament d'estil inesperat que no s'explica mirant només una de les dues fulles.
- Creant
shared-styles.js per a TaskFlow
shared-styles.js per a TaskFlowAplicant l'anterior a TaskFlow, el mòdul compartit pot créixer una mica més enllà de la tipografia bàsica de l'apartat 2, incorporant també els colors i espaiats que es repetiran en diversos components del tauler:
// src/styles/shared-styles.js
import { css } from 'lit';
export const estilosCompartidos = css`
:host {
display: block;
font-family: system-ui, sans-serif;
color: #1f2933;
}
h2, h3 {
margin: 0 0 0.5rem 0;
color: #1f2933;
}
p {
margin: 0.25rem 0;
color: #52606d;
font-size: 0.9rem;
}
`;Nota que aquesta fulla compartida ja inclou :host { display: block; ... }, el que significa que qualsevol component que la incorpori al seu static styles obté automàticament el comportament de bloc explicat a la lliçó anterior, sense haver de repetir aquesta regla a cada component per separat. També defineix un color de text base (#1f2933 per a títols, #52606d per a text secundari) i una família tipogràfica, exactament els dos valors que, a l'apartat 1, es van identificar com a candidats clars a duplicació entre <task-card> i <task-list>.
- Aplicant la fulla compartida a
<task-card>
<task-card>Amb shared-styles.js ja creat, <task-card> s'actualitza per usar-lo, retirant del seu propi static styles les regles que ja cobreix la fulla compartida:
import { LitElement, html, css } from 'lit';
import { estilosCompartidos } from '../styles/shared-styles.js';
class TaskCard extends LitElement {
static properties = {
// ...igual que a la lliçó anterior...
};
static styles = [
estilosCompartidos,
css`
article {
border: 1px solid #d0d5dd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
background-color: #ffffff;
}
.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, alternarExpandida, renderInsigniaEstado, renderFechaLimite i render
// es mantenen exactament igual que a la lliçó anterior.
}
customElements.define('task-card', TaskCard);Compara aquest static styles amb el de la lliçó anterior: les regles :host, h3 i p han desaparegut d'aquí perquè ara les aporta estilosCompartidos, i només queden les regles realment específiques de <task-card> —la vora i el fons d'article, l'aspecte de .insignia i de .aviso—. El resultat visual, després d'aquesta refactorització, és idèntic al que ja es tenia; el que ha canviat és d'on ve cada regla, no com es veu la targeta en pantalla.
- Donant estil a
<task-list>: layout de columna per a les targetes
<task-list>: layout de columna per a les targetesAmb la fulla compartida ja disponible, <task-list> pot rebre els seus primers estils reutilitzant exactament el mateix mecanisme, i afegint a més un layout propi per organitzar les targetes en columna:
import { LitElement, html, css } from 'lit';
import { estilosCompartidos } from '../styles/shared-styles.js';
import './task-card.js';
class TaskList extends LitElement {
static properties = {
tareas: { type: Array },
};
static styles = [
estilosCompartidos,
css`
section {
padding: 1rem;
}
.lista {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
`,
];
constructor() {
super();
this.tareas = [
{ id: 1, titulo: 'Preparar la demo del sprint', estado: 'en-progreso', prioridad: 4, urgente: true },
{ id: 2, titulo: 'Revisar el PR de autenticación', estado: 'pendiente', prioridad: 2, urgente: false },
{ id: 3, titulo: 'Desplegar a producción', estado: 'hecha', prioridad: 5, urgente: false },
];
}
render() {
return html`
<section>
<h2>Mis tareas</h2>
<div class="lista">
${this.tareas.map(
(tarea) => html`
<task-card
.titulo="${tarea.titulo}"
.estado="${tarea.estado}"
.prioridad="${tarea.prioridad}"
.urgente="${tarea.urgente}"
></task-card>
`
)}
</div>
</section>
`;
}
}
customElements.define('task-list', TaskList);La classe .lista, aplicada al <div> que envolta l'Array.map amb les targetes (ja present a la plantilla des del mòdul 2), usa display: flex; flex-direction: column; amb un gap per separar visualment cada <task-card> de la següent, sense necessitat de marges manuals a cada targeta. h2 rep automàticament l'estil ja definit a estilosCompartidos (el mateix color i marge que també usa l'h3 de <task-card>), així que el títol "Mis tareas" queda visualment coherent amb el títol de cada targeta sense haver escrit cap regla nova per a això.
És interessant notar un detall sobre el layout: display: flex es declara sobre .lista, que viu dins del shadow root de <task-list>, i afecta els elements <task-card> que són fills directes d'aquell <div> al DOM lleuger (la llum, en la terminologia de Web Components) de <task-list>. Això funciona perquè Flexbox, com qualsevol propietat de layout de CSS, opera sobre els elements fills reals a l'arbre de renderitzat final, independentment de si aquests fills són elements natius o elements personalitzats amb el seu propi Shadow DOM intern: <task-list> no necessita "veure dins" de cada <task-card> per poder organitzar-les en columna, de la mateixa manera que tampoc necessitava veure dins d'elles per passar-los les seves propietats.
- Quin CSS convé compartir i quin convé deixar local
No tot el CSS d'un projecte amb diversos components és bon candidat per viure en una fulla compartida. Convé un criteri clar per decidir què extreure i què deixar al static styles propi de cada component:
| Candidat a compartir | Candidat a quedar-se local |
|---|---|
Tipografia base (font-family, mides de text generals) |
Estructura de layout específica d'un component (.lista amb flex-direction: column a <task-list>) |
| Colors de text base (títol, text secundari) | Vores, ombres o fons propis de la "forma" visual d'un component concret (la targeta de <task-card>) |
:host { display: block; }, quan aplica a gairebé tots els components del projecte |
Classes molt específiques d'un component (.insignia, .aviso), que no tenen sentit fora d'ell |
| Espaiats o radis de vora molt generals, usats de forma consistent a tota l'aplicació | Qualsevol estil que depengui d'una dada pròpia del component (per exemple, un color que canvia segons la propietat estado, que es tracta a la següent lliçó) |
La guia pràctica és: si canviar aquell valor hauria de canviar, a la vegada, l'aspecte de diversos components diferents de TaskFlow, és un bon candidat per a shared-styles.js. Si només té sentit per a la forma o el comportament visual particular d'un component concret, ha de quedar-se en el seu propi static styles. Forçar massa CSS cap al mòdul compartit, en canvi, corre el risc contrari: convertir-lo en una fulla d'estils genèrica i sobrecarregada, amb regles que en realitat només usa un dels components, el que dificulta raonar sobre quin CSS afecta a què.
Errors Comuns i Consells
- Oblidar embolicar el CSS compartit amb
css, i exportar una cadena de text normal: sishared-styles.jsexportés simplement una cadena (export const estilosCompartidos = '...';sense la funciócss), Lit no la reconeixeria com una fulla d'estils vàlida en incloure-la a l'array destatic styles, i produiria un error en temps d'execució. Elcssde la lliçó anterior és imprescindible també aquí. - Duplicar per error una mateixa regla a la fulla compartida i a la fulla local d'un component: si
estilosCompartidosja defineixcolorper ap, i el component torna a declararcolorper apen el seu propi bloccss, la segona declaració guanya (per anar després a l'array, com es va explicar a l'apartat 3), el que pot generar una inconsistència subtil difícil de detectar a simple vista. Convé revisar, en afegir una regla local, que no estigui ja coberta per la fulla compartida. - Ficar a la fulla compartida CSS que només usa un component: com es va explicar a l'apartat 7, forçar massa CSS cap a
shared-styles.js(per exemple, la classe.insignia, que només existeix a<task-card>) fa que la fulla compartida creixi de forma desordenada i dificulta saber, sense buscar a tots els components, quines regles estan realment en ús. - Esperar que combinar fulles en un array canviï l'ordre d'aplicació del CSS respecte a l'especificitat normal de CSS:
static stylescom a array no introdueix cap mecanisme especial de prioritat més enllà de l'ordre d'aparició i les regles normals d'especificitat de CSS (id, classe, etiqueta); segueix sent CSS corrent per sota, només que repartit en diversos fragments que Lit combina abans d'inserir-los al shadow root.
Exercicis
- Afegeix a
shared-styles.jsuna nova regla per a l'etiquetabutton(pensant en botons que s'afegiran a TaskFlow en mòduls posteriors), amb una tipografia i un padding bàsics, i comprova-la aplicant-la temporalment en un botó de prova dins de<task-list>. - Elimina intencionadament la importació d'
estilosCompartidosatask-card.js(deixant la resta de l'array destatic stylesigual) i observa, recarregant la pàgina, quin aspecte perd la targeta. Explica amb les teves pròpies paraules per què desapareix precisament aquell aspecte i no un altre. - Usant el criteri de la taula de l'apartat 7, decideix si una regla
.detalle { background-color: #f2f4f7; }(el bloc de detall expandit de<task-card>, vist a la lliçó anterior) hauria de moure's ashared-styles.jso quedar-se alstatic stylespropi de<task-card>. Justifica la resposta.
Solucions
// src/styles/shared-styles.js
export const estilosCompartidos = css`
:host {
display: block;
font-family: system-ui, sans-serif;
color: #1f2933;
}
h2, h3 {
margin: 0 0 0.5rem 0;
color: #1f2933;
}
p {
margin: 0.25rem 0;
color: #52606d;
font-size: 0.9rem;
}
button {
font-family: inherit;
font-size: 0.9rem;
padding: 0.4rem 0.8rem;
border-radius: 6px;
border: 1px solid #d0d5dd;
cursor: pointer;
}
`;En afegir temporalment un <button>Prueba</button> dins de render() a <task-list>, el botó apareix ja amb la tipografia i el padding definits, sense necessitat de cap regla addicional al static styles propi de <task-list>, perquè tots dos components comparteixen la mateixa fulla importada.
-
Sense
estilosCompartidosa l'array destatic stylesde<task-card>, la targeta perd el:host { display: block; }(així que diverses targetes seguides tornarien a col·locar-se en línia, com es va explicar a la lliçó anterior) i també perd el color de text i la tipografia base definits per ah3ip, que tornarien a l'estil per defecte del navegador. La vora, el padding i el fons de l'<article>, en canvi, seguirien presents, perquè aquestes regles estan al segon element de l'array, definit localment atask-card.jsi no a la fulla compartida. -
Hauria de quedar-se al
static stylespropi de<task-card>. Aplicant el criteri de l'apartat 7:.detalleés una classe que només existeix dins de la plantilla de<task-card>(el bloc que apareix quanexpandidaéstrue), no té sentit reutilitzar-la a<task-list>ni a cap altre component futur de TaskFlow, i canviar el seu aspecte no hauria, en principi, d'afectar cap altre component del tauler. És exactament el perfil d'"estil propi de la forma visual d'un component concret" que la taula de l'apartat 7 recomana deixar local.
Conclusió
En aquesta lliçó has après a evitar la duplicació de CSS entre diversos components Lit extraient un mòdul d'estils compartits amb css, i a combinar-lo amb el CSS específic de cada component mitjançant static styles com a array. Has aplicat aquest patró creant shared-styles.js per a TaskFlow, usant-lo tant a <task-card> com a la nova versió amb estil de <task-list>, que ara organitza les seves targetes en una columna amb espaiat consistent gràcies a Flexbox.
TaskFlow té ja, per primera vegada al curs, un aspecte visual coherent entre els seus dos components. Però encara queda un problema per resoldre: els colors concrets —el de cada insígnia d'estat, el de l'avís d'urgència— estan escrits com a valors fixos dins del CSS de <task-card>, sense cap manera que qui usi el component des de fora pugui personalitzar-los sense tocar directament el seu codi font. Aquest és exactament el contingut de la següent lliçó, "Variables CSS Personalitzades i Theming", on descobriràs per què les variables CSS són capaces de travessar la frontera del Shadow DOM en la qual tant s'ha insistit en aquesta lliçó, i les usaràs per fer que els colors de TaskFlow siguin configurables des de fora de cada component.
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
