1. Introduzione
Che cos'è Lit
Lit è una semplice libreria per creare componenti web veloci e leggeri che funzionano in qualsiasi framework o senza framework. Con Lit puoi creare componenti condivisibili, applicazioni, sistemi di progettazione e altro ancora.
Obiettivi didattici
Come tradurre diversi concetti di React in Lit, ad esempio:
- JSX e modelli
- Componenti e oggetti di scena
- Stato e ciclo di vita
- Hook
- Bambini
- Refs
- Stato di mediazione
Cosa creerai
Al termine di questo codelab, sarai in grado di convertire i concetti dei componenti React nei loro analoghi Lit.
Che cosa ti serve
- L'ultima versione di Chrome, Safari, Firefox o Edge.
- Conoscenza di HTML, CSS, JavaScript e Chrome DevTools.
- Conoscenza di React
- (Avanzato) Se vuoi la migliore esperienza di sviluppo, scarica VS Code. Avrai bisogno anche di lit-plugin per VS Code e NPM.
2. Lit vs React
I concetti e le funzionalità principali di Lit sono simili a quelli di React per molti aspetti, ma Lit presenta anche alcune differenze e fattori di differenziazione chiave:
È piccolo
Lit è minuscolo: occupa circa 5 kB in formato compresso e ridotto rispetto agli oltre 40 kB di React + ReactDOM.

È veloce
Nei benchmark pubblici che confrontano il sistema di modelli di Lit, lit-html, con il VDOM di React, lit-html risulta più veloce dell'8-10% rispetto a React nel caso peggiore e più veloce del 50% nei casi d'uso più comuni.
LitElement (la classe base dei componenti di Lit) aggiunge un sovraccarico minimo a lit-html, ma supera le prestazioni di React del 16-30% quando si confrontano le funzionalità dei componenti, come l'utilizzo della memoria e i tempi di interazione e avvio.

Non ha bisogno di una build
Grazie a nuove funzionalità del browser come i moduli ES e i modelli letterali taggati, Lit non richiede la compilazione per essere eseguito. Ciò significa che gli ambienti di sviluppo possono essere configurati con un tag script + un browser + un server e sono pronti per l'uso.
Con i moduli ES e le CDN moderne come Skypack o UNPKG, potresti non aver nemmeno bisogno di NPM per iniziare.
Tuttavia, se vuoi, puoi comunque creare e ottimizzare il codice Lit. Il recente consolidamento degli sviluppatori intorno ai moduli ES nativi è stato positivo per Lit: Lit è solo JavaScript normale e non sono necessarie CLI specifiche del framework o gestione della build.
Indipendente dal framework
I componenti di Lit si basano su un insieme di standard web chiamati componenti web. Ciò significa che la creazione di un componente in Lit funzionerà nei framework attuali e futuri. Se supporta gli elementi HTML, supporta i componenti web.
Gli unici problemi di interoperabilità del framework si verificano quando i framework hanno un supporto restrittivo per il DOM. React è uno di questi framework, ma consente di utilizzare le vie di fuga tramite i riferimenti e i riferimenti in React non offrono una buona esperienza per gli sviluppatori.
Il team di Lit sta lavorando a un progetto sperimentale chiamato @lit-labs/react che analizzerà automaticamente i componenti Lit e genererà un wrapper React in modo da non dover utilizzare i riferimenti.
Inoltre, Custom Elements Everywhere ti mostrerà quali framework e librerie funzionano bene con gli elementi personalizzati.
Supporto di prima classe per TypeScript
Sebbene sia possibile scrivere tutto il codice Lit in JavaScript, Lit è scritto in TypeScript e il team di Lit consiglia agli sviluppatori di utilizzare anche TypeScript.
Il team di Lit ha collaborato con la community di Lit per contribuire a gestire i progetti che portano la verifica dei tipi e l'intellisense di TypeScript nei modelli Lit sia in fase di sviluppo che di compilazione con lit-analyzer e lit-plugin.


Gli strumenti per sviluppatori sono integrati nel browser
I componenti Lit sono solo elementi HTML nel DOM. Ciò significa che per ispezionare i componenti non è necessario installare strumenti o estensioni per il browser.
Puoi semplicemente aprire gli strumenti di sviluppo, selezionare un elemento ed esplorarne le proprietà o lo stato.

È stato creato pensando al rendering lato server (SSR)
Lit 2 è stato creato pensando al supporto SSR. Al momento della stesura di questo codelab, il team di Lit non ha ancora rilasciato gli strumenti SSR in forma stabile, ma ha già eseguito il deployment di componenti sottoposti a rendering lato server nei prodotti Google e ha testato SSR nelle applicazioni React. Il team di Lit prevede di rilasciare questi strumenti esternamente su GitHub a breve.
Nel frattempo, puoi seguire i progressi del team di Lit qui.
È un investimento iniziale basso
Lit non richiede un impegno significativo per l'utilizzo. Puoi creare componenti in Lit e aggiungerli al tuo progetto esistente. Se non ti piacciono, non devi convertire l'intera app contemporaneamente, perché i componenti web funzionano in altri framework.
Hai creato un'intera app in Lit e vuoi passare a un altro framework? In questo caso, puoi inserire l'applicazione Lit attuale nel nuovo framework ed eseguire la migrazione di ciò che vuoi ai componenti del nuovo framework.
Inoltre, molti framework moderni supportano l'output nei componenti web, il che significa che in genere possono essere inseriti all'interno di un elemento Lit.
3. Configurazione ed esplorazione del Playground
Esistono due modi per svolgere questo codelab:
- Puoi farlo interamente online, nel browser
- (Avanzato) Puoi farlo sulla tua macchina locale utilizzando VS Code
Accesso al codice
Nel codelab troverai link al playground di Lit come questo:
Playground è una sandbox di codice che viene eseguita completamente nel browser. Può compilare ed eseguire file TypeScript e JavaScript e può anche risolvere automaticamente le importazioni nei moduli Node.js. Ad esempio:
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';
Puoi completare l'intero tutorial nel playground Lit, utilizzando questi checkpoint come punti di partenza. Se utilizzi VS Code, puoi utilizzare questi checkpoint per scaricare il codice iniziale per qualsiasi passaggio, nonché per controllare il tuo lavoro.
Esplorare l'UI del parco giochi illuminato

