Lit pour les développeurs React

Qu'est-ce que Lit ?

Lit est un ensemble de bibliothèques Open Source créées par Google, qui aide les développeurs à concevoir des composants légers et rapides qui fonctionnent dans n'importe quel framework. Il vous permet de créer, entre autres, des applications, des systèmes de conception et des composants partageables.

Ce que vous allez apprendre

Comment convertir les concepts de React ci-dessous dans Lit :

  • JSX et création de modèles
  • Composants et props
  • État et cycle de vie
  • Hooks
  • Enfants
  • Refs
  • État de médiation

Ce que vous allez faire

À la fin de cet atelier de programmation, vous saurez adapter les concepts de composants React à ceux similaires dans Lit.

Ce dont vous avez besoin

  • Dernière version de Chrome, Safari, Firefox ou Edge.
  • Connaissances des langages HTML, CSS, JavaScript et des Outils pour les développeurs Chrome.
  • Connaissances sur la bibliothèque React.
  • (Niveau avancé) Pour une expérience de développement optimale, téléchargez VS Code. Vous aurez aussi besoin du plug-in lit pour VS Code et npm.

Les concepts et capacités de base de Lit sont semblables à ceux de React, à bien des égards, mais Lit présente aussi des différences et facteurs de différenciation non négligeables :

Peu volumineux

Comparé aux plus de 40 ko de React+ReactDOM, Lit ne fait environ que 5 ko une fois compressé au format .gz.

Graphique à barres illustrant la taille en ko après compression. La barre pour Lit indique 5 ko et celle pour React+ReactDOM, 42,2 ko

Plus rapide

Selon des analyses comparatives publiques entre lit-html (le système de création de modèles de Lit) et le DOM virtuel de React, lit-html est 8 à 10 % plus rapide que React dans le pire des cas et plus de 50 % plus rapide dans les cas d'utilisation les plus courants.

LitElement (la classe de base des composants de Lit) ajoute une surcharge minimale à lit-html, mais est 16 à 30 % plus performant que React si l'on compare les fonctionnalités des composants, comme l'utilisation de mémoire, le temps de démarrage, et la date et l'heure de l'interaction.

