Gestiona el click del botón interno y, después, envía un CustomEvent con nombre desde el host del componente. La página que lo usa escucha en ese host y lee event.detail. Esta guía crea una demo completa en un navegador local en un Mac y explica qué opciones de evento necesita cada listener.

Define la notificación que necesita quien usa el componente

En un componente web de JavaScript simple, quien lo usa es la página o el componente contenedor que utiliza tu elemento personalizado. En este caso, la tarea concreta consiste en permitir que quien lo usa reaccione cuando alguien activa un botón interno, sin consultar el DOM de sombra del componente.

El ejemplo usa una ficha sintética etiquetada como Tile A. Su evento público es tile-activate y lleva un itemId y una count de activación. CustomEvent proporciona el campo detail para estos datos de la aplicación; el nombre del evento distingue entre mayúsculas y minúsculas. Tanto el código que lo envía como el listener deben usar exactamente el mismo nombre. Documentación del constructor CustomEvent.

Considera estas decisiones como un pequeño contrato de interfaz:

  • Nombre: tile-activate.
  • Significado: se ha activado el botón interno y se ha actualizado el contador.
  • Datos: detail.itemId identifica la ficha; detail.count contiene su contador actualizado.
  • Origen: el host del elemento personalizado envía la notificación.
  • Entrega: los consumidores directos y los listeners de elementos antecesores pueden recibirla, incluso a través de los límites de componentes de sombra contenedores.

Estas son decisiones de diseño para este ejemplo. Un evento útil describe la acción que le importa al consumidor. Así, el componente puede cambiar su marcado interno más adelante y conservar la misma interfaz de notificación.

Crea y abre el ejemplo completo en tu Mac

Necesitas un navegador de escritorio actualizado y un editor de texto sin formato. Este ejemplo de un solo archivo no requiere instalar paquetes ni compilar. Usa un elemento personalizado autónomo que extiende HTMLElement, el patrón de la plataforma documentado en la sección sobre elementos personalizados del estándar HTML.

En TextEdit, selecciona Archivo > Nuevo y, después, Formato > Convertir en texto sin formato. Pega el documento completo que aparece a continuación. Selecciona Archivo > Guardar, asígnale el nombre component-click.html y elige Usar .html si se te solicita. Estos son los pasos documentados por Apple para crear un archivo HTML en TextEdit. Trabajar con documentos HTML en TextEdit en el 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>

Abre el archivo guardado en tu navegador y haz clic en Activate Tile A. El primer resultado esperado es Direct listener: tile-a, activation 1 y Parent listener: tile-a, activation 1. Cada activación posterior aumenta ambos contadores mostrados en uno. Estos son los resultados esperados del ejemplo, no resultados de pruebas realizadas en un navegador.

La secuencia importante se ve en el código: actualizar el estado del componente, crear la notificación, enviarla desde this y permitir que el consumidor actualice su propia interfaz. dispatchEvent() invoca los listeners correspondientes de forma síncrona antes de devolver el control, por lo que los consumidores ven el contador actualizado durante sus manejadores. Documentación de dispatchEvent.

Elige las opciones adecuadas de propagación y cruce de límites de sombra

El destino del evento y la ubicación del listener determinan qué opciones importan. Un listener adjunto directamente al host recibe un evento enviado en ese host aunque bubbles sea falso. Un listener de un elemento antecesor ordinario necesita la propagación. Para cruzar un límite de sombra también se requiere composed. Los valores predeterminados subyacentes de bubbles, composed y cancelable son todos falsos. Estándar DOM: interfaz Event.

La siguiente tabla de decisiones se basa en esas reglas de propagación. Da por sentado que los listeners normales se registran sin captura y que ningún manejador detiene la propagación.

Dónde se inicia el evento personalizado Dónde escucha el consumidor Configuración para esa ruta
Host del componente El mismo host Ninguna de las dos opciones tiene que ser verdadera
Host del componente Elemento antecesor ordinario en el mismo árbol bubbles: true
Botón interno de sombra Elemento antecesor ordinario fuera de esa raíz de sombra bubbles: true, composed: true
Host del componente dentro de otro árbol de sombra Elemento antecesor ordinario fuera de la raíz de sombra contenedora bubbles: true, composed: true

En la demo, this.dispatchEvent(...) inicia la notificación en el host. Por eso, no necesita salir de la raíz de sombra del propio componente. composed: true permite usar la notificación a través del límite de sombra de un componente contenedor si esta ficha se anida más adelante. Ese alcance más amplio debe formar parte deliberadamente de la interfaz pública. Documentación de la propiedad composed de Event.

