W3docs

Shadow DOM y Eventos

Aprende cómo se comportan los eventos en Shadow DOM: burbujeo a través del límite de sombra, retargeting, event.composedPath(), event.composed y eventos personalizados que cruzan el límite.

Un componente web construido con Shadow DOM mantiene su estructura interna oculta detrás de un límite de sombra. Esa encapsulación cambia cómo fluyen los eventos: algunos eventos cruzan el límite y otros no, y los que lo hacen son redirigidos para que el mundo exterior nunca vea tus partes internas privadas. Este capítulo explica esas reglas para que tus componentes disparen eventos que la página anfitriona pueda usar.

Cubrirás cuatro aspectos: cómo burbujean los eventos a través del límite de sombra, el retargeting de eventos, event.composedPath(), el indicador event.composed, y el envío de eventos personalizados que escapan del árbol de sombra.

Este capítulo asume que ya conoces los conceptos básicos de Shadow DOM y el burbujeo y captura de eventos en general. Si los eventos personalizados son nuevos para ti, lee primero Dispatching Custom Events.

Burbujeo de Eventos en Shadow DOM

El burbujeo de eventos describe cómo un evento se propaga hacia arriba en el árbol DOM: se dispara en el elemento objetivo, luego en cada ancestro en orden, hasta llegar a document. (Para ver el panorama completo, consulta Bubbling and Capturing.)

Dentro de Shadow DOM la pregunta es: ¿el evento sigue burbujeando una vez que llega a la raíz de sombra, hacia el DOM de luz del anfitrión? Eso depende de si el evento es compuesto:

  • Los eventos compuestos cruzan el límite de sombra y siguen burbujeando hacia el DOM de luz. La mayoría de los eventos nativos orientados al usuario son compuestos: click, mousedown, keydown, input, pointermove, y otros.
  • Los eventos no compuestos se detienen en la raíz de sombra y nunca llegan al anfitrión. Ejemplos: focus (usa focusin/focusout si necesitas eventos de foco compuestos), scroll, mouseenter y load.

Para detener la propagación de un evento en cualquier punto — sea compuesto o no — llama a event.stopPropagation().

Retargeting de eventos

Esta es la parte que sorprende a la gente. Cuando un evento compuesto cruza el límite, el navegador lo retargetea: para los oyentes en el DOM de luz, event.target apunta al elemento anfitrión, no al elemento interior en el que realmente hiciste clic.

Eso es deliberado. La encapsulación no tendría sentido si el código externo pudiera leer los nodos privados de tu componente desde event.target. Así que la página anfitriona ve "algo dentro de <my-widget> fue clicado," no "el tercer <button> en tu árbol de sombra fue clicado." Dentro del árbol de sombra, event.target todavía apunta al elemento real.

Si necesitas la ruta real a través del árbol de sombra, usa event.composedPath() — que se explica a continuación.

Uso de event.composedPath()

Como el retargeting oculta el elemento interior de event.target, necesitas otra forma de inspeccionar la ruta de propagación real. event.composedPath() devuelve un array de los nodos por los que pasó el evento, incluyendo los nodos dentro de los árboles de sombra que cruzó, ordenados desde el objetivo más interno hacia afuera hasta window.

Esta es la forma confiable de responder "¿cuál elemento interior fue realmente clicado?" desde un oyente en el DOM de luz — pero solo para componentes cuya raíz de sombra es mode: 'open'. Para una raíz mode: 'closed', composedPath() se detiene en el anfitrión y los nodos internos se omiten, preservando la privacidad del componente cerrado.

Veamos cómo se puede usar event.composedPath() para rastrear la propagación de eventos dentro de Shadow DOM:

<div id="outer"></div>
<script>
  const outer = document.getElementById('outer');
  const shadow = outer.attachShadow({ mode: 'open' });
  const inner = document.createElement('div');
  inner.textContent = 'Click me';

  inner.addEventListener('click', event => {
    const composedInfo = document.createElement('p');
    composedInfo.textContent = 'The event composedPath contains the following elements:';
    shadow.appendChild(composedInfo);
    const path = event.composedPath();
    path.forEach((e) => {
      const pathItem = document.createElement('p');
      pathItem.textContent = e.tagName;
      shadow.appendChild(pathItem);
    });
  });

  shadow.appendChild(inner);
</script>