Graphique à barres groupées illustrant les performances en millisecondes entre Lit et React (plus la valeur est faible, mieux c'est)

Aucun compilation requise

Avec les nouvelles fonctionnalités du navigateur, comme les modules ES et les littéraux de modèles balisés, Lit n'exige aucune compilation pour s'exécuter. Autrement dit, les environnements de développement peuvent être configurés avec une balise de script, un navigateur et un serveur. Voilà juste ce dont vous avez besoin pour être opérationnel.

Grâce aux modules ES et aux CDN récents tels que Skypack ou UNPKG, vous n'aurez peut-être pas besoin de npm pour démarrer.

Toutefois, si vous le souhaitez, vous pouvez quand même compiler et optimiser le code Lit. La récente consolidation des développeurs concernant les modules ES natifs a été bénéfique pour Lit : Lit est seulement un code JavaScript normal qui ne nécessite pas de CLI spécifiques au framework ni de gérer les compilations.

Compatibilité avec plusieurs frameworks

Les composants de Lit s'appuient sur un ensemble de normes Web, appelées "Web Components". Autrement dit, la création d'un composant dans Lit fonctionnera dans les frameworks actuels et futurs. Et si les éléments HTML sont acceptés, les Web Components le sont également.

Le seul problème avec l'interopérabilité des frameworks est lorsque leur compatibilité avec le DOM est limitée. Bien que React soit l'un de ces frameworks, il propose aux développeurs des attributs ref (ou "refs") qui font office de "trappes de secours" sans être l'idéal.

L'équipe Lit a travaillé sur un projet expérimental appelé @lit-labs/react, qui analyse automatiquement vos composants Lit et génère un wrapper React afin que vous n'ayez pas à utiliser de refs.

Un rapport Custom Elements Everywhere vous indique également les frameworks et bibliothèques compatibles avec des éléments personnalisés.

TypeScript comme langage de programmation de première classe

Même si vous pouvez écrire tout votre code Lit en JavaScript, Lit est écrit en TypeScript (que l'équipe Lit recommande d'ailleurs aux développeurs).

L'équipe Lit a collaboré avec la communauté Lit pour aider à maintenir les projets qui intègrent la validation de types et la saisie semi-automatique du code (IntelliSense) dans TypeScript aux modèles Lit lors des phases de développement et de compilation avec lit-analyzer et lit-plugin.

Capture d'écran d'un IDE illustrant une erreur de validation de type liée à la définition du booléen encadré en nombre

Capture d'écran d'un IDE illustrant des suggestions de saisie semi-automatique de code

Outils pour les développeurs Chrome intégrés au navigateur

Les composants Lit sont de simples éléments HTML dans le DOM. Ainsi, pour inspecter vos composants, vous n'avez pas besoin d'installer d'outils ni d'extensions pour votre navigateur.

Il vous suffit d'accéder aux outils pour les développeurs, de sélectionner un élément et d'explorer ses propriétés ou son état.

, que $0.value renvoie hello world, que $0.outlined renvoie true, et que {$0} affiche l'expansion de la propriété" class="l10n-relative-url-src" l10n-attrs-original-order="alt,src,class" src="https://codelabs.developers.google.com/codelabs/lit-2-for-react-devs/./img/browser-tools.png" />

Rendu côté serveur

Lit 2 a été conçu pour prendre en charge le rendu côté serveur. Au moment de la rédaction de cet atelier de programmation, l'équipe Lit n'a pas encore publié les outils de rendu côté serveur sous une forme stable. Toutefois, elle a déjà déployé des composants rendus côté serveur dans l'ensemble des produits Google. Elle compte aussi publier prochainement ces outils en externe sur GitHub.

En attendant, vous pouvez suivre les avancées de l'équipe Lit en cliquant ici.

Flexibilité

Lit n'impose aucun grand engagement. Vous pouvez créer des composants dans Lit et les ajouter à votre projet actuel. S'ils ne vous conviennent pas, vous n'avez pas besoin de convertir tout de suite l'application entière, car les composants Web fonctionnent dans d'autres frameworks.

Vous avez développé une application complète dans Lit et vous voulez passer à autre chose ? Vous pouvez la placer dans votre nouveau framework et migrer tout ce que vous voulez vers les composants de ce framework.

En outre, de nombreux frameworks récents prennent en charge les résultats dans les composants Web, ce qui signifie qu'ils peuvent généralement s'intégrer eux-mêmes à un élément Lit.

Vous pouvez suivre cet atelier de programmation de deux façons :

  • Entièrement en ligne, dans votre navigateur
  • (Niveau avancé) Sur votre ordinateur local à l'aide de VS Code

Accéder au code

Tout au long de cet atelier de programmation, vous trouverez des liens vers le terrain de jeu Lit, comme suit :

Point de contrôle du code

Le terrain de jeu est un bac à sable de code qui s'exécute complètement dans votre navigateur. Il peut compiler et exécuter des fichiers TypeScript et JavaScript, et aussi résoudre automatiquement les importations dans les modules de nœud. Par exemple :

// before
import './my-file.js';
import 'lit';

// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';

Vous pouvez suivre l'intégralité du tutoriel dans le terrain de jeu Lit, en utilisant ces points de contrôle comme points de départ. Si vous utilisez VS Code, vous pouvez vous servir de ces points de contrôle pour télécharger le code de départ de n'importe quelle étape ou pour vérifier votre travail.

Explorer l'UI du terrain de jeu Lit

La section 1 correspond à la barre d'onglets du sélecteur de fichiers, la section 2 à l'éditeur de code, la section 3 à l'aperçu du résultat et la section 4 au bouton d'actualisation de l'aperçu.

La capture d'écran de l'UI du terrain de jeu Lit montre les sections que vous allez utiliser dans cet atelier de programmation.

  1. Sélecteur de fichiers (notez la présence du bouton Plus)
  2. Éditeur de fichier
  3. Aperçu du code
  4. Bouton d'actualisation
  5. Bouton de téléchargement

Configuration de VS Code (niveau avancé)

Voici les avantages que présente cette configuration de VS Code :

  • Validation du type de modèle
  • Saisie semi-automatique du code du modèle (IntelliSense)

Si npm et VS Code (avec le plug-in lit) sont déjà installés et que vous savez comment utiliser cet environnement, vous pouvez simplement télécharger et lancer ces projets comme suit :

  • Appuyez sur le bouton de téléchargement.
  • Extrayez le contenu du fichier .tar dans un répertoire.
  • (Si TypeScript) Configurez rapidement un fichier tsconfig qui génère des modules ES et ES2015+.
  • Installez un serveur de développement capable de résoudre des spécificateurs de module nus (l'équipe Lit recommande @web/dev-server).
  • Exécutez ce serveur et ouvrez votre navigateur (si vous employez @web/dev-server, vous pouvez utiliser web-dev-server --node-resolve --watch --open).

Dans cette section, vous allez découvrir les bases de la création de modèles dans Lit.

JSX et modèles Lit

JSX est une extension syntaxique de JavaScript, qui permet aux utilisateurs de React d'écrire facilement des modèles dans leur code JavaScript. Les modèles Lit ont un objectif similaire : exprimer l'UI d'un composant en tant que fonction de son état.

Syntaxe de base

Point de contrôle du code

Dans React, vous afficheriez un Hello World JSX comme suit :

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
);

Dans l'exemple ci-dessus, il y a deux éléments et une variable "name" incluse. Dans Lit, vous procéderiez comme suit :

import {html, render} from 'lit';

const name = 'Josh Perez';
const element = html`
  <h1>Hello, ${name}</h1>
  <div>How are you?</div>`;

render(
  element,
  mountNode
);

Notez que les modèles Lit n'ont pas besoin de composant React.Fragment pour regrouper plusieurs éléments dans ses modèles.

Dans Lit, les modèles sont encapsulés avec un LITtéral de modèle balisé html (d'où le nom "Lit").

Valeurs des modèles

Les modèles Lit peuvent accepter d'autres modèles Lit, appelés TemplateResult. Par exemple, encapsulez name dans des balises mise en italique (<i>) et encapsulez-le avec un littéral de modèle balisé N.B. Veillez à utiliser l'accent grave (`) et non le guillemet simple (').

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
);

Dans Lit, les classes TemplateResult peuvent accepter des tableaux, des chaînes, d'autres classes TemplateResult, ainsi que des directives.

Point de contrôle du code

Pour faire un test, essayez de convertir le code React suivant dans 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
);

Réponse :

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
);

Transmission et définition de props

Point de contrôle du code

L'une des principales différences entre les syntaxes JSX et Lit est la syntaxe de liaison de données. Prenons l'exemple de cette entrée React avec liaisons :

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
);

Dans l'exemple ci-dessus, une entrée effectue ce qui suit :

  • Elle désactive une variable (dans ce cas, en la définissant sur "false").
  • Elle définit la classe sur static-class, ainsi qu'une variable (dans ce cas, "static-class my-class").
  • Elle définit une valeur par défaut.

Dans Lit, vous procéderiez comme suit :

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
);

Dans l'exemple Lit, une liaison booléenne est ajoutée pour activer/désactiver l'attribut disabled.

Ensuite, une liaison directe est effectuée avec l'attribut class au lieu de className. Vous pouvez ajouter plusieurs liaisons à l'attribut class, sauf si vous utilisez la directive classMap (une aide déclarative pour activer/désactiver des classes).

Enfin, la propriété value est définie sur l'entrée. Contrairement à React, l'élément d'entrée ne sera pas en lecture seule, car il suit l'implémentation et le comportement natifs de l'entrée.

Syntaxe de liaison de props dans Lit

html`<my-element ?attribute-name=${booleanVar}>`;
  • Le préfixe ? correspond à la syntaxe de liaison pour activer/désactiver un attribut sur un élément.
  • Équivalent à inputRef.toggleAttribute('attribute-name', booleanVar).
  • Utile pour les éléments qui utilisent disabled, car disabled="false" est toujours lu comme "true" par le DOM en raison de inputElement.hasAttribute('disabled') === true.
html`<my-element .property-name=${anyVar}>`;
  • Le préfixe . correspond à la syntaxe de liaison pour définir une propriété d'un élément.
  • Équivalent à inputRef.propertyName = anyVar.
  • Idéal pour transmettre des données complexes, comme des objets, tableaux ou classes.
html`<my-element attribute-name=${stringVar}>`;
  • Liaison à l'attribut d'un élément.
  • Équivalent à inputRef.setAttribute('attribute-name', stringVar).
  • Idéal pour les valeurs de base, les sélecteurs de règles de style et les querySelectors.

Transmission de gestionnaires

Point de contrôle du code

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
);

Dans l'exemple ci-dessus, une entrée effectue ce qui suit :

  • Consignation du mot "clic" quand l'utilisateur clique sur l'entrée
  • Consignation de la valeur de l'entrée quand l'utilisateur saisit un caractère

Dans Lit, vous procéderiez comme suit :

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
);

Dans l'exemple Lit, un écouteur est ajouté à l'événement click avec @click.

Ensuite, au lieu d'utiliser onChange, il y a une liaison à l'événement input natif de <input>, car l'événement change natif se déclenche uniquement sur blur (React résume ces événements).

Syntaxe du gestionnaire d'événements dans Lit

html`<my-element @event-name=${() => {...}}></my-element>`;
  • Le préfixe @ correspond à la syntaxe de liaison d'un écouteur d'événements.
  • Équivalent à inputRef.addEventListener('event-name', ...).
  • Utilise le nom des événements DOM natifs.

Dans cette section, vous allez étudier les composants à base de classes et les fonctions composants Lit. L'état et les hooks sont traités plus en détail dans les sections suivantes.

Composants à base de classes et LitElement

Point de contrôle du code (TypeScript)Point de contrôle du code (JavaScript)

LitElement est l'équivalent dans Lit d'un composant à base de classe React, et le concept de "propriétés réactives" de Lit combine l'état et les props de React. Exemple :

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
);

Dans l'exemple ci-dessus, un composant React effectue ce qui suit :

  • Il affiche un name.
  • Il définit la valeur par défaut de name sur une chaîne vide ("").
  • Il réaffecte name à "Elliott".

Voici comment vous feriez cela dans LitElement.

Dans 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>`
  }
}

Dans 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);

Et dans le fichier HTML :

<!-- index.html -->
<head>
  <script type="module" src="./index.js"></script>
</head>
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>

Examen de ce qui se passe dans l'exemple ci-dessus :

@property({type: String})
name = '';
  • Définit une propriété réactive publique (une partie de l'API publique de votre composant)
  • Affiche un attribut (par défaut) ainsi qu'une propriété sur votre composant
  • Indique comment convertir l'attribut du composant (chaînes) en une valeur
static get properties() {
  return {
    name: {type: String}
  }
}
  • Cette propriété remplit la même fonction que le décorateur TypeScript @property, mais s'exécute en mode natif dans JavaScript.
render() {
  return html`<h1>Hello, ${this.name}</h1>`
}
  • Ceci est appelé chaque fois qu'une propriété réactive est modifiée.
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
  ...
}
  • Un nom de balise d'un élément HTML est ainsi associé à une définition de classe.
  • En raison des règles liées aux éléments personnalisés, le nom de balise doit inclure un tiret (-).
  • this dans un LitElement fait référence à l'instance de l'élément personnalisé (<welcome-banner>, dans ce cas).
