Gérez le click du bouton interne, puis envoyez un CustomEvent nommé depuis l’hôte du composant. La page qui utilise le composant écoute cet hôte et lit event.detail. Ce guide crée une démonstration complète dans un navigateur en local sur Mac et explique quelles options d’événement chaque écouteur nécessite.

Définir la notification dont votre consommateur a besoin

Dans un composant Web en JavaScript natif, le consommateur est la page ou le composant englobant qui utilise votre élément personnalisé. Ici, la tâche consiste à permettre à ce consommateur de réagir lorsqu’une personne active un bouton interne, sans interroger le DOM fantôme du composant.

L’exemple utilise une tuile de démonstration intitulée Tuile A. Son événement public est tile-activate, qui transporte un itemId et un count d’activation. CustomEvent fournit le champ detail pour ces données applicatives ; le nom de l’événement est sensible à la casse. Le code qui déclenche l’événement et l’écouteur doivent utiliser exactement le même nom. Documentation du constructeur CustomEvent.

Considérez ces choix comme un petit contrat d’interface :

  • Nom : tile-activate.
  • Signification : le bouton interne a été activé et le compteur mis à jour.
  • Données transmises : detail.itemId identifie la tuile ; detail.count contient son compteur mis à jour.
  • Origine : l’hôte de l’élément personnalisé déclenche la notification.
  • Transmission : les consommateurs directs et les écouteurs ancêtres peuvent la recevoir, y compris au-delà des frontières de composants fantômes englobants.

Ce sont des choix de conception pour cet exemple. Un événement utile décrit l’action qui intéresse le consommateur. Le composant peut ainsi modifier ultérieurement son balisage interne tout en conservant la même interface de notification.

Créer et ouvrir l’exemple complet sur votre Mac

Vous avez besoin d’un navigateur de bureau récent et d’un éditeur de texte brut. Aucun paquet n’a besoin d’être installé et aucune étape de compilation n’est requise pour cet exemple composé d’un seul fichier. Il utilise un élément personnalisé autonome qui étend HTMLElement, selon le modèle de la plateforme décrit dans la section sur les éléments personnalisés du standard HTML.

Dans TextEdit, choisissez Fichier > Nouveau, puis Format > Convertir au format texte. Collez le document complet ci-dessous. Choisissez Fichier > Enregistrer, nommez le fichier component-click.html et sélectionnez Utiliser .html si vous y êtes invité. Apple décrit ces étapes pour créer un fichier HTML dans TextEdit. Travailler avec des documents HTML dans TextEdit sur Mac.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Component click notification</title>
</head>
<body>
  <h1>Component click notification</h1>
  <section id="demo">
    <action-tile id="tile" item-id="tile-a"></action-tile>
    <p id="direct-status">Direct listener: waiting</p>
    <p id="parent-status">Parent listener: waiting</p>
  </section>

  <script>
    class ActionTile extends HTMLElement {
      constructor() {
        super();
        this._count = 0;
        this._button = null;
        this._root = this.attachShadow({ mode: "open" });
        this._handleClick = () => {
          this._count += 1;
          this.dispatchEvent(new CustomEvent("tile-activate", {
            detail: {
              itemId: this.getAttribute("item-id"),
              count: this._count
            },
            bubbles: true,
            composed: true
          }));
        };
      }

      connectedCallback() {
        if (!this._button) {
          this._button = document.createElement("button");
          this._button.type = "button";
          this._button.textContent = "Activate Tile A";
          this._root.append(this._button);
        }
        this._button.addEventListener("click", this._handleClick);
      }

      disconnectedCallback() {
        this._button.removeEventListener("click", this._handleClick);
      }
    }

    customElements.define("action-tile", ActionTile);

    const tile = document.getElementById("tile");
    const demo = document.getElementById("demo");
    const directStatus = document.getElementById("direct-status");
    const parentStatus = document.getElementById("parent-status");

    tile.addEventListener("tile-activate", (event) => {
      const { itemId, count } = event.detail;
      directStatus.textContent =
        `Direct listener: ${itemId}, activation ${count}`;
    });

    demo.addEventListener("tile-activate", (event) => {
      const { itemId, count } = event.detail;
      parentStatus.textContent =
        `Parent listener: ${itemId}, activation ${count}`;
    });
  </script>
</body>
</html>

Ouvrez le fichier enregistré dans votre navigateur et cliquez sur Activer la tuile A. Le premier résultat attendu est Écouteur direct : tile-a, activation 1 et Écouteur parent : tile-a, activation 1. À chaque activation suivante, les deux compteurs affichés augmentent de un. Il s’agit des résultats attendus de l’exemple, et non de résultats de tests rapportés sur un navigateur.

La séquence importante apparaît dans le code : mettre à jour l’état du composant, créer la notification, la déclencher depuis this, puis laisser le consommateur mettre à jour sa propre interface. dispatchEvent() appelle les écouteurs concernés de manière synchrone avant de renvoyer le contrôle ; les consommateurs voient donc le compteur mis à jour pendant l’exécution de leurs gestionnaires. Documentation de dispatchEvent.

Choisir les bonnes options de propagation et de franchissement des frontières

La cible de l’événement et l’emplacement de l’écouteur déterminent quelles options sont nécessaires. Un écouteur attaché directement à l’hôte reçoit un événement déclenché sur cet hôte même si bubbles vaut false. Un écouteur sur un ancêtre ordinaire a besoin de la propagation. Pour franchir une frontière de composant fantôme, il faut aussi composed. Par défaut, les valeurs sous-jacentes de bubbles, composed et cancelable sont toutes false. Standard DOM : interface Event.

Le tableau de décision suivant découle de ces règles de propagation. Il suppose que les écouteurs ordinaires sont enregistrés sans capture et qu’aucun gestionnaire n’interrompt la propagation.