Al hacer clic en el <div> interior se listan todos los nodos en la ruta compuesta: comienza con el DIV en el que hiciste clic, luego DIV (el anfitrión #outer), luego BODY, HTML, y finalmente entradas para document y window (que se muestran como undefined porque no tienen tagName). Las primeras entradas son exactamente lo que event.target oculta de los oyentes en el DOM de luz.

Comprensión de event.composed

La propiedad de solo lectura event.composed es un boolean: true si el evento puede cruzar los límites de sombra, false si está confinado a su árbol de sombra. No puedes cambiarla después — para los eventos nativos está fijada por la especificación, y para los eventos personalizados la fijas cuando construyes el evento mediante la opción composed.

Este indicador importa más cuando construyes un componente y necesitas decidir si tus eventos personalizados deben escapar. Los eventos de interacción nativos como click son compuestos por defecto; tus propios CustomEvents no son compuestos a menos que lo indiques explícitamente.

Examinemos cómo se puede utilizar event.composed en la práctica:

<div id="outer"></div>

<script>
  const outer = document.getElementById('outer');
  const shadow = outer.attachShadow({ mode: 'open' });
  const button = document.createElement('button');
  button.textContent = 'Click me';

  button.addEventListener('click', event => {
    const composedInfo = document.createElement('p');
    composedInfo.textContent = `Composed: ${event.composed}`;
    shadow.appendChild(composedInfo);
  });

  shadow.appendChild(button);
</script>

En este ejemplo, al hacer clic en el botón dentro del shadow DOM se dispara un evento de clic. Creamos dinámicamente un elemento <p> para mostrar la propiedad event.composed dentro del shadow DOM.

Eventos Personalizados en Shadow DOM

Los eventos personalizados permiten que un componente anuncie cosas al mundo exterior — "valor cambiado," "elemento seleccionado," "diálogo cerrado" — sin exponer sus partes internas. Esta es la forma estándar en que un componente web se comunica con la página que lo utiliza. (Consulta Dispatching Custom Events para conocer la API en detalle.)

Para que un evento personalizado llegue a un oyente en el elemento anfitrión en el DOM de luz, necesitas dos opciones configuradas:

  • composed: true — permite que el evento cruce el límite de sombra.
  • bubbles: true — permite que viaje hacia arriba en el árbol para llegar a los oyentes ancestros.

Si configuras solo bubbles, el evento burbujea dentro del árbol de sombra pero se detiene en la raíz de sombra. Si configuras solo composed, cruza el límite pero no subirá hasta los ancestros. Casi siempre querrás ambos.

Creemos y despachemos un evento personalizado dentro de un shadow DOM:

<div id="container"></div>

<script>
  const container = document.getElementById('container');
  const shadow = container.attachShadow({ mode: 'open' });
  const button = document.createElement('button');
  button.textContent = 'Click me';

  button.addEventListener('click', () => {
    const event = new CustomEvent('customEvent', { bubbles: true, composed: true });
    button.dispatchEvent(event);
  });

  shadow.appendChild(button);

  container.addEventListener('customEvent', () => {
    const composedInfo = document.createElement('p');
    composedInfo.textContent = `Custom Event Triggered!`;
    container.appendChild(composedInfo);
  });
</script>

Al hacer clic en el botón se despacha customEvent con bubbles: true y composed: true, por lo que cruza el límite de sombra y burbujea hasta el oyente en el anfitrión (container) en el DOM de luz. Para pasar datos junto con el evento, usa la propiedad detail:

button.dispatchEvent(new CustomEvent('customEvent', {
  bubbles: true,
  composed: true,
  detail: { value: 42 }
}));

container.addEventListener('customEvent', (event) => {
  console.log(event.detail.value); // 42
});

Ten en cuenta que aunque el evento llega al anfitrión, el retargeting sigue aplicándose: en el oyente de container, event.target es el elemento anfitrión, no el button interior. Usa event.composedPath()[0] si necesitas el objetivo original.

Referencia rápida

Propiedad / métodoLo que te indica
event.composedtrue si el evento puede cruzar los límites de sombra (solo lectura).
event.composedPath()Array de nodos que el evento recorre, incluyendo árboles de sombra abiertos, del más interno hacia afuera.
event.target (desde el DOM de luz)El elemento anfitrión, debido al retargeting — nunca el nodo interior privado.
Opción bubblesPermite que un evento personalizado viaje hacia arriba en el árbol.
Opción composedPermite que un evento personalizado salga del árbol de sombra.

Errores comunes

  • Olvidar composed: true en los eventos personalizados. Un evento personalizado con solo bubbles muere silenciosamente en la raíz de sombra y nunca llega a la página anfitriona — un error frecuente de "mi oyente no se dispara".
  • Leer event.target desde afuera. Está redirigido al anfitrión. Usa event.composedPath() cuando necesites el objetivo interior real.
  • focus no es compuesto. Usa focusin/focusout si necesitas que los cambios de foco lleguen al anfitrión.
  • Raíces de sombra closed. composedPath() no revelará nodos dentro de una raíz mode: 'closed', así que no dependas de él para inspeccionar componentes cerrados.

Capítulos relacionados

Conclusión

Los eventos en Shadow DOM siguen algunas reglas claras: los eventos compuestos cruzan el límite, los no compuestos no lo hacen, y los eventos compuestos son redirigidos al anfitrión para que tus partes internas permanezcan privadas. Usa event.composed para verificar si puede cruzar, event.composedPath() para recuperar la ruta real, y CustomEvent con bubbles: true y composed: true para que tus componentes puedan comunicarse con la página que los aloja.

Práctica

Práctica
¿Qué método proporciona una forma de recuperar la secuencia de elementos DOM que un evento recorre durante su propagación?
¿Qué método proporciona una forma de recuperar la secuencia de elementos DOM que un evento recorre durante su propagación?
Práctica
¿Qué opciones debe tener un CustomEvent para que un oyente en el elemento anfitrión en el DOM de luz pueda capturarlo?
¿Qué opciones debe tener un CustomEvent para que un oyente en el elemento anfitrión en el DOM de luz pueda capturarlo?
Práctica
Desde un oyente en el DOM de luz, ¿a qué apunta event.target cuando ocurre un clic dentro de un árbol de sombra abierto?
Desde un oyente en el DOM de luz, ¿a qué apunta event.target cuando ocurre un clic dentro de un árbol de sombra abierto?
Was this page helpful?