customElements.define('welcome-banner', WelcomeBanner);
  • Il s'agit de l'équivalent JavaScript du décorateur TypeScript @customElement.
<head>
  <script type="module" src="./index.js"></script>
</head>
  • La définition de l'élément personnalisé est importée.
<body>
  <welcome-banner name="Elliott"></welcome-banner>
</body>
  • L'élément personnalisé est ajouté à la page.
  • La propriété name est définie sur 'Elliott'.

Fonctions composants

Point de contrôle du code

Lit n'a pas une interprétation individuelle d'une fonction composant, car il n'utilise ni JSX, ni un préprocesseur. Toutefois, il est assez simple de composer une fonction qui utilise des propriétés et affiche un DOM en fonction de ces propriétés. Exemple :

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Elliott"/>
ReactDOM.render(
  element,
  mountNode
);

Voici comment cela serait dans Lit :

import {html, render} from 'lit';

function Welcome(props) {
  return html`<h1>Hello, ${props.name}</h1>`;
}

render(
  Welcome({name: 'Elliott'}),
  document.body.querySelector('#root')
);

Dans cette section, vous allez étudier l'état et le cycle de vie dans Lit.

État

Le concept de "propriétés réactives" dans Lit allie l'état et les props de React. Lorsqu'elles sont modifiées, ces propriétés peuvent déclencher le cycle de vie du composant. Elles se déclinent en deux variantes :

Propriétés réactives publiques

// 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';
}
  • Propriété définie par @property
  • Semblable aux props et à l'état dans React (mais modifiable)
  • API publique accessible et définie par les utilisateurs du composant

État réactif interne

// 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';
}
  • Propriété définie par @state
  • Semblable à l'état dans React (mais modifiable)
  • État interne privé accessible généralement depuis le composant ou les sous-classes

Cycle de vie

Le cycle de vie dans Lit est assez semblable à celui dans React. Toutefois, il y a quelques différences notables.

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';
  }
}
  • Équivalent dans Lit : constructor
  • Inutile de transmettre quelque chose au super appel
  • Appelé par (pas totalement inclusif) :
    • document.createElement
    • document.innerHTML
    • new ComponentClass()
    • Si un nom de balise non mis à jour figure sur la page, et que la définition est chargée et enregistrée auprès de @customElement ou de customElements.define
  • Fonction semblable à constructor dans React

render

// React
render() {
  return <div>Hello World</div>
}

// Lit
render() {
  return html`<div>Hello World</div>`;
}
  • Équivalent dans Lit : render
  • Peut renvoyer n'importe quel résultat affichable (ex. : TemplateResult, string, etc.)
  • Semblable à celle dans React, render() doit être une fonction pure
  • Rendu à n'importe quel nœud renvoyé par createRenderRoot() (ShadowRoot par défaut)

componentDidMount

componentDidMount est semblable à une combinaison des rappels de cycle de vie firstUpdated et connectedCallback dans Lit.

firstUpdated

import Chart from 'chart.js';

// React
componentDidMount() {
  this._chart = new Chart(this.chartElRef.current, {...});
}

// Lit
firstUpdated() {
  this._chart = new Chart(this.chartEl, {...});
}
  • Appelé la première fois que le modèle du composant est affiché dans la racine du composant
  • Ne sera appelé que si l'élément est connecté (par exemple, non appelé via document.createElement('my-component') tant que ce nœud n'est pas ajouté à l'arborescence DOM)
  • Bon endroit pour la configuration du composant qui nécessite que le DOM soit affiché par le composant
  • Contrairement à componentDidMount dans React, les modifications apportées aux propriétés réactives dans firstUpdated entraînent un nouveau rendu, bien que le navigateur regroupe généralement les modifications dans le même frame. Si ces modifications n'exigent pas d'accéder au DOM de la racine, elles doivent généralement figurer dans willUpdate

connectedCallback

// React
componentDidMount() {
  this.window.addEventListener('resize', this.boundOnResize);
}

// Lit
connectedCallback() {
  super.connectedCallback();
  this.window.addEventListener('resize', this.boundOnResize);
}
  • Appelé chaque fois que l'élément personnalisé est inséré dans l'arborescence DOM
  • Contrairement aux composants React, lorsque des éléments personnalisés sont dissociés du DOM, ils ne sont pas détruits et ils peuvent ainsi être "connectés" plusieurs fois
  • Utile pour réinitialiser le DOM ou associer de nouveau des écouteurs d'événements qui ont été effacés lors de la déconnexion
  • Remarque : connectedCallback peut être appelé avant firstUpdated. Ainsi, lors du premier appel, le DOM peut ne pas être disponible

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);
  }
}
  • Équivalent dans Lit : updated (avec "update" au passé en anglais)
  • Contrairement à React, updated est également appelé lors du rendu initial
  • Fonction semblable à componentDidUpdate dans React

componentWillUnmount

// React
componentWillUnmount() {
  this.window.removeEventListener('resize', this.boundOnResize);
}

// Lit
disconnectedCallback() {
  super.disconnectedCallback();
  this.window.removeEventListener('resize', this.boundOnResize);
}
  • Équivalent dans Lit semblable à disconnectedCallback.
  • Contrairement aux composants React, si les éléments personnalisés sont dissociés du DOM, le composant n'est pas détruit.
  • Contrairement à componentWillUnmount, disconnectedCallback est appelé après que l'élément est supprimé de l'arborescence.
  • Le DOM à l'intérieur de la racine est quand même associé à la sous-arborescence de la racine.
  • Utile pour effacer les écouteurs d'événements et les références qui fuient afin que le navigateur puisse récupérer le composant.

Exercice

Point de contrôle du code (TypeScript)Point de contrôle du code (JavaScript)

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')
);

Dans l'exemple ci-dessus, une horloge simple effectue ce qui suit :

  • Elle affiche "Hello World ! Il est…", suivi de l'heure.
  • L'horloge est mise à jour toutes les secondes.
  • Lorsque le composant est démonté, l'intervalle qui appelle la méthode tick est effacé.

Commencez par la déclaration de la classe du composant :

// 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);

Ensuite, initialisez la date et déclarez-la comme propriété réactive interne avec @state, car les utilisateurs du composant ne définiront pas directement la date.

// 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);

Affichez ensuite le modèle.

// Lit (JS & TS)
render() {
  return html`
    <div>
      <h1>Hello, World!</h1>
      <h2>It is ${this.date.toLocaleTimeString()}.</h2>
    </div>
  `;
}

Implémentez maintenant la méthode tick.

tick() {
  this.date = new Date();
}

Vient ensuite l'implémentation de componentDidMount. Encore une fois, l'équivalent dans Lit combine firstUpdated et connectedCallback. Dans le cas de ce composant, il n'est pas nécessaire d'accéder au DOM à la racine pour appeler la méthode tick avec setInterval. De plus, l'intervalle est effacé une fois l'élément supprimé de l'arborescence de documents. Par conséquent, si vous l'associez de nouveau, l'intervalle devra redémarrer. Il est donc préférable d'utiliser connectedCallback.

// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
  @state()
  private date = new Date();
  private timerId = -1; // initialize timerId for TS

  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
  );
}

Enfin, effacez l'intervalle de sorte qu'il n'exécute pas la méthode tick une fois l'élément détaché de l'arborescence de documents.

// Lit (TS & JS)
disconnectedCallback() {
  super.disconnectedCallback();
  clearInterval(this.timerId);
}

En regroupant tout, cela devrait se présenter comme suit :

// 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;

  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);