Para hacer una prueba concreta, cambia solo bubbles: true a bubbles: false, guarda y vuelve a cargar la página. Al activar el botón, el estado directo debería actualizarse y el estado del elemento antecesor debería seguir mostrando Parent listener: waiting. Después, restaura la propagación. Si cambias solo composed a false, ambos listeners deberían seguir funcionando en este documento concreto, porque ninguna de las rutas cruza un límite de sombra.

Conserva el comportamiento de inicialización y reconexión

La entrega de eventos puede parecer correcta al principio y, aun así, producir notificaciones duplicadas cuando un componente se mueve o se vuelve a conectar. Deja explícitas las responsabilidades de creación, conexión y limpieza.

El constructor llama primero a super(), inicializa el estado y adjunta una raíz de sombra. El renderizado ocurre en connectedCallback() y está protegido por la referencia existente al botón para que se ejecute una sola vez. Estas decisiones siguen los requisitos del estándar HTML para los constructores y sus pautas para proteger la inicialización que no debe repetirse cuando vuelven a ejecutarse las funciones de conexión. Requisitos para constructores y reacciones de elementos personalizados.

El componente registra su listener interno para el clic cuando se conecta y lo elimina cuando se desconecta. Al reconectarse, reutiliza el botón existente y conserva _count. Las funciones de conexión y desconexión son hooks del ciclo de vida que se invocan cuando los elementos entran y salen del documento. Uso de elementos personalizados.

La función _handleClick almacenada es importante: al eliminarla hay que identificar la misma función que se usó al registrarla, con la misma opción de captura. Crear una nueva función flecha en la llamada de eliminación no identificaría el listener registrado. Documentación de removeEventListener.

Si añades listeners a window, document u otro destino externo en un componente más grande, deja igual de explícita la limpieza de esos listeners. Mantén los cambios del ciclo de vida separados del significado de la notificación para que reconectar el elemento no anuncie por error una acción del usuario.

Diagnostica las notificaciones ausentes o repetidas

Usa las dos líneas de estado visibles para localizar el fallo antes de cambiar las opciones del evento. Te permiten distinguir entre un problema con la notificación del componente y otro con la entrega a elementos antecesores.

  1. No cambia ninguno de los estados: compara el nombre del evento al enviarlo y al registrarlo; después, comprueba que se ejecuten la definición del elemento personalizado y el manejador del botón. En este documento, los listeners del consumidor se registran antes de cualquier activación del usuario.
  2. Solo cambia el estado directo: comprueba bubbles. También inspecciona los manejadores intermedios que detengan la propagación.
  3. La entrega falla solo al anidarse en otro componente: identifica el límite de sombra de esa ruta y comprueba composed.
  4. Los contadores saltan después de volver a conectar el componente: inspecciona los registros repetidos de listeners, en especial las nuevas funciones anónimas creadas cada vez que se conecta el elemento.

Las funciones anónimas repetidas son objetos listener distintos, aunque el código fuente sea idéntico. Mantener una referencia estable a la función facilita entender el registro y la limpieza. addEventListener() también distingue la opción de captura del listener, que afecta el momento en que se ejecuta. Documentación de addEventListener.

Puede que un click nativo ya llegue fuera de un árbol de sombra, porque los clics enviados por el navegador están compuestos. Un evento personalizado aparte resulta útil cuando el consumidor necesita una acción del componente con nombre y datos definidos. Evita gestionar ambas notificaciones como si fueran la misma acción de la aplicación, salvo que la duplicación sea intencional. Documentación de la propiedad composed de Event.

Por último, en el diseño de eventos, mantén separadas la notificación y la autorización para continuar. Este ejemplo incrementa el contador antes de enviar el evento y lo deja como no cancelable. Si un consumidor llama a preventDefault(), no puede deshacer esa actualización. Una solicitud cancelable requeriría una implementación explícita que compruebe el valor devuelto por dispatchEvent() antes de realizar la acción solicitada. Valor devuelto por dispatchEvent.

Para esta tarea, el contrato completo es pequeño: un manejador del botón interno actualiza el estado, un evento personalizado con nombre sale del host y los consumidores leen los datos documentados. Haz que funcione primero el listener directo y, después, habilita las rutas de propagación que realmente necesita la interfaz contenedora.

Fuentes