Lo screenshot della UI del playground Lit evidenzia le sezioni che utilizzerai in questo codelab.
- Selettore file. Prendi nota del pulsante Più…
- Editor di file.
- Anteprima del codice.
- Pulsante Ricarica.
- Pulsante di download.
Configurazione di VS Code (avanzata)
Ecco i vantaggi dell'utilizzo di questa configurazione di VS Code:
- Controllo del tipo di modello
- Intellisense e completamento automatico dei modelli
Se hai già installato NPM e VS Code (con il plug-in lit-plugin) e sai come utilizzare questo ambiente, puoi semplicemente scaricare e avviare questi progetti nel seguente modo:
- Premi il pulsante di download.
- Estrai i contenuti del file tar in una directory
- (Se TS) configura un quick tsconfig che restituisce moduli ES e ES2015+
- Installa un server di sviluppo in grado di risolvere gli identificatori di moduli semplici (il team Lit consiglia @web/dev-server)
- Ecco un esempio
package.json
- Ecco un esempio
- Esegui il server di sviluppo e apri il browser (se utilizzi @web/dev-server, puoi utilizzare
npx web-dev-server --node-resolve --watch --open)- Se utilizzi l'esempio
package.json, usanpm run dev
- Se utilizzi l'esempio
4. JSX e modelli
In questa sezione, imparerai le nozioni di base dei modelli in Lit.
Modelli JSX e Lit
JSX è un'estensione della sintassi di JavaScript che consente agli utenti di React di scrivere facilmente modelli nel proprio codice JavaScript. I modelli Lit� hanno uno scopo simile: esprimere l'interfaccia utente di un componente in funzione del suo stato.
Sintassi di base
In React, il rendering di un JSX Hello World è simile al seguente:
import 'react';
import ReactDOM from 'react-dom';
const name = 'Josh Perez';
const element = (
<>
<h1>Hello, {name}</h1>
<div>How are you?</div>
</>
);
ReactDOM.render(
element,
mountNode
);
Nell'esempio precedente, sono presenti due elementi e una variabile "name" inclusa. In Lit, devi:
import {html, render} from 'lit';
const name = 'Josh Perez';
const element = html`
<h1>Hello, ${name}</h1>
<div>How are you?</div>`;
render(
element,
mountNode
);
Tieni presente che i modelli Lit non richiedono un frammento React per raggruppare più elementi nei modelli.
In Lit, i modelli sono racchiusi in un html modello con tag LITeral, da cui deriva il nome di Lit.
Valori del modello
I modelli Lit possono accettare altri modelli Lit, noti come TemplateResult. Ad esempio, racchiudi name tra tag in corsivo (<i>) e racchiudilo con un letterale di modello con tag N.B. Assicurati di utilizzare l'accento grave (`) e non l'apice singolo (').
import {html, render} from 'lit';
const name = html`<i>Josh Perez</i>`;
const element = html`
<h1>Hello, ${name}</h1>
<div>How are you?</div>`;
render(
element,
mountNode
);
I lit TemplateResult possono accettare array, stringhe, altri TemplateResult e direttive.
Per un esercizio, prova a convertire il seguente codice React in Lit:
const itemsToBuy = [
<li>Bananas</li>,
<li>oranges</li>,
<li>apples</li>,
<li>grapes</li>
];
const element = (
<>
<h1>Things to buy:</h1>
<ol>
{itemsToBuy}
</ol>
</>);
ReactDOM.render(
element,
mountNode
);
Risposta:
import {html, render} from 'lit';
const itemsToBuy = [
html`<li>Bananas</li>`,
html`<li>oranges</li>`,
html`<li>apples</li>`,
html`<li>grapes</li>`
];
const element = html`
<h1>Things to buy:</h1>
<ol>
${itemsToBuy}
</ol>`;
render(
element,
mountNode
);
Passaggio e impostazione delle proprietà
Una delle differenze più grandi tra le sintassi di JSX e Lit è la sintassi del data binding. Ad esempio, prendi questo input React con i binding:
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
<input
disabled={disabled}
className={`static-class ${myClass}`}
defaultValue={value}/>;
ReactDOM.render(
element,
mountNode
);
Nell'esempio precedente, è definito un input che esegue le seguenti operazioni:
- Imposta disabled su una variabile definita (false in questo caso)
- Imposta la classe su
static-classpiù una variabile (in questo caso"static-class my-class") - Imposta un valore predefinito
In Lit, devi:
import {html, render} from 'lit';
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
<input
?disabled=${disabled}
class="static-class ${myClass}"
.value=${value}>`;
render(
element,
mountNode
);
Nell'esempio di Lit viene aggiunta un'associazione booleana per attivare/disattivare l'attributo disabled.
Successivamente, c'è un binding direttamente all'attributo class anziché a className. È possibile aggiungere più binding all'attributo class, a meno che tu non stia utilizzando la direttiva classMap, che è un helper dichiarativo per attivare/disattivare le classi.
Infine, la proprietà value viene impostata sull'input. A differenza di React, questo non imposta l'elemento di input come di sola lettura, in quanto segue l'implementazione e il comportamento nativi dell'input.
Sintassi di binding delle proprietà di illuminazione
html`<my-element ?attribute-name=${booleanVar}>`;
- Il prefisso
?è la sintassi di binding per attivare/disattivare un attributo di un elemento. - Equivalente a
inputRef.toggleAttribute('attribute-name', booleanVar) - Utile per gli elementi che utilizzano
disabledperchédisabled="false"viene comunque letto come vero dal DOM perchéinputElement.hasAttribute('disabled') === true
html`<my-element .property-name=${anyVar}>`;
- Il prefisso
.è la sintassi di binding per impostare una proprietà di un elemento - Equivalente a
inputRef.propertyName = anyVar - Ideale per passare dati complessi come oggetti, array o classi
html`<my-element attribute-name=${stringVar}>`;
- Si associa all'attributo di un elemento
- Equivalente a
inputRef.setAttribute('attribute-name', stringVar) - Ideale per valori di base, selettori di regole di stile e querySelector
Gestori di intent
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
<input
onClick={() => console.log('click')}
onChange={e => console.log(e.target.value)} />;
ReactDOM.render(
element,
mountNode
);
Nell'esempio precedente, è definito un input che esegue le seguenti operazioni:
- Registra la parola "click" quando viene fatto clic sull'input
- Registra il valore dell'input quando l'utente digita un carattere
In Lit, devi:
import {html, render} from 'lit';
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
<input
@click=${() => console.log('click')}
@input=${e => console.log(e.target.value)}>`;
render(
element,
mountNode
);
Nell'esempio Lit, al listener viene aggiunto l'evento click con @click.
Successivamente, anziché utilizzare onChange, esiste un binding all'evento input nativo di <input>, poiché l'evento change nativo viene attivato solo su blur (React astrae questi eventi).
Sintassi del gestore di eventi Lit
html`<my-element @event-name=${() => {...}}></my-element>`;
- Il prefisso
@è la sintassi di binding per un listener di eventi - Equivalente a
inputRef.addEventListener('event-name', ...) - Utilizza i nomi degli eventi DOM nativi
5. Componenti e oggetti di scena
In questa sezione scoprirai i componenti e le funzioni della classe Lit. Lo stato e gli hook vengono trattati in modo più dettagliato nelle sezioni successive.
Componenti delle classi e LitElement
L'equivalente Lit di un componente di classe React è LitElement e il concetto di "proprietà reattive" di Lit è una combinazione di props e stato di React. Ad esempio:
import React from 'react';
import ReactDOM from 'react-dom';
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {name: ''};
}
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
const element = <Welcome name="Elliott"/>
ReactDOM.render(
element,
mountNode
);
Nell'esempio precedente è presente un componente React che:
- Visualizza un
name - Imposta il valore predefinito di
namesu stringa vuota ("") - Riassegna
namea"Elliott"
Ecco come fare in LitElement
In TypeScript:
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
@property({type: String})
name = '';
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
}
In JavaScript:
import {LitElement, html} from 'lit';
class WelcomeBanner extends LitElement {
static get properties() {
return {
name: {type: String}
}
}
constructor() {
super();
this.name = '';
}
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
}
customElements.define('welcome-banner', WelcomeBanner);
Nel file HTML:
<!-- index.html -->
<head>
<script type="module" src="./index.js"></script>
</head>
<body>
<welcome-banner name="Elliott"></welcome-banner>
</body>
Riepilogo di ciò che sta accadendo nell'esempio riportato sopra:
@property({type: String})
name = '';
- Definisce una proprietà reattiva pubblica, una parte dell'API pubblica del componente
- Espone un attributo (per impostazione predefinita) e una proprietà del componente
- Definisce come tradurre l'attributo del componente (che sono stringhe) in un valore
static get properties() {
return {
name: {type: String}
}
}
- Svolge la stessa funzione del decoratore
@propertyTS, ma viene eseguito in modo nativo in JavaScript
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
- Viene chiamato ogni volta che viene modificata una proprietà reattiva
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
...
}
- In questo modo, un nome tag di elemento HTML viene associato a una definizione di classe
- A causa dello standard Custom Elements, il nome tag deve includere un trattino (-)
thisin un LitElement si riferisce all'istanza dell'elemento personalizzato (<welcome-banner>in questo caso)
customElements.define('welcome-banner', WelcomeBanner);
- Questo è l'equivalente JavaScript del decoratore TS
@customElement
<head>
<script type="module" src="./index.js"></script>
</head>
- Importa la definizione dell'elemento personalizzato
<body>
<welcome-banner name="Elliott"></welcome-banner>
</body>
- Aggiunge l'elemento personalizzato alla pagina
- Imposta la proprietà
namesu'Elliott'
Componenti della funzione
Lit non ha un'interpretazione 1:1 di un componente funzionale perché non utilizza JSX o un preprocessor. Tuttavia, è abbastanza semplice comporre una funzione che accetta proprietà e esegue il rendering del DOM in base a queste proprietà. Ad esempio:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Elliott"/>
ReactDOM.render(
element,
mountNode
);
In Lit, questo sarebbe:
import {html, render} from 'lit';
function Welcome(props) {
return html`<h1>Hello, ${props.name}</h1>`;
}
render(
Welcome({name: 'Elliott'}),
document.body.querySelector('#root')
);
6. Stato e ciclo di vita
In questa sezione imparerai a conoscere lo stato e il ciclo di vita di Lit.
Stato
Il concetto di "proprietà reattive" di Lit è un mix di stato e proprietà di React. Le proprietà reattive, se modificate, possono attivare il ciclo di vita del componente. Le proprietà reattive sono disponibili in due varianti:
Proprietà reattive pubbliche
// React
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props)
this.state = {name: 'there'}
}
componentWillReceiveProps(nextProps) {
if (this.props.name !== nextProps.name) {
this.setState({name: nextProps.name})
}
}
}
// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';
class MyEl extends LitElement {
@property() name = 'there';
}
- Definito da
@property - Simile a props e state di React, ma modificabile
- API pubblica a cui accedono e che impostano i consumer del componente
Stato reattivo interno
// React
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props)
this.state = {name: 'there'}
}
}
// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators.js';
class MyEl extends LitElement {
@state() name = 'there';
}
- Definito da
@state - Simile allo stato di React, ma modificabile
- Stato interno privato a cui si accede in genere dall'interno del componente o delle sottoclassi
Lifecycle
Il ciclo di vita di Lit è molto simile a quello di React, ma presenta alcune differenze notevoli.
constructor
// React (js)
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this._privateProp = 'private';
}
}
// Lit (ts)
class MyEl extends LitElement {
@property({type: Number}) counter = 0;
private _privateProp = 'private';
}
// Lit (js)
class MyEl extends LitElement {
static get properties() {
return { counter: {type: Number} }
}
constructor() {
this.counter = 0;
this._privateProp = 'private';
}
}
- L'equivalente in litri è
constructor - Non è necessario passare nulla alla super chiamata
- Richiamato da (non totalmente inclusivo):
document.createElementdocument.innerHTMLnew ComponentClass()- Se nella pagina è presente un nome tag non aggiornato e la definizione viene caricata e registrata con
@customElementocustomElements.define
- Simile per funzione a
constructordi React
render
// React
render() {
return <div>Hello World</div>
}
// Lit
render() {
return html`<div>Hello World</div>`;
}
- L'equivalente in litri è
render - Può restituire qualsiasi risultato visualizzabile, ad esempio
TemplateResultostringe così via. - Come in React,
render()deve essere una funzione pura - Verrà eseguito il rendering del nodo
createRenderRoot()restituito (ShadowRootper impostazione predefinita)
componentDidMount
componentDidMount è simile a una combinazione dei callback del ciclo di vita firstUpdated e connectedCallback di Lit.
firstUpdated
import Chart from 'chart.js';
// React
componentDidMount() {
this._chart = new Chart(this.chartElRef.current, {...});
}
// Lit
firstUpdated() {
this._chart = new Chart(this.chartEl, {...});
}
- Chiamato la prima volta che il modello del componente viene visualizzato nella radice del componente
- Verrà chiamato solo se l'elemento è connesso, ad esempio non verrà chiamato tramite
document.createElement('my-component')finché il nodo non viene aggiunto all'albero DOM. - Questo è un buon punto per eseguire la configurazione dei componenti che richiedono il rendering del DOM da parte del componente
- A differenza delle modifiche apportate da React alle proprietà reattive in
firstUpdated,componentDidMountcauserà un nuovo rendering, anche se il browser in genere raggruppa le modifiche nello stesso frame. Se queste modifiche non richiedono l'accesso al DOM della radice, in genere devono essere inserite inwillUpdate
connectedCallback
// React
componentDidMount() {
this.window.addEventListener('resize', this.boundOnResize);
}
// Lit
connectedCallback() {
super.connectedCallback();
this.window.addEventListener('resize', this.boundOnResize);
}
- Chiamato ogni volta che l'elemento personalizzato viene inserito nell'albero DOM
- A differenza dei componenti React, quando gli elementi personalizzati vengono scollegati dal DOM, non vengono eliminati e possono quindi essere "collegati" più volte
firstUpdatednon verrà più chiamato
- Utile per reinizializzare il DOM o ricollegare i listener di eventi che sono stati puliti al momento della disconnessione
- Nota:
connectedCallbackpotrebbe essere chiamato prima difirstUpdated, quindi alla prima chiamata il DOM potrebbe non essere disponibile
componentDidUpdate
// React
componentDidUpdate(prevProps) {
if (this.props.title !== prevProps.title) {
this._chart.setTitle(this.props.title);
}
}
// Lit (ts)
updated(prevProps: PropertyValues<this>) {
if (prevProps.has('title')) {
this._chart.setTitle(this.title);
}
}
- L'equivalente di Lit è
updated(utilizzando il passato inglese di "update") - A differenza di React,
updatedviene chiamato anche nel rendering iniziale - Simile per funzione a
componentDidUpdatedi React
componentWillUnmount
// React
componentWillUnmount() {
this.window.removeEventListener('resize', this.boundOnResize);
}
// Lit
disconnectedCallback() {
super.disconnectedCallback();
this.window.removeEventListener('resize', this.boundOnResize);
}
- L'equivalente letterario è simile a
disconnectedCallback - A differenza dei componenti React, quando gli elementi personalizzati vengono scollegati dal DOM, il componente non viene eliminato
- A differenza di
componentWillUnmount,disconnectedCallbackviene chiamato dopo la rimozione dell'elemento dall'albero - Il DOM all'interno della radice è ancora collegato al sottoalbero della radice
- Utile per pulire i listener di eventi e i riferimenti con perdite in modo che il browser possa raccogliere i componenti inutilizzati
Esercizio
import React from 'react';
import ReactDOM from 'react-dom';
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
Nell'esempio precedente, è presente un semplice orologio che esegue le seguenti operazioni:
- Viene visualizzato "Hello World! Sono le" e poi mostra l'ora
- Ogni secondo l'orologio si aggiorna
- Quando viene smontato, cancella l'intervallo che chiama il segno di graduazione.
Inizia con la dichiarazione della classe del componente:
// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
}
// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
}
customElements.define('lit-clock', LitClock);
Successivamente, inizializza date e dichiaralo una proprietà reattiva interna con @state, poiché gli utenti del componente non imposteranno date direttamente.
// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
@state() // declares internal reactive prop
private date = new Date(); // initialization
}
// Lit (JS)
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
static get properties() {
return {
// declares internal reactive prop
date: {state: true}
}
}
constructor() {
super();
// initialization
this.date = new Date();
}
}
customElements.define('lit-clock', LitClock);
Poi esegui il rendering del modello.
// Lit (JS & TS)
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
Ora implementa il metodo del segno di spunta.
tick() {
this.date = new Date();
}
Poi viene l'implementazione di componentDidMount. Anche in questo caso, l'analogo di Lit è una combinazione di firstUpdated e connectedCallback. Nel caso di questo componente, la chiamata di tick con setInterval non richiede l'accesso al DOM all'interno della radice. Inoltre, l'intervallo verrà cancellato quando l'elemento viene rimosso dalla struttura del documento, quindi se viene ricollegato, l'intervallo dovrà ricominciare. Pertanto, connectedCallback è una scelta migliore in questo caso.
// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
@state()
private date = new Date();
// initialize timerId for TS
private timerId = -1 as unknown as ReturnType<typeof setTimeout>;
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
...
}
// Lit (JS)
constructor() {
super();
// initialization
this.date = new Date();
this.timerId = -1; // initialize timerId for JS
}
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
Infine, pulisci l'intervallo in modo che non esegua il tick dopo che l'elemento è stato disconnesso dall'albero del documento.
// Lit (TS & JS)
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
Mettendo tutto insieme, dovrebbe avere questo aspetto:
// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
@state()
private date = new Date();
private timerId = -1 as unknown as ReturnType<typeof setTimeout>;
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.date = new Date();
}
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
}
// Lit (JS)
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
static get properties() {
return {
date: {state: true}
}
}
constructor() {
super();
this.date = new Date();
}
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.date = new Date();
}
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
}
customElements.define('lit-clock', LitClock);
7. Hook
In questa sezione imparerai a tradurre i concetti di React Hook in Lit.
I concetti degli hook di React
Gli hook React forniscono un modo per i componenti funzionali di "agganciarsi" allo stato. Questo approccio offre diversi vantaggi.
- Semplificano il riutilizzo della logica stateful
- Aiutare a dividere un componente in funzioni più piccole
Inoltre, l'attenzione ai componenti basati su funzioni ha risolto alcuni problemi relativi alla sintassi basata su classi di React, ad esempio:
- Dover passare
propsdaconstructorasuper - L'inizializzazione disordinata delle proprietà in
constructor- Questo era un motivo dichiarato dal team di React all'epoca, ma risolto da ES2019
- Problemi causati da
thische non fa più riferimento al componente
Concetti relativi agli hook di React in Lit
Come indicato nella sezione Componenti e proprietà, Lit non offre un modo per creare elementi personalizzati da una funzione, ma LitElement risolve la maggior parte dei problemi principali dei componenti di classe React. Ad esempio:
// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';
class MyEl extends React.Component {
constructor(props) {
super(props); // Leaky implementation
this.state = {count: 0};
this._chart = null; // Deemed messy
}
render() {
return (
<>
<div>Num times clicked {count}</div>
<button onClick={this.clickCallback}>click me</button>
</>
);
}
clickCallback() {
// Errors because `this` no longer refers to the component
this.setState({count: this.count + 1});
}
}
// Lit (ts)
class MyEl extends LitElement {
@property({type: Number}) count = 0; // No need for constructor to set state
private _chart = null; // Public class fields introduced to JS in 2019
render() {
return html`
<div>Num times clicked ${count}</div>
<button @click=${this.clickCallback}>click me</button>`;
}
private clickCallback() {
// No error because `this` refers to component
this.count++;
}
}
In che modo Lit affronta questi problemi?
constructornon accetta argomenti- Tutti i binding
@eventvengono associati automaticamente athis thisnella stragrande maggioranza dei casi si riferisce al riferimento dell'elemento personalizzato- Le proprietà della classe ora possono essere istanziate come membri della classe. In questo modo, vengono pulite le implementazioni basate su costruttori
Controller reattivi
I concetti principali alla base degli hook esistono in Lit come controller reattivi. I pattern del controller reattivo consentono di condividere la logica stateful, suddividere i componenti in parti più piccole e modulari e collegarsi al ciclo di vita degli aggiornamenti di un elemento.
Un controller reattivo è un'interfaccia oggetto che può essere collegata al ciclo di vita di aggiornamento di un host controller come LitElement.
Il ciclo di vita di un ReactiveController e di un reactiveControllerHost è:
interface ReactiveController {
hostConnected(): void;
hostUpdate(): void;
hostUpdated(): void;
hostDisconnected(): void;
}
interface ReactiveControllerHost {
addController(controller: ReactiveController): void;
removeController(controller: ReactiveController): void;
requestUpdate(): void;
readonly updateComplete: Promise<boolean>;
}
Se crei un controller reattivo e lo colleghi a un host con addController, il ciclo di vita del controller verrà chiamato insieme a quello dell'host. Ad esempio, ricorda l'esempio dell'orologio della sezione Stato e ciclo di vita:
import React from 'react';
import ReactDOM from 'react-dom';
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
Nell'esempio precedente, è presente un semplice orologio che esegue le seguenti operazioni:
- Viene visualizzato "Hello World! Sono le" e poi mostra l'ora
- Ogni secondo l'orologio si aggiorna
- Quando viene smontato, cancella l'intervallo che chiama il segno di graduazione.
Creazione della struttura del componente
Inizia con la dichiarazione della classe del componente e aggiungi la funzione render.
// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
@customElement('my-element')
class MyElement extends LitElement {
render() {
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${'time to get Lit'}.</h2>
</div>
`;
}
}
// Lit (JS) - index.js
import {LitElement, html} from 'lit';
class MyElement extends LitElement {
render() {
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${'time to get Lit'}.</h2>
</div>
`;
}
}
customElements.define('my-element', MyElement);
Creazione del controller
Ora passa a clock.ts e crea un corso per ClockController e configura constructor:
// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';
export class ClockController implements ReactiveController {
private readonly host: ReactiveControllerHost;
constructor(host: ReactiveControllerHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
}
private tick() {
}
hostDisconnected() {
}
}
// Lit (JS) - clock.js
export class ClockController {
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
}
tick() {
}
hostDisconnected() {
}
}
Un controller reattivo può essere creato in qualsiasi modo, purché condivida l'interfaccia ReactiveController, ma l'utilizzo di una classe con un constructor che può accettare un'interfaccia ReactiveControllerHost, nonché qualsiasi altra proprietà necessaria per inizializzare il controller, è un pattern che il team Lit preferisce utilizzare per i casi più semplici.
Ora devi tradurre i callback del ciclo di vita di React in callback del controller. In breve:
componentDidMount- A
connectedCallbackdi LitElement - Al
hostConnecteddel controller
- A
ComponentWillUnmount- A
disconnectedCallbackdi LitElement - Al
hostDisconnecteddel controller
- A
Per saperne di più sulla conversione del ciclo di vita di React in quello di Lit, consulta la sezione Stato e ciclo di vita.
Successivamente, implementa il callback hostConnected e i metodi tick e pulisci l'intervallo in hostDisconnected come nell'esempio nella sezione Stato e ciclo di vita.
// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
private readonly host: ReactiveControllerHost;
private interval = 0 as unknown as ReturnType<typeof setTimeout>;
date = new Date();
constructor(host: ReactiveControllerHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
this.interval = setInterval(() => this.tick(), 1000);
}
private tick() {
this.date = new Date();
}
hostDisconnected() {
clearInterval(this.interval);
}
}
// Lit (JS) - clock.js
export class ClockController {
interval = 0;
host;
date = new Date();
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
this.interval = setInterval(() => this.tick(), 1000);
}
tick() {
this.date = new Date();
}
hostDisconnected() {
clearInterval(this.interval);
}
}
Utilizzare il controller
Per utilizzare il controller dell'orologio, importa il controller e aggiorna il componente in index.ts o index.js.
// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock.js';
@customElement('my-element')
class MyElement extends LitElement {
private readonly clock = new ClockController(this); // Instantiate
render() {
// Use controller
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
</div>
`;
}
}
// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';
class MyElement extends LitElement {
clock = new ClockController(this); // Instantiate
render() {
// Use controller
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
</div>
`;
}
}
customElements.define('my-element', MyElement);
Per utilizzare il controller, devi crearlo passando un riferimento all'host del controller (il componente <my-element>), quindi utilizzarlo nel metodo render.
Attivare il rendering nel controller
Nota che viene visualizzata l'ora, ma non viene aggiornata. Questo perché il controller imposta la data ogni secondo, ma l'host non si aggiorna. Questo perché date sta cambiando nella classe ClockController e non più nel componente. Ciò significa che dopo aver impostato date sul controller, l'host deve essere informato di eseguire il ciclo di vita dell'aggiornamento con host.requestUpdate().
// Lit (TS & JS) - clock.ts / clock.js
private tick() {
this.date = new Date();
this.host.requestUpdate();
}
Ora il conto alla rovescia dovrebbe essere iniziato.
Per un confronto più approfondito dei casi d'uso comuni con gli hook, consulta la sezione Argomenti avanzati - Hook.
8. Bambini
In questa sezione imparerai a utilizzare gli slot per gestire i figli in Lit.
Slot e bambini
Gli slot consentono la composizione permettendoti di nidificare i componenti.
In React, i figli vengono ereditati tramite le proprietà. Lo slot predefinito è props.children e la funzione render definisce la posizione dello slot predefinito. Ad esempio:
const MyArticle = (props) => {
return <article>{props.children}</article>;
};
Tieni presente che props.children sono componenti React e non elementi HTML.
In Lit, gli elementi secondari vengono composti nella funzione di rendering con gli elementi slot. Tieni presente che gli elementi secondari non vengono ereditati nello stesso modo di React. In Lit, i figli sono elementi HTML collegati agli slot. Questo allegato è chiamato Proiezione.
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<slot></slot>
</article>
`;
}
}
Più slot
In React, l'aggiunta di più slot è essenzialmente la stessa cosa dell'ereditarietà di più proprietà.
const MyArticle = (props) => {
return (
<article>
<header>
{props.headerChildren}
</header>
<section>
{props.sectionChildren}
</section>
</article>
);
};
Allo stesso modo, l'aggiunta di altri elementi <slot> crea più spazi in Lit. Sono definiti più slot con l'attributo name: <slot name="slot-name">. In questo modo, i bambini possono dichiarare a quale slot verranno assegnati.
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<header>
<slot name="headerChildren"></slot>
</header>
<section>
<slot name="sectionChildren"></slot>
</section>
</article>
`;
}
}
Contenuti dello slot predefiniti
Gli slot mostreranno il loro sottoalbero quando non sono previsti nodi per quello slot. Quando i nodi vengono proiettati in uno slot, questo non visualizzerà il suo sottoalbero, ma i nodi proiettati.
@customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`
<section>
<div>
<slot name="slotWithDefault">
<p>
This message will not be rendered when children are attached to this slot!
<p>
</slot>
</div>
</section>
`;
}
}
Assegnare i bambini agli spazi
In React, i figli vengono assegnati agli slot tramite le proprietà di un componente. Nell'esempio seguente, gli elementi React vengono passati alle proprietà headerChildren e sectionChildren.
const MyNewsArticle = () => {
return (
<MyArticle
headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
sectionChildren={<p>Children are props in React!</p>}
/>
);
};
In Lit, i figli vengono assegnati agli slot utilizzando l'attributo slot.
@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
render() {
return html`
<my-article>
<h3 slot="headerChildren">
Extry, Extry! Read all about it!
</h3>
<p slot="sectionChildren">
Children are composed with slots in Lit!
</p>
</my-article>
`;
}
}
Se non è presente uno slot predefinito (ad es. <slot>) e non è presente uno slot con un attributo name (ad es. <slot name="foo">) che corrisponda all'attributo slot dei nodi secondari dell'elemento personalizzato (ad es. <div slot="foo">), il nodo non verrà proiettato e non verrà visualizzato.
9. Refs
A volte, uno sviluppatore potrebbe dover accedere all'API di un HTMLElement.
In questa sezione, imparerai ad acquisire i riferimenti agli elementi in Lit.
Riferimenti di React
Un componente React viene sottoposto a transpiling in una serie di chiamate di funzioni che creano un DOM virtuale quando vengono richiamate. Questo DOM virtuale viene interpretato da ReactDOM e sottoposto a rendering di HTMLElements.
In React, i riferimenti sono uno spazio in memoria per contenere un HTMLElement generato.
const RefsExample = (props) => {
const inputRef = React.useRef(null);
const onButtonClick = React.useCallback(() => {
inputRef.current?.focus();
}, [inputRef]);
return (
<div>
<input type={"text"} ref={inputRef} />
<br />
<button onClick={onButtonClick}>
Click to focus on the input above!
</button>
</div>
);
};
Nell'esempio precedente, il componente React eseguirà le seguenti operazioni:
- Visualizza un campo di input di testo vuoto e un pulsante con testo
- Metti a fuoco l'input quando viene fatto clic sul pulsante
Dopo il rendering iniziale, React imposterà inputRef.current sul valore HTMLInputElement generato tramite l'attributo ref.
Lit "References" con @query
Lit è integrato nel browser e crea un'astrazione molto sottile sulle funzionalità native del browser.
L'equivalente di refs in Lit è l'HTMLElement restituito dai decoratori @query e @queryAll.
@customElement("my-element")
export class MyElement extends LitElement {
@query('input') // Define the query
inputEl!: HTMLInputElement; // Declare the prop
// Declare the click event listener
onButtonClick() {
// Use the query to focus
this.inputEl.focus();
}
render() {
return html`
<input type="text">
<br />
<!-- Bind the click listener -->
<button @click=${this.onButtonClick}>
Click to focus on the input above!
</button>
`;
}
}
Nell'esempio precedente, il componente Lit esegue le seguenti operazioni:
- Definisce una proprietà su
MyElementutilizzando il decoratore@query(creando un getter per unHTMLInputElement). - Dichiara e collega un callback dell'evento clic chiamato
onButtonClick. - Imposta lo stato attivo dell'input al clic del pulsante
In JavaScript, i decoratori @query e @queryAll eseguono rispettivamente querySelector e querySelectorAll. Questo è l'equivalente JavaScript di @query('input') inputEl!: HTMLInputElement;
get inputEl() {
return this.renderRoot.querySelector('input');
}
Dopo che il componente Lit esegue il commit del modello del metodo render nella radice di my-element, il decoratore @query ora consente a inputEl di restituire il primo elemento input trovato nella radice di rendering. Restituisce null se @query non riesce a trovare l'elemento specificato.
Se nella radice di rendering erano presenti più elementi input, @queryAll restituiva un elenco di nodi.
10. Stato di mediazione
In questa sezione, imparerai come mediare lo stato tra i componenti in Lit.
Componenti riutilizzabili
React imita le pipeline di rendering funzionali con flusso di dati dall'alto verso il basso. I genitori forniscono lo stato ai figli tramite gli oggetti di scena. I bambini comunicano con i genitori tramite i callback trovati nelle proprietà.
const CounterButton = (props) => {
const label = props.step < 0
? `- ${-1 * props.step}`
: `+ ${props.step}`;
return (
<button
onClick={() =>
props.addToCounter(props.step)}>{label}</button>
);
};
Nell'esempio precedente, un componente React esegue le seguenti operazioni:
- Crea un'etichetta in base al valore
props.step. - Visualizza un pulsante con +step o -step come etichetta
- Aggiorna il componente principale chiamando
props.addToCounterconprops.stepcome argomento al clic
Sebbene sia possibile passare callback in Lit, i pattern convenzionali sono diversi. Il componente React nell'esempio precedente potrebbe essere scritto come componente Lit nell'esempio seguente:
@customElement('counter-button')
export class CounterButton extends LitElement {
@property({type: Number}) step: number = 0;
onClick() {
const event = new CustomEvent('update-counter', {
bubbles: true,
detail: {
step: this.step,
}
});
this.dispatchEvent(event);
}
render() {
const label = this.step < 0
? `- ${-1 * this.step}` // "- 1"
: `+ ${this.step}`; // "+ 1"
return html`
<button @click=${this.onClick}>${label}</button>
`;
}
}
Nell'esempio precedente, un componente Lit esegue le seguenti operazioni:
- Crea la proprietà reattiva
step - Invia un evento personalizzato denominato
update-counterche trasporta il valorestepdell'elemento al clic
Gli eventi del browser vengono propagati dagli elementi secondari a quelli principali. Gli eventi consentono ai bambini di trasmettere eventi di interazione e modifiche dello stato. React passa fondamentalmente lo stato nella direzione opposta, quindi è raro che i componenti React inviino e ascoltino eventi nello stesso modo dei componenti Lit.
Componenti stateful
In React, è prassi comune utilizzare gli hook per gestire lo stato. Un componente MyCounter può essere creato riutilizzando il componente CounterButton. Nota come addToCounter viene passato a entrambe le istanze di CounterButton.
const MyCounter = (props) => {
const [counterSum, setCounterSum] = React.useState(0);
const addToCounter = useCallback(
(step) => {
setCounterSum(counterSum + step);
},
[counterSum, setCounterSum]
);
return (
<div>
<h3>Σ: {counterSum}</h3>
<CounterButton
step={-1}
addToCounter={addToCounter} />
<CounterButton
step={1}
addToCounter={addToCounter} />
</div>
);
};
L'esempio riportato sopra esegue le seguenti operazioni:
- Crea uno stato
count. - Crea un callback che aggiunge un numero a uno stato
count. CounterButtonutilizzaaddToCounterper aggiornarecountdistepa ogni clic.
Un'implementazione simile di MyCounter può essere ottenuta in Lit. Nota come addToCounter non viene trasmesso a counter-button. Al contrario, il callback è associato come listener di eventi all'evento @update-counter su un elemento principale.
@customElement("my-counter")
export class MyCounter extends LitElement {
@property({type: Number}) count = 0;
addToCounter(e: CustomEvent<{step: number}>) {
// Get step from detail of event or via @query
this.count += e.detail.step;
}
render() {
return html`
<div @update-counter="${this.addToCounter}">
<h3>Σ ${this.count}</h3>
<counter-button step="-1"></counter-button>
<counter-button step="1"></counter-button>
</div>
`;
}
}
L'esempio riportato sopra esegue le seguenti operazioni:
- Crea una proprietà reattiva chiamata
countche aggiornerà il componente quando il valore viene modificato - Associa il callback
addToCounteral listener di eventi@update-counter - Aggiorna
countaggiungendo il valore trovato indetail.stepdell'eventoupdate-counter - Imposta il valore di
counter-buttondisteptramite l'attributostep
È più convenzionale utilizzare le proprietà reattive in Lit per trasmettere le modifiche dai genitori ai figli. Allo stesso modo, è buona norma utilizzare il sistema di eventi del browser per propagare i dettagli dal basso verso l'alto.
Questo approccio segue le best practice e rispetta l'obiettivo di Lit di fornire supporto cross-platform per i componenti web.
11. Stili
In questa sezione imparerai a utilizzare gli stili in Lit.
Stili
Lit offre diversi modi per applicare stili agli elementi, nonché una soluzione integrata.
Stili in linea
Lit supporta gli stili in linea e il binding.
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
@customElement('my-element')
class MyElement extends LitElement {
render() {
return html`
<div>
<h1 style="color:orange;">This text is orange</h1>
<h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
</div>
`;
}
}
Nell'esempio precedente sono presenti due intestazioni, ognuna con uno stile in linea.
Ora importa e associa un bordo da border-color.js al testo arancione:
...
import borderColor from './border-color.js';
...
html`
...
<h1 style="color:orange;${borderColor}">This text is orange</h1>
...`
Dover calcolare la stringa di stile ogni volta può essere un po' fastidioso, quindi Lit offre una direttiva per aiutarti.
styleMap
La styleMap direttiva semplifica l'utilizzo di JavaScript per impostare gli stili in linea. Ad esempio:
import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';
@customElement('my-element')
class MyElement extends LitElement {
@property({type: String})
color = '#000'
render() {
// Define the styleMap
const headerStyle = styleMap({
'border-color': this.color,
});
return html`
<div>
<h1
style="border-style:solid;
<!-- Use the styleMap -->
border-width:2px;${headerStyle}">
This div has a border color of ${this.color}
</h1>
<input
type="color"
@input=${e => (this.color = e.target.value)}
value="#000">
</div>
`;
}
}
L'esempio riportato sopra esegue le seguenti operazioni:
- Visualizza un
h1con un bordo e un selettore di colori - Modifica
border-colorcon il valore del selettore colori
Inoltre, è presente styleMap, che viene utilizzato per impostare gli stili di h1. styleMap segue una sintassi simile a quella di associazione degli attributi style di React.
CSSResult
Il modo consigliato per applicare lo stile ai componenti è utilizzare il letterale di modello con tag css.
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
const ORANGE = css`orange`;
@customElement('my-element')
class MyElement extends LitElement {
static styles = [
css`
#orange {
color: ${ORANGE};
}
#purple {
color: rebeccapurple;
}
`
];
render() {
return html`
<div>
<h1 id="orange">This text is orange</h1>
<h1 id="purple">This text is rebeccapurple</h1>
</div>
`;
}
}
L'esempio riportato sopra esegue le seguenti operazioni:
- Dichiara un letterale di modello con tag CSS con un binding
- Imposta i colori di due
h1con ID
Vantaggi dell'utilizzo del tag modello css:
- Analizzato una volta per classe anziché per istanza
- Implementato pensando al riutilizzo dei moduli
- Può separare facilmente gli stili in file distinti
- Compatibile con il polyfill delle proprietà CSS personalizzate
Inoltre, tieni presente il tag <style> in index.html:
<!-- index.html -->
<style>
h1 {
color: red !important;
}
</style>
Lit limiterà gli stili dei componenti alle relative radici. Ciò significa che gli stili non verranno inseriti e rimossi. Per trasferire gli stili ai componenti, il team di Lit consiglia di utilizzare le proprietà personalizzate CSS, in quanto possono penetrare l'ambito dello stile Lit.
Tag di stile
È anche possibile inserire semplicemente i tag <style> nei modelli. Il browser deduplica questi tag di stile, ma inserendoli nei modelli, vengono analizzati per istanza di componente anziché per classe, come nel caso del modello con tag css. Inoltre, la deduplicazione dei CSSResult da parte del browser è molto più veloce.
Tag link
L'utilizzo di un <link rel="stylesheet"> nel modello è un'altra possibilità per gli stili, ma anche questa non è consigliata perché potrebbe causare un flash iniziale di contenuti senza stile (FOUC).
12. Argomenti avanzati (facoltativo)
JSX e modelli
Lit & Virtual DOM
Lit-html non include un DOM virtuale convenzionale che differenzia ogni singolo nodo. Utilizza invece le funzionalità di rendimento intrinseche alla specifica tagged template literal di ES2015. I tagged template literal sono stringhe letterali di modello a cui sono associate funzioni tag.
Ecco un esempio di letterale modello:
const str = 'string';
console.log(`This is a template literal ${str}`);
Ecco un esempio di modello letterale con tag:
const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true
Nell'esempio precedente, il tag è la funzione tag e la funzione f restituisce una chiamata di un modello letterale con tag.
Gran parte della magia delle prestazioni in Lit deriva dal fatto che gli array di stringhe passati alla funzione tag hanno lo stesso puntatore (come mostrato nel secondo console.log). Il browser non ricrea un nuovo array strings a ogni chiamata della funzione tag, perché utilizza lo stesso letterale di modello (ovvero nella stessa posizione nell'AST). Pertanto, l'associazione, l'analisi e la memorizzazione nella cache dei modelli di Lit possono sfruttare queste funzionalità senza un overhead di differenziazione in fase di runtime eccessivo.
Questo comportamento integrato del browser dei modelli letterali taggati offre a Lit un vantaggio significativo in termini di prestazioni. La maggior parte dei DOM virtuali convenzionali svolge la maggior parte del lavoro in JavaScript. Tuttavia, i letterali modello taggati eseguono la maggior parte del confronto in C++ del browser.
Se vuoi iniziare a utilizzare i modelli letterali con tag HTML con React o Preact, il team di Lit consiglia la libreria htm.
Tuttavia, come nel caso del sito Google Codelabs e di diversi editor di codice online, noterai che l'evidenziazione della sintassi dei letterali modello con tag non è molto comune. Alcuni IDE ed editor di testo li supportano per impostazione predefinita, ad esempio Atom e lo strumento di evidenziazione dei blocchi di codice di GitHub. Il team di Lit collabora a stretto contatto con la community per gestire progetti come lit-plugin, un plug-in di VS Code che aggiunge l'evidenziazione della sintassi, il controllo dei tipi e l'intellisense ai tuoi progetti Lit.
Lit e JSX + React DOM
JSX non viene eseguito nel browser e utilizza invece un preprocessor per convertire JSX in chiamate di funzioni JavaScript (in genere tramite Babel).
Ad esempio, Babel trasformerà questo codice:
const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);
in questo modo:
const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);
React DOM prende l'output di React e lo traduce in DOM effettivo: proprietà, attributi, listener di eventi e così via.
Lit-html utilizza i modelli letterali con tag che possono essere eseguiti nel browser senza transpilazione o un preprocessor. Ciò significa che per iniziare a utilizzare Lit, tutto ciò che ti serve è un file HTML, uno script del modulo ES e un server. Ecco uno script completamente eseguibile nel browser:
<!DOCTYPE html>
<html>
<head>
<script type="module">
import {html, render} from 'https://cdn.skypack.dev/lit';
render(
html`<div>Hello World!</div>`,
document.querySelector('.root')
)
</script>
</head>
<body>
<div class="root"></div>
</body>
</html>
Inoltre, poiché il sistema di modelli di Lit, lit-html, non utilizza un DOM virtuale convenzionale, ma utilizza direttamente l'API DOM, le dimensioni di Lit 2 sono inferiori a 5 kb dopo la minimizzazione e la compressione Gzip rispetto ai 40 kb di React (2,8 kb) + react-dom (39,4 kb) dopo la minimizzazione e la compressione Gzip.
Eventi
React utilizza un sistema di eventi sintetici. Ciò significa che react-dom deve definire ogni evento che verrà utilizzato in ogni componente e fornire un equivalente di listener di eventi camelCase per ogni tipo di nodo. Di conseguenza, JSX non ha un metodo per definire un listener di eventi per un evento personalizzato e gli sviluppatori devono utilizzare un ref e poi applicare in modo imperativo un listener. Ciò crea un'esperienza di sviluppo non ottimale durante l'integrazione di librerie che non tengono conto di React, il che comporta la necessità di scrivere un wrapper specifico per React.
Lit-html accede direttamente al DOM e utilizza eventi nativi, quindi l'aggiunta di listener di eventi è semplice come @event-name=${eventNameListener}. Ciò significa che viene eseguita un'analisi meno runtime per l'aggiunta di listener di eventi e l'attivazione di eventi.
Componenti e oggetti di scena
Componenti React ed elementi personalizzati
LitElement utilizza elementi personalizzati per raggruppare i suoi componenti. Gli elementi personalizzati introducono alcuni compromessi tra i componenti React in termini di suddivisione in componenti (lo stato e il ciclo di vita sono discussi più avanti nella sezione Stato e ciclo di vita).
Alcuni vantaggi degli elementi personalizzati come sistema di componenti:
- Nativi del browser e non richiedono strumenti
- Adattarsi a ogni API del browser, da
innerHTMLedocument.createElementaquerySelector - In genere possono essere utilizzati in tutti i framework
- Può essere registrato in modo differito con
customElements.definee "idratare" il DOM
Alcuni svantaggi degli elementi personalizzati rispetto ai componenti React:
- Non è possibile creare un elemento personalizzato senza definire una classe (quindi nessun componente funzionale simile a JSX)
- Deve contenere un tag di chiusura
- Nota:nonostante la comodità per gli sviluppatori, i fornitori di browser tendono a non apprezzare la specifica dei tag autochiudenti, motivo per cui le specifiche più recenti tendono a non includerli
- Introduce un nodo aggiuntivo nell'albero DOM, che potrebbe causare problemi di layout
- Deve essere registrato tramite JavaScript
Lit ha scelto gli elementi personalizzati anziché un sistema di elementi personalizzati perché gli elementi personalizzati sono integrati nel browser e il team di Lit ritiene che i vantaggi cross-framework superino quelli forniti da un livello di astrazione dei componenti. Infatti, gli sforzi del team Lit nello spazio lit-ssr hanno superato i principali problemi di registrazione JavaScript. Inoltre, alcune aziende come GitHub sfruttano la registrazione differita degli elementi personalizzati per migliorare progressivamente le pagine con funzionalità opzionali.
Trasferire dati a elementi personalizzati
Un'idea sbagliata comune sugli elementi personalizzati è che i dati possono essere passati solo come stringhe. Questo malinteso deriva probabilmente dal fatto che gli attributi degli elementi possono essere scritti solo come stringhe. Sebbene sia vero che Lit esegue il cast degli attributi stringa nei tipi definiti, gli elementi personalizzati possono accettare anche dati complessi come proprietà.
Ad esempio, data la seguente definizione di LitElement:
// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('data-test')
class DataTest extends LitElement {
@property({type: Number})
num = 0;
@property({attribute: false})
data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}
render() {
return html`
<div>num + 1 = ${this.num + 1}</div>
<div>data.a = ${this.data.a}</div>
<div>data.b = ${this.data.b}</div>
<div>data.c = ${this.data.c}</div>`;
}
}
Viene definita una proprietà reattiva primitiva num che converte il valore stringa di un attributo in un number, quindi viene introdotta una struttura di dati complessa con attribute:false che disattiva la gestione degli attributi di Lit.
Ecco come trasmettere i dati a questo elemento personalizzato:
<head>
<script type="module">
import './data-test.js'; // loads element definition
import {html} from './data-test.js';
const el = document.querySelector('data-test');
el.data = {
a: 5,
b: null,
c: [html`<div>foo</div>`,html`<div>bar</div>`]
};
</script>
</head>
<body>
<data-test num="5"></data-test>
</body>
Stato e ciclo di vita
Altri callback del ciclo di vita di React
static getDerivedStateFromProps
Non esiste un equivalente in Lit, in quanto props e state sono entrambe proprietà della stessa classe
shouldComponentUpdate
- L'equivalente in litri è
shouldUpdate - Chiamato al primo rendering, a differenza di React
- Simile per funzione a
shouldComponentUpdatedi React
getSnapshotBeforeUpdate
In Lit, getSnapshotBeforeUpdate è simile sia a update sia a willUpdate
willUpdate
- Chiamato prima del giorno
update - A differenza di
getSnapshotBeforeUpdate,willUpdateviene chiamato prima dirender - Le modifiche alle proprietà reattive in
willUpdatenon attivano nuovamente il ciclo di aggiornamento - Un buon punto per calcolare i valori delle proprietà che dipendono da altre proprietà e che vengono utilizzati nel resto della procedura di aggiornamento
- Questo metodo viene chiamato sul server in SSR, quindi l'accesso al DOM non è consigliato qui
update
- Chiamata effettuata dopo il giorno
willUpdate - A differenza di
getSnapshotBeforeUpdate,updateviene chiamato prima dirender - Le modifiche alle proprietà reattive in
updatenon attivano nuovamente il ciclo di aggiornamento se vengono modificate prima di chiamaresuper.update - Un buon punto per acquisire informazioni dal DOM che circondano il componente prima che l'output sottoposto a rendering venga eseguito nel DOM
- Questo metodo non viene chiamato sul server in SSR
Altri callback del ciclo di vita di Lit
Nella sezione precedente non sono stati menzionati diversi callback del ciclo di vita perché non hanno un analogo in React. Sono:
attributeChangedCallback
Viene richiamato quando cambia uno dei observedAttributes dell'elemento. observedAttributes e attributeChangedCallback fanno parte della specifica degli elementi personalizzati e sono implementati da Lit per fornire un'API degli attributi per gli elementi Lit.
adoptedCallback
Richiamato quando il componente viene spostato in un nuovo documento, ad esempio da un HTMLTemplateElement's documentFragment al document principale. Questo callback fa parte anche della specifica degli elementi personalizzati e deve essere utilizzato solo per casi d'uso avanzati quando il componente modifica i documenti.
Altri metodi e proprietà del ciclo di vita
Questi metodi e proprietà sono membri della classe che puoi chiamare, sostituire o attendere per manipolare il processo del ciclo di vita.
updateComplete
Si tratta di un Promise che si risolve quando l'elemento ha terminato l'aggiornamento, poiché i cicli di vita di aggiornamento e rendering sono asincroni. Un esempio:
async nextButtonClicked() {
this.step++;
// Wait for the next "step" state to render
await this.updateComplete;
this.dispatchEvent(new Event('step-rendered'));
}
getUpdateComplete
Questo è un metodo che deve essere sostituito per personalizzare la risoluzione di updateComplete. Ciò accade spesso quando un componente esegue il rendering di un componente secondario e i relativi cicli di rendering devono essere sincronizzati. Ad esempio:
class MyElement extends LitElement {
...
async getUpdateComplete() {
await super.getUpdateComplete();
await this.myChild.updateComplete;
}
}
performUpdate
Questo metodo chiama i callback del ciclo di vita dell'aggiornamento. In genere, non è necessario, tranne in rari casi in cui l'aggiornamento deve essere eseguito in modo sincrono o per la pianificazione personalizzata.
hasUpdated
Questa proprietà è true se il componente è stato aggiornato almeno una volta.
isConnected
Questa proprietà, che fa parte della specifica degli elementi personalizzati, sarà true se l'elemento è attualmente collegato all'albero del documento principale.
Visualizzazione del ciclo di vita dell'aggiornamento di Lit
Il ciclo di vita degli aggiornamenti è composto da tre parti:
- Pre-aggiornamento
- Aggiorna
- Post-aggiornamento
Pre-Update