Dans cette section, vous allez apprendre à convertir les concepts de hooks React dans Lit.

Concepts des hooks React

Dans React, les hooks permettent aux fonctions composants de "s'accrocher" à un état. Ils présentent plusieurs avantages.

  • Ils permettent de réutiliser facilement la logique avec état.
  • Ils aident à diviser un composant en fonctions plus petites.

De plus, l'accent mis sur les fonctions composants a permis de résoudre certains problèmes liés à la syntaxe basée sur les classes dans React, parmi lesquels :

  • La nécessité de transmettre des props entre le constructor et super
  • L'initialisation désordonnée des propriétés dans le constructor
    • C'était une raison invoquée par l'équipe React à l'époque, mais le problème a été résolu avec ES2019
  • Les problèmes causés par this ne font plus référence au composant

Concepts des hooks React dans Lit

Comme indiqué dans la section Composants et props, Lit ne permet pas de créer des éléments personnalisés à partir d'une fonction. Toutefois, LitElement résout la plupart des principaux problèmes liés aux composants à base de classes React. Exemple :

// 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++;
  }
}

Comment est-ce que Lit gère ces problèmes ?

  • Le constructor n'accepte aucun argument.
  • Toutes les liaisons @event sont automatiquement associées à this.
  • Dans la grande majorité des cas, this renvoie à la référence de l'élément personnalisé.
  • Les propriétés de classe peuvent maintenant être instanciées en tant que membres de classe. Cela efface les implémentations basées sur le constructeur.

Contrôleurs réactifs

Point de contrôle du code (TypeScript)Point de contrôle du code (JavaScript)

Les principaux concepts des hooks existent dans Lit sous la forme de contrôleurs réactifs. Les schémas de contrôleurs réactifs permettent de partager une logique avec état, de diviser les composants en fragments plus petits et plus modulaires, et d'intégrer le cycle de vie de la mise à jour d'un élément.

Un contrôleur réactif est une interface d'objet qui peut intégrer le cycle de vie de la mise à jour d'un hôte contrôleur tel que LitElement.

Le cycle de vie d'un ReactiveController et d'un reactiveControllerHost est le suivant :

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>;
}

En construisant un contrôleur réactif et en l'associant à un hôte avec addController, le cycle de vie du contrôleur est appelé en même temps que celui de l'hôte. Par exemple, reprenez l'exemple d'horloge de la section État et cycle de vie :

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')
);

Dans l'exemple ci-dessus, une horloge simple effectue ce qui suit :

  • Elle affiche "Hello World ! Il est…", suivi de l'heure.
  • L'horloge est mise à jour toutes les secondes.
  • Lorsque le composant est démonté, l'intervalle qui appelle la méthode tick est effacé.

Créer l'échafaudage du composant

Commencez par la déclaration de la classe du composant et ajoutez la fonction 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);

Créer le contrôleur

Passez maintenant à clock.ts, créez une classe pour le ClockController et configurez le 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() {
  }

  // Will not be used but needed for TS compilation
  hostUpdate() {};
  hostUpdated() {};
}

// Lit (JS) - clock.js
export class ClockController {
  constructor(host) {
    this.host = host;
    host.addController(this);
  }

  hostConnected() {
  }

  tick() {
  }

  hostDisconnected() {
  }
}

Un contrôleur réactif peut être construit de n'importe quelle manière tant qu'il partage l'interface du ReactiveController. Toutefois, l'équipe Lit préfère utiliser, dans la plupart des cas de base, un schéma qui consiste à utiliser une classe avec un constructor capable d'assimiler l'interface d'un hôte ReactiveControllerHost, ainsi que toute autre propriété nécessaire à l'initialisation du contrôleur.

Vous devez maintenant convertir les rappels de cycle de vie dans React en rappels du contrôleur. En bref :

  • componentDidMount
    • En rappel connectedCallback de LitElement
    • En rappel hostConnected du contrôleur
  • ComponentWillUnmount
    • En rappel disconnectedCallback de LitElement
    • En rappel hostDisconnected du contrôleur

Pour en savoir plus sur la conversion du cycle de vie dans React en cycle de vie dans Lit, consultez la section État et cycle de vie.

Implémentez ensuite le rappel hostConnected et les méthodes tick, puis effacez l'intervalle dans hostDisconnected, comme indiqué dans l'exemple de la section État et cycle de vie.

// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
  private readonly host: ReactiveControllerHost;
  private interval = 0;
  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);
  }

  hostUpdate() {};
  hostUpdated() {};
}

// 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);
  }
}

Utiliser le contrôleur

Pour utiliser le contrôleur d'horloge, importez-le, puis mettez à jour le composant dans index.ts ou 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);

Pour utiliser le contrôleur, vous devez l'instancier en transmettant une référence à l'hôte du contrôleur (qui est le composant <my-element>), puis utiliser le contrôleur dans la méthode render.

Déclencher un nouveau rendu dans le contrôleur

Notez que l'heure sera affichée, mais qu'elle ne se met pas à jour. Cela est dû au fait que le contrôleur définit la date toutes les secondes, mais que l'hôte ne se met pas à jour. En fait, la date change dans la classe ClockController, mais plus le composant. Autrement dit, une fois que la date est définie sur le contrôleur, l'hôte doit être invité à exécuter son cycle de vie de mise à jour avec host.requestUpdate().

// Lit (TS & JS) - clock.ts / clock.js
private tick() {
  this.date = new Date();
  this.host.requestUpdate();
}

L'horloge devrait maintenant tourner !

Pour comparer plus en détail les cas d'utilisation courants avec des hooks, consultez la section Niveau avancé - Hooks.

Dans cette section, vous allez apprendre à utiliser les emplacements pour gérer les enfants dans Lit.

Emplacements et enfants

Point de contrôle du code

Les emplacements aident à la composition en vous permettant d'imbriquer des composants.

Dans React, les enfants sont hérités via des props. L'emplacement par défaut est props.children et la fonction render définit son emplacement. Exemple :

const MyArticle = (props) => {
 return <article>{props.children}</article>;
};

N'oubliez pas que les props.children sont des composants React et non des éléments HTML.

Dans Lit, les enfants sont composés dans la fonction de rendu avec des éléments d'emplacement. Notez que les enfants ne sont pas hérités de la même manière que dans React. Dans Lit, les enfants sont des éléments HTMLElements associés à des emplacements. On appelle cette association une projection.

@customElement("my-article")
export class MyArticle extends LitElement {
  render() {
    return html`
      <article>
        <slot></slot>
      </article>
   `;
  }
}

Emplacements multiples

Point de contrôle du code

Dans React, l'ajout de plusieurs emplacements revient essentiellement à hériter d'autres props.

const MyArticle = (props) => {
  return (
    <article>
      <header>
        {props.headerChildren}
      </header>
      <section>
        {props.sectionChildren}
      </section>
    </article>
  );
};

De même, l'ajout d'autres éléments <slot> crée davantage d'emplacements dans Lit. Plusieurs emplacements sont définis avec l'attribut name : <slot name="slot-name">. Cela permet aux enfants de déclarer l'emplacement qui leur sera attribué.

@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>
   `;
  }
}

Contenu de l'emplacement par défaut

Un emplacement affiche sa sous-arborescence lorsqu'il n'y a pas de nœuds projetés sur cet emplacement. Si des nœuds y sont projetés, l'emplacement n'affiche pas sa sous-arborescence, mais les nœuds projetés.

@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>
   `;
  }
}

Affecter des enfants à des emplacements

Point de contrôle du code

Dans React, les enfants sont affectés à des emplacements par le biais des propriétés d'un composant. Dans l'exemple ci-dessous, les éléments React sont transmis aux props headerChildren et sectionChildren.

