Modèle, emplacement et ombre

L'avantage des composants Web est leur réutilisation: vous pouvez créer un widget d'UI une seule fois et le réutiliser plusieurs fois. Bien que vous ayez besoin de JavaScript pour créer des composants Web, vous n'avez pas besoin d'une bibliothèque JavaScript. HTML et les API associées fournissent tout ce dont vous avez besoin.

La norme "Composants Web" comprend trois parties : les modèles HTML, les éléments personnalisés et le Shadow DOM. Combinés, ils permettent de créer des éléments personnalisés, autonomes (encapsulés) réutilisables qui peuvent être intégrés facilement dans des applications existantes, comme tous les autres éléments HTML que nous avons déjà abordés.

Dans cette section, nous allons créer l'élément <star-rating>, un composant Web qui permet aux utilisateurs d'évaluer une expérience sur une échelle de 1 à 5 étoiles. Lorsque vous attribuez un nom à un élément personnalisé, il est recommandé d'utiliser des lettres minuscules. Incluez également un tiret, car cela permet de distinguer les éléments standards des éléments personnalisés.

Nous verrons comment utiliser les éléments <template> et <slot>, l'attribut slot et JavaScript pour créer un modèle avec un Shadow DOM encapsulé. Nous allons ensuite réutiliser l'élément défini, en personnalisant une section de texte, comme vous le feriez pour n'importe quel élément ou composant Web. Nous aborderons aussi brièvement l'utilisation du code CSS aussi bien à l'intérieur qu'à l'extérieur de l'élément personnalisé.

L'élément <template>

L'élément <template> permet de déclarer des fragments de HTML à cloner et à insérer dans le DOM avec JavaScript. Le contenu de l'élément n'est pas affiché par défaut. À la place, ils sont instanciés à l'aide de JavaScript.

<template id="star-rating-template">
  <form>
    <fieldset>
      <legend>Rate your experience:</legend>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required />
        <input type="radio" name="rating" value="2" aria-label="2 stars" />
        <input type="radio" name="rating" value="3" aria-label="3 stars" />
        <input type="radio" name="rating" value="4" aria-label="4 stars" />
        <input type="radio" name="rating" value="5" aria-label="5 stars" />
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Étant donné que le contenu d'un élément <template> n'est pas écrit à l'écran, <form> et son contenu ne sont pas affichés. Oui, ce codepen est vide, mais si vous inspectez l'onglet HTML, vous verrez le balisage <template>.

Dans cet exemple, <form> n'est pas un enfant d'un <template> dans le DOM. Le contenu des éléments <template> est plutôt des enfants d'un élément DocumentFragment renvoyé par la propriété HTMLTemplateElement.content. Pour être rendu visible, JavaScript doit être utilisé pour récupérer le contenu et l'ajouter au DOM.

Ce bref JavaScript n'a pas créé d'élément personnalisé. À la place, cet exemple a ajouté le contenu de <template> à <body>. Le contenu est devenu une partie du DOM visible et stylisé.

Capture d&#39;écran du codepen précédent, tel qu&#39;illustré dans le DOM.

Il n'est pas très utile de demander JavaScript pour implémenter un modèle pour une seule note, mais la création d'un composant Web pour un widget d'évaluation personnalisable et utilisé régulièrement est utile.

L'élément <slot>

Nous incluons un emplacement pour inclure une légende personnalisée par occurrence. Le code HTML fournit un élément <slot> en tant qu'espace réservé dans un élément <template> qui, s'il fournit un nom, crée un "emplacement nommé". Un emplacement nommé peut être utilisé pour personnaliser le contenu d'un composant Web. L'élément <slot> nous permet de contrôler l'emplacement des enfants d'un élément personnalisé dans son arborescence fantôme.

Dans notre modèle, nous remplaçons <legend> par <slot>:

<template id="star-rating-template">
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>

L'attribut name permet d'attribuer des emplacements à d'autres éléments si l'élément comporte un attribut slot dont la valeur correspond au nom d'un emplacement nommé. Si l'élément personnalisé ne correspond à aucun espace publicitaire, le contenu de <slot> s'affiche. Nous avons donc inclus un <legend> avec du contenu générique qui peut être affiché si un utilisateur inclut simplement <star-rating></star-rating>, sans contenu, dans son code HTML.

<star-rating>
  <legend slot="star-rating-legend">Blendan Smooth</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Hoover Sukhdeep</legend>
</star-rating>
<star-rating>
  <legend slot="star-rating-legend">Toasty McToastface</legend>
  <p>Is this text visible?</p>
</star-rating>

L'attribut slot est un attribut global permettant de remplacer le contenu de l'<slot> dans un <template>. Dans notre élément personnalisé, l'élément avec l'attribut d'emplacement est <legend>. Ce n'est pas une obligation. Dans notre modèle, <slot name="star-rating-legend"> sera remplacé par <anyElement slot="star-rating-legend">, où <anyElement> peut correspondre à n'importe quel élément, même à un autre élément personnalisé.