Point de départ de l’événement personnalisé Emplacement de l’écouteur du consommateur Configuration pour cette route
Hôte du composant Même hôte Aucun des deux indicateurs n’a besoin d’être true
Hôte du composant Ancêtre ordinaire dans le même arbre bubbles: true
Bouton interne du composant fantôme Ancêtre ordinaire à l’extérieur de cette racine fantôme bubbles: true, composed: true
Hôte du composant à l’intérieur d’un autre arbre fantôme Ancêtre ordinaire à l’extérieur de la racine fantôme englobante bubbles: true, composed: true

Dans la démonstration, this.dispatchEvent(...) déclenche la notification sur l’hôte. Elle n’a donc pas besoin de sortir de la racine fantôme du composant. composed: true permet à la notification de franchir la frontière fantôme d’un composant englobant si cette tuile est imbriquée ultérieurement. Cette portée plus large doit faire partie de l’interface publique par choix délibéré. Documentation de la propriété composed de Event.

Pour une expérience ciblée, remplacez uniquement bubbles: true par bubbles: false, enregistrez le fichier et rechargez la page. Lors de l’activation, l’état direct devrait se mettre à jour, tandis que l’état parent reste Écouteur parent : en attente. Rétablissez ensuite la propagation. Si vous remplacez uniquement composed par false, les deux écouteurs continueront de fonctionner dans ce document, car aucune des deux routes ne franchit une frontière fantôme.

Préserver le comportement à l’initialisation et à la reconnexion

La transmission d’événements peut sembler correcte au départ, puis produire des notifications en double lorsqu’un composant est déplacé ou reconnecté. Définissez clairement les responsabilités de création, de connexion et de nettoyage.

Le constructeur appelle d’abord super(), initialise l’état et attache une racine fantôme. Le rendu a lieu dans connectedCallback(), avec une vérification de la référence au bouton existante pour ne s’exécuter qu’une fois. Ces choix respectent les exigences du standard HTML pour les constructeurs et ses recommandations de protéger les initialisations qui ne doivent pas être répétées lorsque les fonctions de rappel de connexion sont de nouveau exécutées. Exigences relatives aux constructeurs et aux réactions des éléments personnalisés.

Le composant enregistre son écouteur de clic interne lors de la connexion et le supprime lors de la déconnexion. À la reconnexion, il réutilise le bouton existant et conserve _count. Les fonctions de rappel de connexion et de déconnexion sont des hooks de cycle de vie appelés lorsque les éléments entrent dans le document ou en sortent. Utiliser des éléments personnalisés.

La fonction _handleClick stockée est importante : pour supprimer un écouteur, il faut identifier la même fonction que lors de l’enregistrement, avec le même paramètre de capture. Créer une nouvelle fonction fléchée dans l’appel de suppression ne permettrait pas d’identifier l’écouteur enregistré. Documentation de removeEventListener.

Si vous ajoutez des écouteurs à window, document ou à une autre cible externe dans un composant plus complexe, prévoyez un nettoyage tout aussi explicite pour ces écouteurs. Séparez les changements du cycle de vie de la signification de la notification, afin que la reconnexion de l’élément n’annonce pas accidentellement une action de l’utilisateur.

Diagnostiquer les notifications manquantes ou répétées

Servez-vous des deux lignes d’état visibles pour localiser le problème avant de modifier les indicateurs d’événement. Elles permettent de distinguer un problème lié à la notification du composant d’un problème de transmission aux ancêtres.

  1. Aucun des deux états ne change : comparez le nom de l’événement lors de son déclenchement et de son enregistrement, puis vérifiez que la définition de l’élément personnalisé et le gestionnaire du bouton s’exécutent. Dans ce document, les écouteurs du consommateur sont enregistrés avant toute activation par l’utilisateur.
  2. Seul l’état direct change : vérifiez bubbles. Examinez aussi tout gestionnaire intermédiaire qui interrompt la propagation.
  3. La transmission échoue uniquement lorsque le composant est imbriqué dans un autre composant : repérez la frontière fantôme sur cette route et vérifiez composed.
  4. Les compteurs font un bond après la reconnexion : recherchez les enregistrements répétés d’écouteurs, notamment les nouvelles fonctions anonymes créées à chaque connexion de l’élément.

Les fonctions anonymes répétées sont des objets écouteurs distincts, même si leur code source est identique. Une référence de fonction stable facilite le raisonnement sur l’enregistrement et le nettoyage. addEventListener() distingue également l’option de capture de l’écouteur, qui détermine le moment où il s’exécute. Documentation de addEventListener.

Un click natif peut déjà franchir une racine fantôme, car les clics déclenchés par le navigateur sont composés. Un événement personnalisé distinct est utile lorsque le consommateur a besoin d’une action de composant nommée et de données transmises bien définies. Évitez de traiter les deux notifications comme la même action de l’application, sauf si ce doublon est voulu. Documentation de la propriété composed de Event.

Enfin, dans la conception de vos événements, distinguez une notification d’une demande d’autorisation de poursuivre. Cet exemple incrémente le compteur avant le déclenchement et laisse l’événement non annulable. Un consommateur qui appelle preventDefault() ne peut pas annuler cette mise à jour. Une demande annulable nécessiterait une implémentation explicite qui vérifie la valeur renvoyée par dispatchEvent() avant d’effectuer l’action demandée. Valeur de retour de dispatchEvent.

Pour cette tâche, le contrat complet est simple : un gestionnaire de bouton interne met à jour l’état, un événement personnalisé nommé part de l’hôte et les consommateurs lisent les données transmises qui sont documentées. Vérifiez d’abord que l’écouteur direct fonctionne, puis activez les routes de propagation dont votre interface englobante a réellement besoin.

Sources