const MyNewsArticle = () => {
 return (
   <MyArticle
     headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
     sectionChildren={<p>Children are props in React!</p>}
   />
 );
};

Dans Lit, les enfants sont affectés à des emplacements à l'aide de l'attribut 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>
   `;
  }
}

S'il n'y a pas d'emplacement par défaut (<slot>, par exemple) et qu'aucun emplacement ne possède un attribut name (<slot name="foo">, par exemple) qui correspond à l'attribut slot des enfants de l'élément personnalisé (<div slot="foo">, par exemple), ce nœud ne sera alors pas projeté et ne s'affichera pas.

Parfois, un développeur peut avoir besoin d'accéder à l'API d'un élément HTMLElement.

Dans cette section, vous allez apprendre à acquérir des références d'éléments dans Lit.

Références dans React

Point de contrôle du code (TypeScript)Point de contrôle du code (JavaScript)

Un composant React est transpilé dans une série d'appels de fonction qui créent un DOM virtuel lorsqu'il est appelé. Ce DOM virtuel est interprété par ReactOM et affiche des éléments HTMLElements.

Dans React, les refs correspondent à de l'espace en mémoire pour contenir un élément HTMLElement généré.

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>
 );
};

Dans l'exemple ci-dessus, le composant React effectue ce qui suit :

  • Il affiche une entrée de texte vide et un bouton avec du texte.
  • Il sélectionne l'entrée lorsque l'utilisateur clique sur le bouton.

Après le rendu initial, React définit inputRef.current sur l'élément HTMLInputElement généré via l'attribut ref.

Références Lit avec @query

Lit se trouve à proximité du navigateur et crée une abstraction très fine par rapport aux fonctionnalités natives du navigateur.

L'équivalent React de refs dans Lit est l'élément HTMLElement renvoyé par les décorateurs @query et @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"></input>
      <br />
      <!-- Bind the click listener -->
      <button @click=${this.onButtonClick}>
        Click to focus on the input above!
      </button>
   `;
  }
}

Dans l'exemple ci-dessus, le composant Lit effectue ce qui suit :

  • Il définit une propriété sur MyElement à l'aide du décorateur @query (création d'un "getter" pour un élément HTMLInputElement).
  • Il déclare et associe un rappel d'événement de clic appelé onButtonClick.
  • Il sélectionne l'entrée lorsque l'utilisateur clique sur le bouton.

Dans JavaScript, les décorateurs @query et @queryAll exécutent respectivement querySelector et querySelectorAll. Il s'agit de l'équivalent JavaScript de @query('input') inputEl!: HTMLInputElement;.

get inputEl() {
  return this.renderRoot.querySelector('input');
}

Une fois que le composant Lit a validé le modèle de la méthode render à la racine de my-element, le décorateur @query permet alors à inputEl de renvoyer le premier élément input trouvé dans la racine de rendu. La valeur null est renvoyée si @query ne trouve pas l'élément spécifié.

Si la racine de rendu comporte plusieurs éléments input, @queryAll renvoie une liste de nœuds.

Dans cette section, vous allez apprendre à arbitrer l'état entre des composants dans Lit.

Composants réutilisables

Point de contrôle du code

React reproduit les pipelines de rendu fonctionnels avec un flux de données descendant. Les parents fournissent l'état aux enfants via les props, et les enfants communiquent avec leurs parents via les rappels trouvés dans les props.

const CounterButton = (props) => {
  const label = props.step < 0
    ? `- ${-1 * props.step}`
    : `+ ${props.step}`;

  return (
    <button
      onClick={() =>
        props.addToCounter(props.step)}>{label}</button>
  );
};

Dans l'exemple ci-dessus, un composant React effectue ce qui suit :

  • Il crée un libellé en fonction de la valeur props.step.
  • Il affiche un bouton avec "+step" ou "-step" comme libellé.
  • Il met à jour le composant parent en appelant props.addToCounter avec props.step en tant qu'argument lors d'un clic.

Bien qu'il soit possible de transmettre des rappels dans Lit, les schémas conventionnels sont différents. Dans l'exemple ci-dessus, le composant React peut être écrit en tant que composant Lit dans l'exemple ci-dessous :

@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>
    `;
  }
}

Dans l'exemple ci-dessus, un composant Lit effectue ce qui suit :

  • Il crée la propriété réactive step.
  • Il distribue un événement personnalisé appelé update-counter contenant la valeur step de l'élément lorsque l'utilisateur clique.

Les événements du navigateur remontent depuis les enfants vers les éléments parents. Ces événements permettent aux enfants de diffuser des événements d'interaction et des changements d'état. React transmet fondamentalement l'état dans le sens opposé. Il est donc rare de voir les composants React distribuer et écouter des événements comme le font les composants Lit.

Composants avec état

Point de contrôle du code

Dans React, des hooks sont souvent utilisés pour gérer l'état. Un composant MyCounter peut être créé en réutilisant le composant CounterButton. Notez comment addToCounter est transmis aux deux instances de CounterButton.

const MyCounter = (props) => {
 const [counterSum, setCounterSum] = React.useState(0);
 const addToCounter = useCallback(
   (step) => {
     setCounterSum(counterSum + step);
   },
   [counterSum, setCounterSum]
 );

 return (
   <div>
     <h3>&Sigma;: {counterSum}</h3>
     <CounterButton
       step={-1}
       addToCounter={addToCounter} />
     <CounterButton
       step={1}
       addToCounter={addToCounter} />
   </div>
 );
};

L'exemple ci-dessus illustre ce qui suit :

  • Un état count est créé.
  • Un rappel qui ajoute un nombre à un état count est créé.
  • CounterButton utilise addToCounter pour mettre à jour count par step à chaque clic.

Une implémentation similaire de MyCounter peut être réalisée dans Lit. Notez que addToCounter n'est pas transmis à counter-button. Au lieu de cela, le rappel est lié en tant qu'écouteur d'événements à l'événement @update-counter sur un élément parent.

@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>&Sigma; ${this.count}</h3>
        <counter-button step="-1"></counter-button>
        <counter-button step="1"></counter-button>
      </div>
    `;
  }
}

L'exemple ci-dessus illustre ce qui suit :

  • Une propriété réactive appelée count, qui met à jour le composant lorsque la valeur est modifiée, est créée.
  • Le rappel addToCounter est lié à l'écouteur d'événements @update-counter.
  • count est mis à jour en ajoutant la valeur trouvée dans le detail.step de l'événement update-counter.
  • La valeur step de counter-button est définie via l'attribut step.

Il est plus courant d'utiliser les propriétés réactives de Lit pour diffuser les changements des parents aux enfants. De même, il est recommandé d'utiliser le système d'événements du navigateur pour afficher les détails de bas en haut.

Cette approche respecte les bonnes pratiques, ainsi que l'objectif de Lit visant à assurer la compatibilité des composants Web sur de multiples plates-formes.

Dans cette section, vous allez étudier les styles dans Lit.

Styles

Lit propose plusieurs méthodes pour appliquer un style aux éléments, ainsi qu'une solution intégrée.

Styles intégrés

Point de contrôle du code