Éléments non définis

Dans <template>, nous avons utilisé un élément <rating>. Cet élément n'est pas personnalisé. Il s'agit plutôt d'un élément inconnu. Les navigateurs n'échouent pas lorsqu'ils ne reconnaissent pas un élément. Les éléments HTML non reconnus sont traités par le navigateur comme des éléments intégrés anonymes dont le style peut être appliqué à l'aide du code CSS. Comme pour <span>, aucun style ni aucune sémantique ne sont appliqués aux éléments <rating> et <star-rating>.

Notez que <template> et son contenu ne sont pas affichés. <template> est un élément connu qui comporte du contenu qui ne doit pas être affiché. L'élément <star-rating> n'a pas encore été défini. Tant qu'un élément n'est pas défini, le navigateur l'affiche comme tous les éléments non reconnus. Pour le moment, l'élément <star-rating> non reconnu est traité comme un élément intégré anonyme. Par conséquent, le contenu comprenant les légendes et le <p> du troisième <star-rating> s'affiche comme s'il se trouvait dans un élément <span>.

Définissons notre élément pour le convertir en élément personnalisé.

Éléments personnalisés

JavaScript est nécessaire pour définir des éléments personnalisés. Une fois défini, le contenu de l'élément <star-rating> est remplacé par une racine fantôme contenant tout le contenu du modèle que nous lui associons. Les éléments <slot> du modèle sont remplacés par le contenu de l'élément du <star-rating> dont la valeur d'attribut slot correspond à la valeur du nom de <slot>, le cas échéant. Si ce n'est pas le cas, le contenu des emplacements du modèle s'affiche.

Le contenu d'un élément personnalisé qui n'est pas associé à un emplacement (<p>Is this text visible?</p> de notre troisième <star-rating>) n'est pas inclus dans la racine fantôme et ne s'affiche donc pas.

Nous définissons l'élément personnalisé nommé star-rating en étendant HTMLElement:

customElements.define('star-rating',
  class extends HTMLElement {
    constructor() {
      super(); // Always call super first in constructor
      const starRating = document.getElementById('star-rating-template').content;
      const shadowRoot = this.attachShadow({
        mode: 'open'
      });
      shadowRoot.appendChild(starRating.cloneNode(true));
    }
  });

Maintenant que l'élément est défini, chaque fois que le navigateur rencontre un élément <star-rating>, il est affiché tel que défini par l'élément avec #star-rating-template, qui est notre modèle. Le navigateur associe une arborescence Shadow DOM au nœud, en ajoutant un clone du contenu du modèle à ce DOM. Notez que les éléments sur lesquels vous pouvez attachShadow() sont limités.

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(starRating.cloneNode(true));

Si vous consultez les outils pour les développeurs, vous remarquerez que l'élément <form> de <template> fait partie de la racine fantôme de chaque élément personnalisé. Un clone du contenu de <template> est visible dans chaque élément personnalisé des outils pour les développeurs et visible dans le navigateur, mais le contenu de l'élément personnalisé lui-même n'est pas affiché à l'écran.

Capture d&#39;écran des outils de développement montrant le contenu du modèle cloné dans chaque élément personnalisé.

Dans l'exemple <template>, nous avons ajouté le contenu du modèle au corps du document, en ajoutant le contenu au DOM standard. Dans la définition de customElements, nous avons utilisé le même élément appendChild(), mais le contenu du modèle cloné a été ajouté à un Shadow DOM encapsulé.

Vous avez remarqué que les cases d'option étaient de nouveau auparavant sans style ? Étant donné qu'il fait partie d'un Shadow DOM, et non du DOM standard, le style défini dans l'onglet CSS de Codepen ne s'applique pas. Les styles CSS de cet onglet sont limités au document, et non au Shadow DOM. Les styles ne sont donc pas appliqués. Nous devons créer des styles encapsulés pour styliser notre contenu Shadow DOM encapsulé.

Shadow DOM

Le Shadow DOM définit des styles CSS pour chaque arborescence fantôme, en l'isolant du reste du document. Cela signifie que le CSS externe ne s'applique pas à votre composant et que les styles de composant n'ont aucun effet sur le reste du document, sauf si nous les redirigeons intentionnellement.

Comme nous avons ajouté le contenu à un Shadow DOM, nous pouvons inclure un élément <style> fournissant du code CSS encapsulé à l'élément personnalisé.

