Kodokon kodokon.com

Web Components : custom elements, shadow DOM et slots

Maîtrisez le cycle de vie des custom elements, l'encapsulation du shadow DOM et la projection de contenu par slots.

12 min · 3 questions

Ouvrir cette leçon dans Kodokon

Les custom elements étendent le vocabulaire HTML : toute classe héritant de HTMLElement peut être enregistrée dans le CustomElementRegistry, à condition que son nom contienne un tiret (user-badge, jamais userbadge). Point subtil : si la balise apparaît dans le document avant l'appel à customElements.define, elle est d'abord créée comme élément inconnu puis upgradée sur place - le constructeur est alors invoqué a posteriori sur un nœud déjà présent dans l'arbre.

JAVASCRIPT
class UserBadge extends HTMLElement {
  static observedAttributes = ["name"];
  connectedCallback() {
    this.render();
  }
  attributeChangedCallback() {
    this.render();
  }
  render() {
    this.textContent = this.getAttribute("name");
  }
}
customElements.define("user-badge", UserBadge);
Un custom element minimal avec cycle de vie complet.

Le cycle de vie réserve des pièges : attributeChangedCallback n'est déclenché que pour les attributs listés dans observedAttributes, et lors d'un upgrade il est appelé pour chaque attribut déjà présent avant connectedCallback. La spécification interdit au constructeur d'examiner les attributs et les enfants, ou d'en ajouter : au moment où il s'exécute (via createElement ou un clonage), l'élément peut être totalement vide.

HTML
<template id="card-tpl">
  <style>
    :host { display: block; padding: 1rem; }
    ::slotted(h2) { margin: 0; }
  </style>
  <slot name="title"></slot>
  <slot></slot>
</template>
Un template inerte : parsé, mais ni exécuté ni rendu.

attachShadow crée un sous-arbre encapsulé : les styles n'en sortent pas, les sélecteurs de la page n'y entrent pas - seules les propriétés CSS héritées (couleur, police) et les custom properties traversent la frontière. Le contenu d'un <template> vit dans un DocumentFragment accessible via .content : ses scripts ne s'exécutent pas et ses images ne se chargent pas tant que vous ne le clonez pas dans le document. Les nœuds placés dans le light DOM sont projetés dans les <slot> : ils restent physiquement dans le light DOM, et l'événement slotchange vous signale tout changement d'affectation.

JAVASCRIPT
class InfoCard extends HTMLElement {
  constructor() {
    super();
    const tpl = document.getElementById("card-tpl");
    this.attachShadow({ mode: "open" })
      .appendChild(tpl.content.cloneNode(true));
  }
}
customElements.define("info-card", InfoCard);
Clonage du template dans un shadow root ouvert.

Deux extensions méritent votre attention. Les form-associated custom elements : avec static formAssociated = true et attachInternals(), votre composant participe pleinement aux formulaires (valeur soumise, validation native, états :invalid). Et le shadow DOM déclaratif : <template shadowrootmode=open> attache un shadow root dès le parsing, sans JavaScript, ce qui rend les web components compatibles avec le rendu côté serveur.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Pourquoi le constructeur d'un custom element ne doit-il ni lire ses attributs ni créer d'enfants ?
    • Parce que les attributs sont figés en lecture seule avant connectedCallback
    • Parce que le shadow DOM masque le light DOM pendant la construction
    • Parce qu'à cet instant (createElement, clonage), l'élément peut être vide ; la spécification impose un constructeur sans effet sur le contenu
  2. Avec attachShadow({ mode: 'closed' }), que renvoie element.shadowRoot depuis l'extérieur ?
    • null : seule la référence retournée par attachShadow permet d'accéder au shadow root
    • Le shadow root, mais en lecture seule
    • Un DocumentFragment vide
    • Une exception DOMException est levée
  3. Où se trouvent physiquement les nœuds projetés dans un <slot> ?
    • Ils sont déplacés dans le shadow DOM au moment du rendu
    • Ils restent dans le light DOM ; le slot ne fait que les afficher à sa position dans l'arbre aplati
    • Ils sont clonés : l'original reste dans le light DOM, la copie vit dans le shadow DOM