Da componente web a elemento luminoso

1. Introduzione

Ultimo aggiornamento: 10/08/2021

Componenti web

I componenti web sono un insieme di API della piattaforma web che ti consentono di creare nuovi tag HTML personalizzati, riutilizzabili e incapsulati da utilizzare in pagine web e app web. I componenti e i widget personalizzati basati sugli standard dei componenti web funzioneranno su tutti i browser moderni e possono essere utilizzati con qualsiasi libreria o framework JavaScript che funzioni con HTML.

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.

Lit fornisce API per semplificare le attività comuni dei componenti web, come la gestione di proprietà, attributi e rendering.

Cosa imparerai a fare

  • Che cos'è un componente web
  • I concetti dei componenti web
  • Come creare un componente web
  • Che cosa sono lit-html e LitElement
  • Cosa fa Lit in aggiunta a un componente web

Cosa creerai

  • Un componente web vaniglia Mi piace / Non mi piace
  • Un componente web basato su Lit con pollice su / giù

Che cosa ti serve

  • Qualsiasi browser moderno aggiornato (Chrome, Safari, Firefox, Chromium Edge). I componenti web funzionano in tutti i browser moderni e sono disponibili polyfill per Microsoft Internet Explorer 11 e Microsoft Edge non basato su Chromium.
  • Conoscenza di HTML, CSS, JavaScript e Chrome DevTools.

2. Configurazione ed esplorazione del Playground

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://unpkg.com/lit?module';

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

La barra delle schede del selettore di file è etichettata come Sezione 1, la sezione di modifica del codice come Sezione 2, l'anteprima dell'output come Sezione 3 e il pulsante di ricarica dell'anteprima come Sezione 4.

Lo screenshot della UI del playground Lit evidenzia le sezioni che utilizzerai in questo codelab.

  1. Selettore file. Prendi nota del pulsante Più…
  2. Editor di file.
  3. Anteprima del codice.
  4. Pulsante Ricarica.
  5. 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, 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
  • Installa un server di sviluppo in grado di risolvere gli identificatori di moduli semplici (il team Lit consiglia @web/dev-server)
  • 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, usa npm run serve

3. Definisci un elemento personalizzato

Elementi personalizzati

I componenti web sono una raccolta di quattro API web native. Sono:

  • Moduli ES
  • Elementi personalizzati
  • Shadow DOM
  • Modelli HTML

Hai già utilizzato la specifica dei moduli ES, che ti consente di creare moduli JavaScript con importazioni ed esportazioni che vengono caricati nella pagina con <script type="module">.

Definizione di un elemento personalizzato

La specifica Elementi personalizzati consente agli utenti di definire i propri elementi HTML utilizzando JavaScript. I nomi devono contenere un trattino (-) per distinguerli dagli elementi del browser nativi. Cancella il file index.js e definisci una classe di elementi personalizzati:

index.js

class RatingElement extends HTMLElement {}

customElements.define('rating-element', RatingElement);

Un elemento personalizzato viene definito associando una classe che estende HTMLElement a un nome tag con trattino. La chiamata a customElements.define indica al browser di associare la classe RatingElement a tagName ‘rating-element'. Ciò significa che ogni elemento del documento con il nome <rating-element> sarà associato a questa classe.

Inserisci un <rating-element> nel corpo del documento e vedi cosa viene visualizzato.

index.html

<body>
 <rating-element></rating-element>
</body>

Ora, guardando l'output, vedrai che non è stato eseguito il rendering di nulla. Questo comportamento è previsto perché non hai indicato al browser come eseguire il rendering di <rating-element>. Puoi verificare che la definizione dell'elemento personalizzato sia riuscita selezionando <rating-element> nel selettore di elementi di Chrome DevTools e chiamando nella console:

$0.constructor

che dovrebbe restituire:

class RatingElement extends HTMLElement {}

Ciclo di vita degli elementi personalizzati