Grâce à la portée de l'élément personnalisé, vous n'avez pas à vous soucier de l'apparition des styles dans le reste du document. Nous pouvons réduire considérablement la spécificité des sélecteurs. Par exemple, comme les seules entrées utilisées dans l'élément personnalisé sont des cases d'option, nous pouvons utiliser input au lieu de input[type="radio"] comme sélecteur.

 <template id="star-rating-template">
  <style>
    rating {
      display: inline-flex;
    }
    input {
      appearance: none;
      margin: 0;
      box-shadow: none;
    }
    input::after {
      content: '\2605'; /* solid star */
      font-size: 32px;
    }
    rating:hover input:invalid::after,
    rating:focus-within input:invalid::after {
      color: #888;
    }
    input:invalid::after,
      rating:hover input:hover ~ input:invalid::after,
      input:focus ~ input:invalid::after  {
      color: #ddd;
    }
    input:valid {
      color: orange;
    }
    input:checked ~ input:not(:checked)::after {
      color: #ccc;
      content: '\2606'; /* hollow star */
    }
  </style>
  <form>
    <fieldset>
      <slot name="star-rating-legend">
        <legend>Rate your experience:</legend>
      </slot>
      <rating>
        <input type="radio" name="rating" value="1" aria-label="1 star" required/>
        <input type="radio" name="rating" value="2" aria-label="2 stars"/>
        <input type="radio" name="rating" value="3" aria-label="3 stars"/>
        <input type="radio" name="rating" value="4" aria-label="4 stars"/>
        <input type="radio" name="rating" value="5" aria-label="5 stars"/>
      </rating>
    </fieldset>
    <button type="reset">Reset</button>
    <button type="submit">Submit</button>
  </form>
</template>

Bien que les composants Web soient encapsulés avec un balisage in-<template> et que les styles CSS soient limités au Shadow DOM et ne soient pas visibles à l'extérieur des composants, le contenu de l'emplacement qui s'affiche, c'est-à-dire la partie <anyElement slot="star-rating-legend"> de <star-rating>, n'est pas encapsulé.

Définir un style en dehors du champ d'application actuel

Il est possible, mais pas simple, de styliser le document à partir d'un Shadow DOM et de styliser le contenu d'un Shadow DOM à partir des styles globaux. La limite de l'ombre, qui correspond à la fin du DOM Shadow et au début du DOM standard, peut être balayée, mais seulement de manière très intentionnelle.

L'arborescence ombrée est l'arborescence DOM située dans le Shadow DOM. La racine fantôme est le nœud racine de l'arborescence fantôme.

La pseudo-classe :host sélectionne <star-rating>, l'élément hôte Shadow. L'hôte Shadow est le nœud DOM auquel le Shadow DOM est associé. Pour ne cibler que des versions spécifiques de l'hôte, utilisez :host(). Cela permet de sélectionner uniquement les éléments des hôtes fantômes qui correspondent au paramètre transmis, comme un sélecteur de classe ou d'attribut. Pour sélectionner tous les éléments personnalisés, vous pouvez utiliser star-rating { /* styles */ } dans le CSS global ou :host(:not(#nonExistantId)) dans les styles de modèle. En termes de spécificité, le CSS mondial l'emporte.

Le pseudo-élément ::slotted() traverse la limite du Shadow DOM à partir du Shadow DOM. Il sélectionne un élément positionné s'il correspond au sélecteur. Dans notre exemple, ::slotted(legend) correspond à nos trois légendes.

Pour cibler un Shadow DOM à partir de CSS dans le champ d'application global, vous devez modifier le modèle. L'attribut part peut être ajouté à tout élément auquel vous souhaitez appliquer un style. Utilisez ensuite le pseudo-élément ::part() pour mettre en correspondance les éléments d'une arborescence fantôme qui correspondent au paramètre transmis. L'élément d'ancrage ou d'origine du pseudo-élément est le nom de l'hôte ou de l'élément personnalisé (dans ce cas, star-rating). Le paramètre correspond à la valeur de l'attribut part.

Si le balisage de notre modèle a commencé comme suit:

<template id="star-rating-template">
  <form part="formPart">
    <fieldset part="fieldsetPart">

Nous pourrions cibler <form> et <fieldset> avec:

star-rating::part(formPart) { /* styles */ }
star-rating::part(fieldsetPart) { /* styles */ }

Les noms de pièces fonctionnent de la même manière que les classes: un élément peut avoir plusieurs noms de pièces séparés par un espace, et plusieurs éléments peuvent avoir le même nom de pièce.

Google propose une checklist efficace pour créer des éléments personnalisés. Vous pouvez également en savoir plus sur les Shadow DOM déclaratifs.

Testez vos connaissances

Testez vos connaissances sur le modèle, l'emplacement et l'ombre.

Par défaut, les styles externes au Shadow DOM stylisent les éléments qui se trouvent à l'intérieur.

Vrai
Réessayez.
Faux
Bonne réponse !

Quelle réponse est la description correcte de l'élément <template> ?

Élément générique utilisé pour afficher n'importe quel contenu sur votre page.
Réessayez.
Élément d'espace réservé.
Réessayez.
Élément utilisé pour déclarer des fragments de HTML, qui ne seront pas affichés par défaut.
Bonne réponse !