Dopo requestUpdate, è previsto un aggiornamento pianificato.
Aggiorna

Post-aggiornamento

Hook
Perché gli hook
Gli hook sono stati introdotti in React per casi d'uso semplici dei componenti funzionali che richiedevano uno stato. In molti casi semplici, i componenti funzionali con hook tendono a essere molto più semplici e leggibili rispetto alle loro controparti di componenti di classe. Tuttavia, quando si introducono aggiornamenti di stato asincroni e si passano dati tra hook o effetti, il pattern degli hook tende a non essere sufficiente e una soluzione basata su classi come i controller reattivi tende a brillare.
Hook e controller delle richieste API
È comune scrivere un hook che richieda dati da un'API. Ad esempio, prendi questo componente funzionale React che esegue le seguenti operazioni:
index.tsx- Visualizza il testo
- Visualizza la risposta di
useAPI- ID utente + Nome utente
- Messaggio di errore
- 404 quando raggiunge l'utente 11 (per progettazione)
- Errore di interruzione se il recupero dell'API viene interrotto
- Caricamento del messaggio
- Visualizza un pulsante di azione
- Utente successivo: recupera l'API per l'utente successivo
- Annulla: interrompe il recupero dell'API e mostra un errore
useApi.tsx- Definisce un hook personalizzato
useApi - Recupera in modo asincrono un oggetto utente da un'API
- Emette:
- Nome utente
- Indica se il recupero è in corso di caricamento
- Eventuali messaggi di errore
- Un callback per interrompere il recupero
- Interrompe i recuperi in corso se smontato
- Definisce un hook personalizzato
Ecco l'implementazione del controller Lit + Reactive.
Concetti principali:
- I controller reattivi sono molto simili agli hook personalizzati
- Trasferimento di dati non visualizzabili tra callback ed effetti
- React utilizza
useRefper trasferire dati trauseEffecteuseCallback - Lit utilizza una proprietà di classe privata
- React imita essenzialmente il comportamento di una proprietà di classe privata
- React utilizza
Inoltre, se ti piace molto la sintassi dei componenti funzionali React con gli hook, ma preferisci lo stesso ambiente senza build di Lit, il team di Lit consiglia vivamente la libreria Haunted.
Bambini
Slot predefinito
Quando agli elementi HTML non viene assegnato un attributo slot, vengono assegnati allo slot predefinito senza nome. Nell'esempio seguente, MyApp inserisce un paragrafo in uno slot denominato. L'altro paragrafo verrà impostato per impostazione predefinita sullo spazio senza nome".
@customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`
<section>
<div>
<slot></slot>
</div>
<div>
<slot name="custom-slot"></slot>
</div>
</section>
`;
}
}
@customElement("my-app")
export class MyApp extends LitElement {
render() {
return html`
<my-element>
<p slot="custom-slot">
This paragraph will be placed in the custom-slot!
</p>
<p>
This paragraph will be placed in the unnamed default slot!
</p>
</my-element>
`;
}
}
Aggiornamenti degli slot
Quando la struttura dei discendenti degli slot cambia, viene attivato un evento slotchange. Un componente Lit può associare un listener di eventi a un evento slotchange. Nell'esempio seguente, il primo slot trovato in shadowRoot avrà i relativi assignedNodes registrati nella console il giorno slotchange.
@customElement("my-element")
export class MyElement extends LitElement {
onSlotChange(e: Event) {
const slot = this.shadowRoot.querySelector('slot');
console.log(slot.assignedNodes({flatten: true}));
}
render() {
return html`
<section>
<div>
<slot @slotchange="{this.onSlotChange}"></slot>
</div>
</section>
`;
}
}
Refs
Generazione di riferimenti
Lit e React espongono entrambi un riferimento a un HTMLElement dopo che sono state chiamate le funzioni render. Tuttavia, vale la pena esaminare il modo in cui React e Lit compongono il DOM che viene restituito in un secondo momento tramite un decoratore @query di Lit o un riferimento React.
React è una pipeline funzionale che crea componenti React, non elementi HTML. Poiché un Ref viene dichiarato prima del rendering di un HTMLElement, viene allocato uno spazio in memoria. Per questo motivo, null viene visualizzato come valore iniziale di un riferimento, perché l'elemento DOM effettivo non è ancora stato creato (o sottoposto a rendering), ovvero useRef(null).
Dopo che ReactDOM converte un componente React in un HTMLElement, cerca un attributo chiamato ref nel componente React. Se disponibile, ReactDOM inserisce il riferimento all'HTMLElement in ref.current.
LitElement utilizza la funzione di tag di modello html di lit-html per comporre un elemento modello. LitElement stampa i contenuti del modello nello shadow DOM di un elemento personalizzato dopo il rendering. Lo shadow DOM è un albero DOM con ambito definito incapsulato da una radice shadow. Il decoratore @query crea quindi un getter per la proprietà che esegue essenzialmente un this.shadowRoot.querySelector sulla radice con ambito.
Eseguire query su più elementi
Nell'esempio seguente, il decoratore @queryAll restituirà i due paragrafi nella radice shadow come NodeList.
@customElement("my-element")
export class MyElement extends LitElement {
@queryAll('p')
paragraphs!: NodeList;
render() {
return html`
<p>Hello, world!</p>
<p>How are you?</p>
`;
}
}
In sostanza, @queryAll crea un getter per paragraphs che restituisce i risultati di this.shadowRoot.querySelectorAll(). In JavaScript, è possibile dichiarare un getter per svolgere lo stesso scopo:
get paragraphs() {
return this.renderRoot.querySelectorAll('p');
}
Elementi di modifica delle query
Il decoratore @queryAsync è più adatto a gestire un nodo che può cambiare in base allo stato di un'altra proprietà dell'elemento.
Nell'esempio riportato di seguito, @queryAsync troverà il primo elemento paragrafo. Tuttavia, un elemento paragrafo verrà visualizzato solo quando renderParagraph genera in modo casuale un numero dispari. La direttiva @queryAsync restituirà una promessa che verrà risolta quando il primo paragrafo sarà disponibile.
@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
@queryAsync('p')
paragraph!: Promise<HTMLElement>;
renderParagraph() {
const randomNumber = Math.floor(Math.random() * 10)
if (randomNumber % 2 === 0) {
return "";
}
return html`<p>This checkbox is checked!`
}
render() {
return html`
${this.renderParagraph()}
`;
}
}
Stato di mediazione
In React, la convenzione prevede l'utilizzo di callback perché lo stato è mediato da React stesso. React fa del suo meglio per non fare affidamento sullo stato fornito dagli elementi. Il DOM è semplicemente un effetto del processo di rendering.
Stato esterno
È possibile utilizzare Redux, MobX o qualsiasi altra libreria di gestione dello stato insieme a Lit.
I componenti Lit vengono creati nell'ambito del browser. Pertanto, qualsiasi libreria esistente anche nell'ambito del browser è disponibile per Lit. Sono state create molte librerie straordinarie per utilizzare i sistemi di gestione dello stato esistenti in Lit.
Ecco una serie di Vaadin che spiega come utilizzare Redux in un componente Lit.
Dai un'occhiata a lit-mobx di Adobe per scoprire come un sito su larga scala può sfruttare MobX in Lit.
Dai un'occhiata anche ad Apollo Elements per scoprire come gli sviluppatori includono GraphQL nei loro componenti web.
Lit funziona con le funzionalità native del browser e la maggior parte delle soluzioni di gestione dello stato nell'ambito del browser può essere utilizzata in un componente Lit.
Stili
Shadow DOM
Per incapsulare in modo nativo stili e DOM all'interno di un elemento personalizzato, Lit utilizza shadow DOM. Gli Shadow Roots generano un albero ombra separato dall'albero del documento principale. Ciò significa che la maggior parte degli stili è limitata a questo documento. Alcuni stili, come il colore e altri stili correlati ai caratteri, vengono mantenuti.
Il DOM ombra introduce anche nuovi concetti e selettori nella specifica CSS:
:host, :host(:hover), :host([hover]) {
/* Styles the element in which the shadow root is attached to */
}
slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
/*
* Styles the elements projected into a slot element. NOTE: the spec only allows
* styling the direcly slotted elements. Children of those elements are not stylable.
*/
}
Condivisione di stili
Lit semplifica la condivisione degli stili tra i componenti sotto forma di CSSTemplateResults tramite i tag dei modelli css. Ad esempio:
// typography.ts
export const body1 = css`
.body1 {
...
}
`;
// my-el.ts
import {body1} from './typography.ts';
@customElement('my-el')
class MyEl Extends {
static get styles = [
body1,
css`/* local styles come after so they will override bod1 */`
]
render() {
return html`<div class="body1">...</div>`
}
}
Applicazione tema
Le radici ombra rappresentano una sfida per i temi convenzionali, che in genere sono approcci ai tag di stile dall'alto verso il basso. Il modo convenzionale per affrontare la creazione di temi con i componenti web che utilizzano Shadow DOM è esporre un'API di stile tramite le proprietà personalizzate CSS. Ad esempio, questo è un pattern utilizzato da Material Design:
.mdc-textfield-outline {
border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
caret-color: var(--mdc-theme-primary, #...);
}
L'utente modificherebbe quindi il tema del sito applicando valori di proprietà personalizzati:
html {
--mdc-theme-primary: #F00;
}
html[dark] {
--mdc-theme-primary: #F88;
}
Se l'applicazione di temi dall'alto verso il basso è obbligatoria e non riesci a esporre gli stili, è sempre possibile disattivare Shadow DOM eseguendo l'override di createRenderRoot per restituire this, che eseguirà il rendering del modello dei componenti nell'elemento personalizzato stesso anziché in una radice ombra collegata all'elemento personalizzato. In questo modo perderai: incapsulamento degli stili, incapsulamento del DOM e slot.
Produzione
IE 11
Se devi supportare browser meno recenti come IE 11, dovrai caricare alcuni polyfill che occupano circa altri 33 KB. Ulteriori informazioni sono disponibili qui.
Pacchetti condizionali
Il team Lit consiglia di pubblicare due bundle diversi, uno per IE 11 e uno per i browser moderni. Questo approccio offre diversi vantaggi:
- L'utilizzo di ES6 è più veloce e servirà la maggior parte dei tuoi client
- La transpilazione di ES 5 aumenta notevolmente le dimensioni del bundle
- I bundle condizionali ti offrono il meglio di entrambi i mondi
- Supporto di IE 11
- Nessun rallentamento sui browser moderni
Per saperne di più su come creare un bundle pubblicato in modo condizionale, visita il nostro sito di documentazione qui.