Gli elementi personalizzati sono dotati di un insieme di hook del ciclo di vita. Sono:

  • constructor
  • connectedCallback
  • disconnectedCallback
  • attributeChangedCallback
  • adoptedCallback

constructor viene chiamato quando l'elemento viene creato per la prima volta, ad esempio chiamando document.createElement(‘rating-element') o new RatingElement(). Il costruttore è un buon punto di partenza per configurare l'elemento, ma in genere è considerata una pratica sconsigliata eseguire manipolazioni DOM nel costruttore per motivi di rendimento dell'avvio dell'elemento.

connectedCallback viene chiamato quando l'elemento personalizzato viene collegato al DOM. In genere è qui che avvengono le manipolazioni iniziali del DOM.

disconnectedCallback viene chiamato dopo che l'elemento personalizzato viene rimosso dal DOM.

attributeChangedCallback(attrName, oldValue, newValue) viene chiamato quando viene modificato uno degli attributi specificati dall'utente.

adoptedCallback viene chiamato quando l'elemento personalizzato viene adottato da un altro documentFragment nel documento principale tramite adoptNode, ad esempio in HTMLTemplateElement.

Render DOM

Ora torna all'elemento personalizzato e associa alcuni DOM. Imposta i contenuti dell'elemento quando viene collegato al DOM:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   this.innerHTML = `
     <style>
       rating-element {
         display: inline-flex;
         align-items: center;
       }
       rating-element button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

In constructor, memorizzi una proprietà dell'istanza chiamata rating sull'elemento. In connectedCallback, aggiungi elementi secondari DOM a <rating-element> per visualizzare la valutazione attuale, insieme ai pulsanti Mi piace e Non mi piace.

4. Shadow DOM

Perché shadow DOM?

Nel passaggio precedente, noterai che i selettori nel tag di stile che hai inserito selezionano qualsiasi elemento di valutazione nella pagina, nonché qualsiasi pulsante. Ciò potrebbe comportare la fuoriuscita degli stili dall'elemento e la selezione di altri nodi che potresti non voler stilizzare. Inoltre, altri stili al di fuori di questo elemento personalizzato potrebbero applicare uno stile involontario ai nodi all'interno dell'elemento personalizzato. Ad esempio, prova a inserire un tag di stile nell'intestazione del documento principale:

index.html

<!DOCTYPE html>
<html>
 <head>
   <script src="./index.js" type="module"></script>
   <style>
     span {
       border: 1px solid red;
     }
   </style>
 </head>
 <body>
   <rating-element></rating-element>
 </body>
</html>

L'output deve avere un riquadro con bordo rosso intorno allo span per la valutazione. Si tratta di un caso banale, ma la mancanza di incapsulamento del DOM può causare problemi più grandi per applicazioni più complesse. È qui che entra in gioco lo shadow DOM.

Collegamento di una radice ombra

Collega una radice shadow all'elemento e visualizza il DOM all'interno di questa radice:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});

   shadowRoot.innerHTML = `
     <style>
       :host {
         display: inline-flex;
         align-items: center;
       }
       button {
         background: transparent;
         border: none;
         cursor: pointer;
       }
     </style>
     <button class="thumb_down" >
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
     </button>
     <span class="rating">${this.rating}</span>
     <button class="thumb_up">
       <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
     </button>
   `;
 }
}

customElements.define('rating-element', RatingElement);

Quando aggiorni la pagina, noterai che gli stili nel documento principale non possono più selezionare i nodi all'interno della radice ombra.

Come hai fatto? In connectedCallback hai chiamato this.attachShadow, che collega una radice ombra a un elemento. La modalità open indica che i contenuti shadow sono ispezionabili e che la radice shadow è accessibile anche tramite this.shadowRoot. Dai un'occhiata anche al componente web nello strumento di ispezione di Chrome:

L&#39;albero DOM in Chrome Inspector. È presente un <rating-element> con #shadow-root (open) come elemento secondario e il DOM precedente all&#39;interno di shadowroot.

A questo punto, dovresti vedere un elemento shadow root espandibile che contiene i contenuti. Tutto ciò che si trova all'interno della radice shadow è chiamato Shadow DOM. Se selezioni l'elemento di valutazione in Chrome Dev Tools e chiami $0.children, noterai che non restituisce elementi secondari. Questo perché Shadow DOM non è considerato parte dello stesso albero DOM dei figli diretti, ma piuttosto dell'albero ombra.

Light DOM

Un esperimento: aggiungi un nodo come elemento secondario diretto di <rating-element>:

index.html

<rating-element>
 <div>
   This is the light DOM!
 </div>
</rating-element>

Aggiorna la pagina e vedrai che questo nuovo nodo DOM nel Light DOM di questo elemento personalizzato non viene visualizzato nella pagina. Questo perché Shadow DOM ha funzionalità per controllare il modo in cui i nodi Light DOM vengono proiettati in Shadow DOM tramite gli elementi <slot>.

5. Modelli HTML

Perché utilizzare i modelli

L'utilizzo di innerHTML e stringhe letterali di modelli senza sanificazione può causare problemi di sicurezza con l'iniezione di script. In passato, i metodi includevano l'utilizzo di DocumentFragment, ma anche questi presentano altri problemi, come il caricamento di immagini e l'esecuzione di script quando vengono definiti i modelli, oltre a introdurre ostacoli alla riutilizzabilità. È qui che entra in gioco l'elemento <template>: i modelli forniscono un DOM inerte, un metodo ad alte prestazioni per clonare i nodi e modelli riutilizzabili.

Utilizzare i modelli

Successivamente, esegui la transizione del componente per utilizzare i modelli HTML:

index.html

<body>
 <template id="rating-element-template">
   <style>
     :host {
       display: inline-flex;
       align-items: center;
     }
     button {
       background: transparent;
       border: none;
       cursor: pointer;
     }
   </style>
   <button class="thumb_down" >
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
   </button>
   <span class="rating"></span>
   <button class="thumb_up">
     <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
   </button>
 </template>

 <rating-element>
   <div>
     This is the light DOM!
   </div>
 </rating-element>
</body>

Qui hai spostato i contenuti DOM in un tag modello nel DOM del documento principale. Ora refactorizza la definizione dell'elemento personalizzato:

index.js

class RatingElement extends HTMLElement {
 constructor() {
   super();
   this.rating = 0;
 }
 connectedCallback() {
   const shadowRoot = this.attachShadow({mode: 'open'});
   const templateContent = document.getElementById('rating-element-template').content;
   const clonedContent = templateContent.cloneNode(true);
   shadowRoot.appendChild(clonedContent);

   this.shadowRoot.querySelector('.rating').innerText = this.rating;
 }
}

customElements.define('rating-element', RatingElement);

Per utilizzare questo elemento del modello, esegui una query sul modello, recupera i relativi contenuti e clona i nodi con templateContent.cloneNode, dove l'argomento true esegue una clonazione profonda. Quindi inizializzi il DOM con i dati.

Congratulazioni, ora hai un componente web. Purtroppo non fa ancora nulla, quindi aggiungi alcune funzionalità.

6. Aggiunta di funzionalità

Associazioni di proprietà

Attualmente, l'unico modo per impostare la valutazione sull'elemento di valutazione è costruire l'elemento, impostare la proprietà rating sull'oggetto e poi inserirla nella pagina. Purtroppo, gli elementi HTML nativi non funzionano in questo modo. Gli elementi HTML nativi tendono ad aggiornarsi con le modifiche sia delle proprietà sia degli attributi.

Fai in modo che l'elemento personalizzato aggiorni la visualizzazione quando la proprietà rating cambia aggiungendo le seguenti righe:

index.js

constructor() {
  super();
  this._rating = 0;
}

set rating(value) {
  this._rating = value;
  if (!this.shadowRoot) {
    return;
  }

  const ratingEl = this.shadowRoot.querySelector('.rating');
  if (ratingEl) {
    ratingEl.innerText = this._rating;
  }
}

get rating() {
  return this._rating;
}

Aggiungi un setter e un getter per la proprietà di valutazione, quindi aggiorna il testo dell'elemento di valutazione, se disponibile. Ciò significa che se imposti la proprietà di valutazione sull'elemento, la visualizzazione verrà aggiornata. Prova a eseguire un rapido test nella console degli strumenti di sviluppo.

Associazioni di attributi

Ora aggiorna la visualizzazione quando l'attributo cambia. Questa operazione è simile all'aggiornamento della visualizzazione di un input quando imposti <input value="newValue">. Fortunatamente, il ciclo di vita del componente web include attributeChangedCallback. Aggiorna la classificazione aggiungendo le seguenti righe:

index.js

static get observedAttributes() {
 return ['rating'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
 if (attributeName === 'rating') {
   const newRating = Number(newValue);
   this.rating = newRating;
 }
}

Affinché attributeChangedCallback venga attivato, devi impostare un getter statico per RatingElement.observedAttributes which defines the attributes to be observed for changes. A questo punto, imposta la valutazione in modo dichiarativo nel DOM. Prova:

index.html

<rating-element rating="5"></rating-element>

La classificazione dovrebbe ora essere aggiornata in modo dichiarativo.

Funzionalità dei pulsanti

Ora manca solo la funzionalità del pulsante. Il comportamento di questo componente deve consentire all'utente di fornire una singola valutazione con voto positivo o negativo e fornire un feedback visivo all'utente. Puoi implementare questa funzionalità con alcuni listener di eventi e una proprietà di riflessione, ma prima aggiorna gli stili per fornire un feedback visivo aggiungendo le seguenti righe:

index.html

<style>
...

 :host([vote=up]) .thumb_up {
   fill: green;
 }
  :host([vote=down]) .thumb_down {
   fill: red;
 }
</style>

In Shadow DOM, il selettore :host fa riferimento al nodo o all'elemento personalizzato a cui è collegata la radice shadow. In questo caso, se l'attributo vote è "up", il pulsante Mi piace diventerà verde, ma se vote è "down", then it will turn the thumb-down button red. Ora implementa la logica creando una proprietà / un attributo di riflessione per vote in modo simile a come hai implementato rating. Inizia con il setter e il getter della proprietà:

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }
  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }
  this._vote = newValue;
  this.setAttribute('vote', newValue);
}

get vote() {
  return this._vote;
}

Inizializzi la proprietà dell'istanza _vote con null in constructor e nel setter controlli se il nuovo valore è diverso. In questo caso, devi modificare la valutazione di conseguenza e, soprattutto, restituire l'attributo vote all'host con this.setAttribute.

Successivamente, configura l'associazione degli attributi:

index.js

static get observedAttributes() {
  return ['rating', 'vote'];
}

attributeChangedCallback(attributeName, oldValue, newValue) {
  if (attributeName === 'rating') {
    const newRating = Number(newValue);

    this.rating = newRating;
  } else if (attributeName === 'vote') {
    this.vote = newValue;
  }
}

Anche in questo caso, la procedura è la stessa che hai seguito per l'associazione dell'attributo rating: aggiungi vote a observedAttributes e imposta la proprietà vote in attributeChangedCallback. Infine, aggiungi alcuni listener di eventi di clic per dare funzionalità ai pulsanti.

index.js

constructor() {
 super();
 this._rating = 0;
 this._vote = null;
 this._boundOnUpClick = this._onUpClick.bind(this);
 this._boundOnDownClick = this._onDownClick.bind(this);
}

connectedCallback() {
  ...
  this.shadowRoot.querySelector('.thumb_up')
    .addEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .addEventListener('click', this._boundOnDownClick);
}

disconnectedCallback() {
  this.shadowRoot.querySelector('.thumb_up')
    .removeEventListener('click', this._boundOnUpClick);
  this.shadowRoot.querySelector('.thumb_down')
    .removeEventListener('click', this._boundOnDownClick);
}

_onUpClick() {
  this.vote = 'up';
}

_onDownClick() {
  this.vote = 'down';
}

In constructor associ alcuni listener di clic all'elemento e mantieni i riferimenti. In connectedCallback ascolti gli eventi di clic sui pulsanti. In disconnectedCallback liberi spazio per questi listener e imposti vote in modo appropriato per i click listener stessi.

Congratulazioni, ora hai un componente web completo. Prova a fare clic su alcuni pulsanti. Il problema ora è che il mio file JS raggiunge le 96 righe, il mio file HTML le 43 righe e il codice è piuttosto prolisso e imperativo per un componente così semplice. È qui che entra in gioco il progetto Lit di Google.

7. Lit-html

Code Checkpoint

Perché lit-html

Innanzitutto, il tag <template> è utile e performante, ma non è incluso nella logica del componente, il che rende difficile distribuire il modello con il resto della logica. Inoltre, il modo in cui vengono utilizzati gli elementi del modello si presta intrinsecamente al codice imperativo, che in molti casi porta a un codice meno leggibile rispetto ai pattern di codifica dichiarativa.

È qui che entra in gioco lit-html. Lit HTML è il sistema di rendering di Lit che ti consente di scrivere modelli HTML in JavaScript, quindi di eseguire il rendering e il rendering di questi modelli in modo efficiente insieme ai dati per creare e aggiornare il DOM. È simile alle librerie JSX e VDOM più diffuse, ma viene eseguito in modo nativo nel browser e in molti casi in modo molto più efficiente.

Utilizzo di Lit HTML

Successivamente, esegui la migrazione del componente web nativo rating-element per utilizzare il modello Lit che utilizza i Tagged Template Literal, ovvero funzioni che accettano stringhe di modelli come argomenti con una sintassi speciale. Lit utilizza quindi gli elementi del modello in background per fornire un rendering rapido, nonché alcune funzionalità di sanificazione per la sicurezza. Inizia eseguendo la migrazione di <template> in index.html in un modello Lit aggiungendo un metodo render() al componente web:

index.js

// Dont forget to import from Lit!
import {render, html} from 'lit';

class RatingElement extends HTMLElement {
  ...
  render() {
    if (!this.shadowRoot) {
      return;
    }

    const template = html`
      <style>
        :host {
          display: inline-flex;
          align-items: center;
        }
        button {
          background: transparent;
          border: none;
          cursor: pointer;
        }

       :host([vote=up]) .thumb_up {
         fill: green;
       }

       :host([vote=down]) .thumb_down {
         fill: red;
       }
      </style>
      <button class="thumb_down">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
      </button>
      <span class="rating">${this.rating}</span>
      <button class="thumb_up">
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
      </button>`;

    render(template, this.shadowRoot);
  }
}

Puoi anche eliminare il modello da index.html. In questo metodo di rendering definisci una variabile denominata template e richiami la funzione letterale del modello con tag html. Noterai anche di aver eseguito una semplice associazione di dati all'interno dell'elemento span.rating utilizzando la sintassi di interpolazione dei modelli letterali di ${...}. Ciò significa che alla fine non sarà più necessario aggiornare in modo imperativo quel nodo. Inoltre, chiami il metodo lit render che esegue il rendering sincrono del modello nella radice ombra.

Migrazione alla sintassi dichiarativa

Ora che hai eliminato l'elemento <template>, esegui il refactoring del codice per chiamare invece il metodo render appena definito. Puoi iniziare sfruttando il binding del listener di eventi di lit per cancellare il codice del listener:

index.js

<button
    class="thumb_down"
    @click=${() => {this.vote = 'down'}}>
...
<button
    class="thumb_up"
    @click=${() => {this.vote = 'up'}}>

I modelli Lit possono aggiungere un listener di eventi a un nodo con la sintassi di binding @EVENT_NAME, in cui, in questo caso, aggiorni la proprietà vote ogni volta che viene fatto clic su questi pulsanti.

Successivamente, pulisci il codice di inizializzazione del listener di eventi in constructor, connectedCallback e disconnectedCallback:

index.js

constructor() {
  super();
  this._rating = 0;
  this._vote = null;
}

connectedCallback() {
  this.attachShadow({mode: 'open'});
  this.render();
}

// remove disonnectedCallback and _onUpClick and _onDownClick

Sei riuscito a rimuovere la logica del click listener da tutti e tre i callback e persino a rimuovere completamente disconnectedCallback. Inoltre, hai potuto rimuovere tutto il codice di inizializzazione del DOM da connectedCallback, rendendolo molto più elegante. Ciò significa anche che puoi eliminare i metodi di listener _onUpClick e _onDownClick.

Infine, aggiorna i setter delle proprietà in modo che utilizzino il metodo render, in modo che il DOM possa aggiornarsi quando cambiano le proprietà o gli attributi:

index.js

set rating(value) {
  this._rating = value;
  this.render();
}

...

set vote(newValue) {
  const oldValue = this._vote;
  if (newValue === oldValue) {
    return;
  }

  if (newValue === 'up') {
    if (oldValue === 'down') {
      this.rating += 2;
    } else {
      this.rating += 1;
    }
  } else if (newValue === 'down') {
    if (oldValue === 'up') {
      this.rating -= 2;
    } else {
      this.rating -= 1;
    }
  }

  this._vote = newValue;
  this.setAttribute('vote', newValue);
  // add render method
  this.render();
}

Qui hai potuto rimuovere la logica di aggiornamento del DOM dal setter rating e hai aggiunto una chiamata a render dal setter vote. Ora il modello è molto più leggibile, in quanto puoi vedere dove vengono applicati i binding e i listener di eventi.

Aggiorna la pagina e dovresti avere un pulsante di valutazione funzionante che dovrebbe avere questo aspetto quando viene premuto il voto positivo.

Cursore di valutazione con pollice su e pollice giù con un valore di 6 e il pollice su colorato di verde

8. LitElement

Perché LitElement

Alcuni problemi sono ancora presenti nel codice. Innanzitutto, se modifichi la proprietà o l'attributo vote, potrebbe cambiare la proprietà rating, il che comporterà la chiamata di render due volte. Nonostante le chiamate ripetute di rendering siano essenzialmente un'operazione no-op ed efficiente, la VM JavaScript continua a dedicare tempo a chiamare due volte la funzione in modo sincrono. In secondo luogo, l'aggiunta di nuove proprietà e attributi è noiosa perché richiede molto codice boilerplate. È qui che entra in gioco LitElement.

LitElement è la classe base di Lit per la creazione di componenti web veloci e leggeri che possono essere utilizzati in framework e ambienti diversi. Poi, scopri cosa può fare LitElement per noi in rating-element modificando l'implementazione per utilizzarlo.

Utilizzare LitElement

Inizia importando e creando una sottoclasse della classe base LitElement dal pacchetto lit:

index.js

import {LitElement, html, css} from 'lit';

class RatingElement extends LitElement {
// remove connectedCallback()
...

Importi LitElement, che è la nuova classe base per rating-element. Successivamente, mantieni l'importazione di html e infine css, che ci consente di definire i modelli letterali con tag CSS per la matematica CSS, i modelli e altre funzionalità interne.

Successivamente, sposta gli stili dal metodo di rendering al foglio di stile statico di Lit:

index.js

class RatingElement extends LitElement {
  static get styles() {
    return css`
      :host {
        display: inline-flex;
        align-items: center;
      }
      button {
        background: transparent;
        border: none;
        cursor: pointer;
      }

      :host([vote=up]) .thumb_up {
        fill: green;
      }

      :host([vote=down]) .thumb_down {
        fill: red;
      }
    `;
  }
 ...

È qui che si trovano la maggior parte degli stili in Lit. Lit prenderà questi stili e utilizzerà funzionalità del browser come i fogli di stile costruibili per fornire tempi di rendering più rapidi, oltre a passarli attraverso il polyfill dei componenti web sui browser meno recenti, se necessario.

Ciclo di vita

Lit introduce un insieme di metodi di callback del ciclo di vita del rendering in aggiunta ai callback dei componenti web nativi. Questi callback vengono attivati quando vengono modificate le proprietà Lit dichiarate.

Per utilizzare questa funzionalità, devi dichiarare in modo statico quali proprietà attiveranno il ciclo di vita del rendering.

index.js

static get properties() {
  return {
    rating: {
      type: Number,
    },
    vote: {
      type: String,
      reflect: true,
    }
  };
}

// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()

Qui definisci che rating e vote attiveranno il ciclo di vita di rendering di LitElement, oltre a definire i tipi che verranno utilizzati per convertire gli attributi stringa in proprietà.

<user-profile .name=${this.user.name} .age=${this.user.age}>
  ${this.user.family.map(member => html`
        <family-member
             .name=${member.name}
             .relation=${member.relation}>
        </family-member>`)}
</user-profile>

Inoltre, il flag reflect nella proprietà vote aggiornerà automaticamente l'attributo vote dell'elemento host che hai attivato manualmente nel setter vote.

Ora che hai il blocco delle proprietà statiche, puoi rimuovere tutta la logica di aggiornamento del rendering degli attributi e delle proprietà. Ciò significa che puoi rimuovere i seguenti metodi:

  • connectedCallback
  • observedAttributes
  • attributeChangedCallback
  • rating (setter e getter)
  • vote (setter e getter, ma mantieni la logica di modifica del setter)

Ciò che mantieni è constructor, oltre ad aggiungere un nuovo metodo del ciclo di vita willUpdate:

index.js

constructor() {
  super();
  this.rating = 0;
  this.vote = null;
}

willUpdate(changedProps) {
  if (changedProps.has('vote')) {
    const newValue = this.vote;
    const oldValue = changedProps.get('vote');

    if (newValue === 'up') {
      if (oldValue === 'down') {
        this.rating += 2;
      } else {
        this.rating += 1;
      }
    } else if (newValue === 'down') {
      if (oldValue === 'up') {
        this.rating -= 2;
      } else {
        this.rating -= 1;
      }
    }
  }
}

// remove set vote() and get vote()

Qui inizializzi semplicemente rating e vote e sposti la logica del setter vote nel metodo del ciclo di vita willUpdate. Il metodo willUpdate viene chiamato prima di render ogni volta che viene modificata una proprietà di aggiornamento, perché LitElement raggruppa le modifiche alle proprietà e rende asincrona la visualizzazione. Le modifiche alle proprietà reattive (come this.rating) in willUpdate non attiveranno chiamate del ciclo di vita render non necessarie.

Infine, render è un metodo del ciclo di vita di LitElement che richiede di restituire un modello Lit:

index.js

render() {
  return html`
    <button
        class="thumb_down"
        @click=${() => {this.vote = 'down'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
    </button>
    <span class="rating">${this.rating}</span>
    <button
        class="thumb_up"
        @click=${() => {this.vote = 'up'}}>
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
    </button>`;
}

Non devi più controllare la radice ombra e non devi più chiamare la funzione render importata in precedenza dal pacchetto 'lit'.

L'elemento dovrebbe ora essere visualizzato nell'anteprima. Fai clic.

9. Complimenti

Congratulazioni, hai creato un componente web da zero e lo hai trasformato in un LitElement.

Lit è molto piccolo (< 5 kB minimizzati + compressi con Gzip), molto veloce e davvero divertente da usare per la programmazione. Puoi creare componenti da utilizzare con altri framework o creare app complete.

Ora sai cos'è un componente web, come crearne uno e come Lit semplifica la creazione.

Code Checkpoint

Vuoi confrontare il tuo codice finale con il nostro? Confrontalo qui.

Passaggi successivi

Dai un'occhiata agli altri codelab.

Further reading

Community