Lit accepte les styles intégrés ainsi que leurs liaisons.

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>
    `;
  }
}

Dans l'exemple ci-dessus, un style intégré est appliqué à deux titres.

Essayez maintenant de lier une bordure border: 1px solid black au texte orange :

<h1 style="color:orange;${'border: 1px solid black;'}">This text is orange</h1>

Le calcul de la chaîne de style pouvant être fastidieux, Lit fournit une directive pour vous aider.

styleMap

La directive styleMap permet de définir plus facilement des styles intégrés à l'aide de JavaScript. Exemple :

Point de contrôle du code

import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map';

@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'exemple ci-dessus illustre ce qui suit :

  • Un h1 avec une bordure et un sélecteur de couleur est affiché.
  • border-color est remplacé par la valeur du sélecteur de couleur.

styleMap est également utilisé pour définir les styles du h1. styleMap suit une syntaxe semblable à la syntaxe de liaison de l'attribut style de React.

CSSResult

Point de contrôle du code

Pour définir le style des composants, il est recommandé d'utiliser le littéral de modèle balisé 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'exemple ci-dessus illustre ce qui suit :

  • Un littéral de modèle balisé CSS est déclaré avec une liaison.
  • Les couleurs de deux éléments h1 avec ID sont définies.

Avantages de la balise de modèle css :

  • Analysée une fois par classe versus par instance
  • Implémentée avec à l'esprit la possibilité de réutiliser facilement les modules
  • Peut séparer facilement les styles dans leurs propres fichiers
  • Compatible avec le polyfill des propriétés personnalisées CSS

Notez également la balise <style> dans index.html :

<!-- index.html -->
<style>
  h1 {
    color: red !important;
  }
</style>

Lit va définir les styles de vos composants en fonction de leur racine. Autrement dit, les styles ne seront pas placés à l'intérieur ni à l'extérieur. Pour transmettre des styles entre les composants, l'équipe Lit recommande d'utiliser les propriétés CSS personnalisées, car elles peuvent pénétrer la portée des styles Lit.

Balises de style

Il est également possible d'insérer simplement des balises <style> dans vos modèles. Le navigateur déduplique ces balises de style, mais en les plaçant dans vos modèles, elles sont analysées par instance de composant plutôt que par classe, comme c'est le cas avec le modèle balisé css. De plus, la déduplication des CSSResult par le navigateur est bien plus rapide.

Il est également possible d'utiliser une <link rel="stylesheet"> dans votre modèle pour les styles. Toutefois, ce n'est pas recommandée, car cela peut entraîner un flash initial de contenu sans style.

JSX et création de modèles

Lit et DOM virtuel

Lit-html n'inclut pas de DOM virtuel traditionnel qui différencie chaque nœud individuel. Au lieu de cela, il utilise des fonctionnalités de performances intrinsèques conformément à la spécification des littéraux de modèles balisés d'ES2015. Les littéraux de modèles balisés sont des chaînes de littéraux de modèles auxquelles sont associées des fonctions de balise.

Voici un exemple de littéral de modèle :

const str = 'string';
console.log(`This is a template literal ${str}`);

Voici un exemple de littéral de modèle balisé :

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

Dans l'exemple ci-dessus, la balise est la fonction tag, et la fonction f renvoie un appel d'un littéral de modèle balisé.

Les remarquables performances dans Lit viennent en grande partie du fait que les tableaux de chaînes transmis à la fonction de balise ont le même pointeur (comme illustré dans le second console.log). Le navigateur ne recrée pas un nouveau tableau de strings à chaque appel de fonction de balise, car il utilise le même littéral de modèle (c.-à-d. à la même position dans l'AST). Ainsi, la liaison, l'analyse et la mise en cache de modèles dans Lit peuvent tirer parti de ces fonctionnalités sans que cela entraîne une surcharge de l'environnement d'exécution.

Ce comportement intégré au navigateur pour les littéraux de modèles balisés donne à Lit un avantage considérable en termes de performances. La plupart des DOM virtuels exécutent la majorité de leurs tâches en JavaScript. Toutefois, les littéraux de modèles balisés exécutent la commande diff en grande partie dans le C++ du navigateur.

Si vous voulez commencer à utiliser les littéraux de modèle balisés HTML avec React ou Preact, l'équipe Lit recommande la bibliothèque htm.

Toutefois, comme pour le site Google Codelabs et plusieurs éditeurs de code en ligne, vous remarquerez que la mise en évidence de la syntaxe des littéraux de modèles balisés n'est pas très courante. Certains IDE et éditeurs de texte acceptent ces outils par défaut (par exemple, le surligneur de bloc de codes Atom et GitHub. L'équipe Lit collabore également très étroitement avec la communauté pour gérer des projets tels que lit-plugin (un plug-in VS Code qui permet de mettre en évidence la syntaxe, de valider le type et de bénéficier d'IntelliSense dans vos projets Lit).

Lit et JSX + DOM React

JSX ne s'exécute pas dans le navigateur et utilise à la place un préprocesseur pour convertir les appels de fonction JSX en JavaScript (généralement via Babel).

Par exemple, Babel transforme ce qui suit :

const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);

comme ceci :

const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);

Le DOM React convertit ensuite le résultat React en DOM réel (propriétés, attributs, écouteurs d'événements, etc.).

Lit-html utilise des littéraux de modèles balisés qui peuvent s'exécuter dans le navigateur sans transpilation ni préprocesseur. Autrement dit, pour commencer avec Lit, vous avez besoin uniquement d'un fichier HTML, d'un script de module ES et d'un serveur. Voici un script entièrement exécutable avec le navigateur :

<!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>

En outre, comme le système de création de modèles de Lit (lit-html) utilise directement l'API DOM et non un DOM virtuel classique, Lit 2 fait moins de 5 ko une fois compressé au format .gz contre plus de 40 ko pour React (2,8 ko) + le DOM React (39,4 ko).

Événements

React utilise un système d'événements synthétiques. Cela signifie que le DOM React doit définir chaque événement qui sera utilisé sur chaque composant et fournir un écouteur d'événements en camelCase pour chaque type de nœud. Par conséquent, JSX n'a pas de méthode permettant de définir un écouteur d'événements pour un événement personnalisé. Les développeurs doivent utiliser une ref, puis appliquer impérativement un écouteur. L'expérience développeur est ainsi médiocre quand des bibliothèques pour lesquelles React n'est pas utilisé sont intégrées, ce qui implique d'écrire un wrapper propre à React.

Lit-html accède directement au DOM et utilise des événements natifs. L'ajout d'écouteurs d'événements est aussi simple que @event-name=${eventNameListener}. Autrement dit, moins d'analyses d'exécution sont réalisées pour ajouter des écouteurs d'événements et déclencher des événements.

Composants et props

Composants et éléments personnalisés dans React

"En coulisses", LitElement utilise des éléments personnalisés pour empaqueter ses composants. Ces éléments personnalisés introduisent des compromis entre les composants React lorsqu'il s'agit de séparer des composants (l'état et le cycle de vie sont décrits plus en détail dans la section État et cycle de vie).

Voici quelques avantages des éléments personnalisés en tant que système de composants :

  • Ce sont des éléments natifs du navigateur qui ne nécessitent aucun outil.
  • Ils s'intègrent dans chaque API de navigateur (de innerHTML et document.createElement à querySelector).
  • Ils sont généralement utilisables dans tous les frameworks.
  • Ils peuvent être enregistrés de manière différée avec customElements.define et le DOM "hydrate".

Voici quelques inconvénients des éléments personnalisés par rapport aux composants React :

  • Il est impossible de créer un élément personnalisé sans définir une classe (pas de composants fonctionnels de type JSX).
  • Ces éléments doivent contenir une balise fermante.
    • Remarque : Malgré l'aspect pratique pour les développeurs, les fournisseurs de navigateurs ont tendance à regretter la spécification des balises à fermeture automatique. C'est pourquoi les spécifications les plus récentes n'incluent généralement pas de balises à fermeture automatique.
  • Un nœud supplémentaire est ajouté à l'arborescence DOM, ce qui peut causer des problèmes de mise en page.
  • Ils doivent être enregistrés via JavaScript.

Lit a choisi les éléments personnalisés plutôt qu'un système d'éléments sur mesure, car ils sont intégrés au navigateur. L'équipe Lit estime que les avantages sur le plan de la compatibilité avec plusieurs frameworks l'emportent sur ceux qu'offre une couche d'abstraction de composants. En réalité, les efforts de l'équipe de Lit dans l'espace lit-ssr ont permis de surmonter les principaux problèmes d'enregistrement JavaScript. En outre, certaines entreprises telles que GitHub profitent de l'enregistrement différé des éléments personnalisés pour améliorer progressivement les pages en y intégrant une touche facultative.

Transmettre des données à des éléments personnalisés

On estime souvent à tort au sujet des éléments personnalisés que les données ne peuvent être transmises que sous forme de chaînes. Cette idée fausse vient probablement du fait que les attributs des éléments ne peuvent être écrits que sous forme de chaînes. Même s'il est vrai que Lit convertit les attributs de chaîne en leurs types définis, les éléments personnalisés peuvent aussi accepter des données complexes en tant que propriétés.

Par exemple, prenons la définition de LitElement suivante :

code

// 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>`;
  }
}

Une propriété réactive primitive num est définie, laquelle convertira la valeur de chaîne d'un attribut en number. Une structure de données complexe est ensuite introduite avec attribute:false, qui désactive le traitement des attributs dans Lit.

Voici comment transmettre des données à cet élément personnalisé :

<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>

État et cycle de vie

Autres rappels de cycle de vie dans React

static getDerivedStateFromProps

Il n'y a pas d'équivalent dans Lit, car les props et l'état sont des propriétés de la même classe.

shouldComponentUpdate

  • shouldUpdate est son équivalent dans Lit.
  • Il est appelé lors du premier rendu (contrairement à React).
  • Il a une fonction semblable à shouldComponentUpdate dans React.

getSnapshotBeforeUpdate

Dans Lit, getSnapshotBeforeUpdate est semblable à update et à willUpdate.

willUpdate

  • Ce rappel est appelé avant update.
  • Contrairement à getSnapshotBeforeUpdate, willUpdate est appelé avant render.
  • La modification des propriétés réactives dans willUpdate ne déclenche pas de nouveau le cycle de mise à jour.
  • Idéal pour calculer les valeurs de propriété qui dépendent d'autres propriétés et qui sont utilisées dans le reste du processus de mise à jour.
  • Cette méthode est appelée sur le serveur dans le rendu côté serveur. Il n'est donc pas recommandé d'accéder au DOM dans le cas présent.

update

  • Ce rappel est appelé après willUpdate.
  • Contrairement à getSnapshotBeforeUpdate, update est appelé avant render.
  • La modification des propriétés réactives dans update ne déclenche pas de nouveau le cycle de mise à jour si ce rappel est modifié avant d'appeler super.update.
  • Idéal pour capturer les informations du DOM entourant le composant avant que le résultat affiché ne soit validée dans le DOM.
  • Cette méthode n'est pas appelée sur le serveur dans le rendu côté serveur.

Autres rappels de cycle de vie dans Lit

Plusieurs rappels de cycle de vie n'ont pas été mentionnés dans la section précédente, car ils n'ont pas d'équivalents dans React. à savoir :

attributeChangedCallback

Ce rappel est appelé quand l'un des attributs observedAttributes de l'élément est modifié. observedAttributes et attributeChangedCallback font tous deux partie de la spécification des éléments personnalisés et sont implémentés par Lit en arrière-plan pour fournir une API d'attribut pour les éléments Lit.

adoptedCallback

Ce rappel est appelé lorsque le composant est déplacé vers un nouveau document (par exemple, d'un documentFragment de l'élément HTMLTemplateElement vers le document principal). Il fait également partie de la spécification des éléments personnalisés et ne doit être utilisé que pour des cas d'utilisation avancés lorsque le composant modifie des documents.

Autres méthodes et propriétés du cycle de vie

Ces méthodes et propriétés sont des membres de la classe que vous pouvez appeler, ignorer ou attendre pour faciliter la manipulation du processus de cycle de vie.

updateComplete

Il s'agit d'une Promise qui se résout une fois la mise à jour de l'élément terminée, étant donné que les cycles de mise à jour et de rendu sont asynchrones. Exemple :

async nextButtonClicked() {
  this.step++;
  // Wait for the next "step" state to render
  await this.updateComplete;
  this.dispatchEvent(new Event('step-rendered'));
}

getUpdateComplete

Cette méthode doit être remplacée pour être personnalisée lorsque updateComplete se résout. Ceci est fréquent lorsqu'un composant effectue le rendu d'un composant enfant, et que leurs cycles de rendu doivent être synchronisés. Exemple :

class MyElement extends LitElement {
  ...
  async getUpdateComplete() {
    await super.getUpdateComplete();
    await this.myChild.updateComplete;
  }
}

performUpdate

Cette méthode correspond à ce qui appelle les rappels de cycle de vie de mise à jour. Cela n'est généralement pas nécessaire, sauf dans de rares cas où la mise à jour doit être effectuée de manière synchrone ou pour une planification personnalisée.

hasUpdated

Cette propriété est true si le composant a été mis à jour au moins une fois.

isConnected

Cette propriété fait partie de la spécification des éléments personnalisés. Elle est true si l'élément est actuellement associé à l'arborescence du document principal.

Visualisation du cycle de vie d'une mise à jour dans Lit

Le cycle de vie d'une mise à jour comprend trois parties :

  • Avant la mise à jour
  • Mise à jour
  • Après la mise à jour

Avant la mise à jour

Graphique acyclique dirigé qui illustre des nœuds avec des noms de rappel. constructeur vers requestUpdate. @property à un setter de propriété. attributeChangedCallback à un setter de propriété. Setteur de propriété sur hasChanged. hasChanged à requestUpdate. requestUpdate renvoie vers le graphique suivant du cycle de vie de la mise à jour.

Après la requestUpdate, une mise à jour planifiée est disponible.

Mise à jour

Graphique acyclique dirigé qui illustre des nœuds avec des noms de rappel. Flèche de l'image précédente du cycle de vie avant la mise à jour qui pointe vers performUpdate. performUpdate vers shouldUpdate. shouldUpdate renvoie à la fois à "complete update if false" ainsi que willUpdate. willUpdate vers update. update vers render ainsi que vers le graphique suivant illustrant le cycle de vie après la mise à jour. render pointe également vers le graphique suivant illustrant le cycle de vie après la mise à jour.

Après la mise à jour

Graphique acyclique dirigé qui illustre des nœuds avec des noms de rappel. Flèche de l'image précédente illustrant le cycle de vie de la mise à jour, qui pointe vers firstUpdated. firstUpdated vers updated. updated vers updateComplete.

Hooks

Pourquoi utiliser des hooks

Les hooks ont été introduits dans React pour les cas d'utilisation simples de fonctions composants nécessitant un état. Dans de nombreux cas simples, les fonctions composants avec des hooks sont souvent beaucoup plus simples et lisibles que les composants à base de classes. Toutefois, lors de l'introduction de mises à jour d'état asynchrones et de la transmission de données entre les hooks ou les effets, le schéma de hooks ne suffit pas toujours, tandis qu'une solution basée sur les classes, comme les contrôleurs réactifs, est souvent plus efficace.

Hooks et contrôleurs qui envoient des requêtes à une API

Un hook est souvent écrit pour demander des données à une API. Prenons l'exemple de cette fonction composant React qui effectue ce qui suit :

  • index.tsx
    • Affiche du texte
    • Affiche la réponse de useAPI
      • ID et nom d'utilisateur
      • Message d'erreur
        • 404 une fois l'utilisateur 11 atteint (par nature)
        • Erreur d'annulation si l'extraction de l'API est annulée
      • Chargement du message
    • Affiche un bouton d'action
      • Utilisateur suivant : qui extrait l'API pour l'utilisateur suivant
      • Annuler : abandonne l'extraction de l'API et affiche une erreur
  • useApi.tsx
    • Définit un hook personnalisé useApi
    • Va extraire de manière asynchrone un objet utilisateur d'une API
    • Émet :
      • Nom d'utilisateur
      • Si l'extraction est en cours de chargement
      • Tous les messages d'erreur
      • Un rappel pour annuler l'extraction
    • Annule les extractions en cours si le composant est démonté

Découvrez l'implémentation de Lit et du contrôleur réactif.

Conclusions :

  • Les contrôleurs réactifs sont semblables aux hooks personnalisés.
  • Transmission des données ne pouvant pas être affichées entre les rappels et les effets.
    • React utilise useRef pour transmettre des données entre useEffect et useCallback.
    • Lit utilise une propriété de classe privée.
    • React reproduit essentiellement le comportement d'une propriété de classe privée.

Enfants

Emplacement par défaut

Lorsque des éléments HTML n'ont pas d'attribut slot, ils sont affectés à l'emplacement sans nom par défaut. Dans l'exemple ci-dessous, MyApp place un paragraphe à un emplacement nommé. L'autre paragraphe sera par défaut à l'emplacement sans nom.

Terrain de jeu

@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>
   `;
  }
}

Modification des emplacements

Lorsque la structure des descendants de l'emplacement change, un événement slotchange est déclenché. Un composant Lit peut lier un écouteur d'événements à un événement slotchange. Dans l'exemple ci-dessous, les assignedNodes du premier emplacement trouvé à la shadowRoot seront consignés dans la console en cas de 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

Génération de références

Lit et React montrent tous les deux une référence à un élément HTMLElement une fois que leurs fonctions render ont été appelées. Toutefois, il est intéressant d'examiner comment React et Lit composent le DOM qui est renvoyé par la suite via un décorateur @query dans Lit ou une référence dans React.

React est un pipeline fonctionnel qui crée des composants React et non des éléments HTMLElements. Étant donné qu'un attribut ref est déclaré avant l'affichage d'un élément HTMLElement, un espace en mémoire est alloué. C'est la raison pour laquelle, vous voyez null comme valeur initiale d'un attribut Ref, car l'élément DOM réel n'a pas encore été créé ou rendu (par exemple, useRef(null)).

Une fois que ReactDOM convertit un composant React en élément HTMLElement, il recherche un attribut appelé ref dans le composant ReactComponent. Le cas échéant, ReactDOM ajoute la référence de l'élément HTMLElement à ref.current.

LitElement utilise la fonction de balise de modèle html de lit-html pour composer un élément de modèle en arrière-plan. LitElement remplace le contenu du modèle dans le Shadow DOM d'un élément personnalisé après le rendu. Le Shadow DOM est une arborescence DOM étendue, qui est encapsulée par une racine fantôme. Le décorateur @query crée ensuite une méthode getter pour la propriété, qui exécute essentiellement un this.shadowRoot.querySelector à la racine étendue.

Interroger plusieurs éléments

Dans l'exemple ci-dessous, le décorateur @queryAll renvoie les deux paragraphes de la racine fantôme en tant que 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>
   `;
  }
}

Globalement, @queryAll crée un getter pour les paragraphs, qui renvoie les résultats de this.shadowRoot.querySelectorAll(). Dans JavaScript, un getter peut être déclaré pour effectuer la même fonction :

get paragraphs() {
  return this.renderRoot.querySelectorAll('p');
}

Interroger les éléments qui changent

Le décorateur @queryAsync est mieux adapté pour gérer un nœud qui peut changer en fonction de l'état d'une autre propriété d'élément.

Dans l'exemple ci-dessous, @queryAsync trouvera le premier élément de paragraphe. Toutefois, un élément de paragraphe ne sera affiché que si renderParagraph génère un nombre impair de manière aléatoire. La directive @queryAsync renverra une promesse qui se résout lorsque le premier paragraphe sera disponible.

@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()}
   `;
  }
}

État de médiation

Dans React, la convention est d'utiliser des rappels, car l'état est paramétré par React lui-même. React fait de son mieux pour ne pas dépendre de l'état fourni par les éléments. Le DOM est simplement un effet du processus de rendu.

État externe

Il est possible d'utiliser Redux, MobX ou toute autre bibliothèque de gestion d'état parallèlement à Lit.

Les composants Lit sont créés dans le champ d'application du navigateur. Ainsi, toutes les bibliothèques qui existent également dans ce champ d'application sont disponibles pour Lit. De nombreuses bibliothèques incroyables ont été créées pour utiliser les systèmes de gestion d'état existants dans Lit.

Voici une série de Vaadin, qui explique comment exploiter Redux dans un composant Lit.

Consultez lit-mobx d'Adobe pour découvrir comment un grand site peut exploiter MobX dans Lit.

Découvrez également les éléments Apollo pour voir comment les développeurs intègrent GraphQL dans leurs composants Web.

Lit fonctionne avec les fonctionnalités natives du navigateur. La plupart des solutions de gestion d'état dans le champ d'application du navigateur peuvent être utilisées dans un composant Lit.

Styles

Shadow DOM

Pour encapsuler de façon native des styles et le DOM dans un élément personnalisé, Lit utilise Shadow DOM. Les racines fantômes génèrent une arborescence fantôme distincte de l'arborescence du document principal. La plupart des styles sont donc limités à ce document. Certains styles passent outre cette limitation (par exemple, la couleur et d'autres styles liés à la police).

Shadow DOM introduit également de nouveaux concepts et sélecteurs dans la spécification 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.
   */
}

Partager les styles

Lit facilite le partage des styles entre les composants sous la forme de CSSTemplateResults via les balises de modèle css. Exemple :

// 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>`
  }
}

Thématisation

Les racines fantômes, qui sont généralement des approches descendantes de balises de style, présentent un véritable défi pour la thématisation classique. La méthode classique d'aborder la thématisation avec les Web Components qui utilisent Shadow DOM consiste à exposer une API de style via des propriétés personnalisées CSS. Par exemple, Material Design utilise le schéma suivant :

.mdc-textfield-outline {
  border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
  caret-color: var(--mdc-theme-primary, #...);
}

L'utilisateur changerait alors le thème du site en appliquant des valeurs de propriétés personnalisées :

html {
  --mdc-theme-primary: #F00;
}
html[dark] {
  --mdc-theme-primary: #F88;
}

Si la thématisation descendante est incontournable et que vous ne pouvez pas exposer de styles, il est toujours possible de désactiver le Shadow DOM en ignorant createRenderRoot pour renvoyer this. Le modèle de vos composants sera alors affiché à l'élément personnalisé lui-même plutôt qu'à une racine fantôme associée à l'élément personnalisé. Vous perdrez alors l'encapsulation du style et du DOM, ainsi que les emplacements.

Production

IE 11

Si vous avez besoin de prendre en charge des navigateurs plus anciens comme IE 11, vous devrez charger des polyfills qui ajouteront environ 33 ko. Pour en savoir plus, cliquez ici.

Groupes conditionnels

L'équipe Lit recommande de diffuser deux groupes différents, un pour IE 11 et l'autre pour les navigateurs récents. Cela présente plusieurs avantages :

  • Le service ES 6 est plus rapide et répond à la plupart de vos clients.
  • L'ES 5 transpilé augmente considérablement la taille du groupe.
  • Les groupes conditionnels vous offrent le meilleur des deux possibilités.
    • Compatibilité avec Internet Explorer 11
    • Aucun ralentissement sur les navigateurs récents

Pour en savoir plus sur la création d'un groupe diffusé de manière conditionnelle, rendez-vous sur notre site de documentation.