W3docs

Shadow DOM und Events

Events im Shadow DOM: Bubbling über die Shadow-Grenze, Event-Retargeting, event.composedPath(), event.composed und eigene Events, die die Grenze überqueren.

Eine Web-Komponente, die mit Shadow DOM gebaut wurde, verbirgt ihre interne Struktur hinter einer Shadow-Grenze. Diese Kapselung verändert den Ereignisfluss: Manche Events überqueren die Grenze, andere nicht, und diejenigen, die sie überqueren, werden retargeted, sodass die Außenwelt niemals Einblick in private Interna erhält. Dieses Kapitel erklärt diese Regeln, damit deine Komponenten Events auslösen, die die Host-Seite tatsächlich nutzen kann.

Du lernst vier Dinge: wie Events über die Shadow-Grenze bubbeln, Event-Retargeting, event.composedPath(), das Flag event.composed und das Versenden eigener Events, die den Shadow-Baum verlassen.

Dieses Kapitel setzt voraus, dass du die Grundlagen von Shadow DOM und allgemeines Event-Bubbling und -Capturing bereits kennst. Falls Custom Events neu für dich sind, lies zuerst Dispatching Custom Events.

Event-Bubbling im Shadow DOM

Event-Bubbling beschreibt, wie sich ein Event den DOM-Baum nach oben ausbreitet: Es wird zunächst auf dem Zielelement ausgelöst, dann der Reihe nach auf jedem Vorfahren, bis es document erreicht. (Den vollständigen Überblick bietet Bubbling and Capturing.)

Im Shadow DOM stellt sich die Frage: Bubbled das Event weiter, sobald es den Shadow-Root erreicht, und gelangt es in den Light DOM des Hosts? Das hängt davon ab, ob das Event composed ist:

  • Composed Events überqueren die Shadow-Grenze und bubbeln weiter in den Light DOM. Die meisten nativen, nutzerorientierten Events sind composed: click, mousedown, keydown, input, pointermove und so weiter.
  • Non-composed Events halten am Shadow-Root an und erreichen den Host nicht. Beispiele: focus (nutze focusin/focusout, wenn du composed Focus-Events brauchst), scroll, mouseenter und load.

Um die Weiterverbreitung eines Events an einem beliebigen Punkt zu stoppen – unabhängig davon, ob es composed ist oder nicht –, rufe event.stopPropagation() auf.

Event-Retargeting

Das ist der Teil, der die meisten Menschen überrascht. Wenn ein composed Event die Grenze überquert, retargeted der Browser es: Für Listener im Light DOM zeigt event.target auf das Host-Element, nicht auf das innere Element, das tatsächlich angeklickt wurde.

Das ist beabsichtigt. Kapselung wäre sinnlos, wenn externer Code die privaten Knoten einer Komponente über event.target auslesen könnte. Die Host-Seite sieht also „etwas innerhalb von <my-widget> wurde angeklickt" – nicht „der dritte <button> in deinem Shadow-Baum wurde angeklickt". Innerhalb des Shadow-Baums zeigt event.target weiterhin auf das echte Element.

Falls du den tatsächlichen Pfad durch den Shadow-Baum benötigst, verwende event.composedPath() – dazu mehr im nächsten Abschnitt.

Verwendung von event.composedPath()

Da Retargeting das innere Element vor event.target verbirgt, brauchst du einen anderen Weg, um den tatsächlichen Propagationspfad zu inspizieren. event.composedPath() gibt ein array der Knoten zurück, durch die das Event gelaufen ist, einschließlich Knoten in allen durchquerten Shadow-Bäumen, geordnet vom innersten Ziel nach außen bis zu window.

Das ist der zuverlässige Weg, um aus einem Light-DOM-Listener heraus zu ermitteln, welches innere Element tatsächlich angeklickt wurde – allerdings nur bei Komponenten, deren Shadow-Root mode: 'open' hat. Bei einem mode: 'closed'-Root stoppt composedPath() beim Host, und die inneren Knoten werden ausgelassen, um die Privatsphäre der geschlossenen Komponente zu wahren.

Schauen wir uns an, wie event.composedPath() genutzt werden kann, um die Event-Propagation im Shadow DOM nachzuverfolgen:

<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>

Ein Klick auf das innere <div> listet alle Knoten im composed Path auf: Es beginnt mit dem angeklickten DIV, dann DIV (das Host-Element #outer), dann BODY, HTML und schließlich Einträge für document und window (die als undefined erscheinen, weil sie kein tagName haben). Die ersten Einträge sind genau das, was event.target vor Light-DOM-Listenern verbirgt.

Das Flag event.composed verstehen

Die schreibgeschützte Eigenschaft event.composed ist ein boolean: true, wenn das Event Shadow-Grenzen überqueren kann, false, wenn es auf seinen Shadow-Baum beschränkt ist. Sie lässt sich nachträglich nicht setzen – bei nativen Events ist sie durch die Spezifikation festgelegt, bei Custom Events wird sie beim Erstellen des Events über die Option composed gesetzt.

Dieses Flag ist besonders wichtig, wenn du eine Komponente baust und entscheiden musst, ob deine Custom Events nach außen gelangen sollen. Native Interaktions-Events wie click sind standardmäßig composed; eigene CustomEvents sind es nicht, sofern du es nicht explizit aktivierst.

Sehen wir uns an, wie event.composed in der Praxis eingesetzt werden kann:

<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>

In diesem Beispiel löst ein Klick auf den Button im Shadow DOM ein Click-Event aus. Wir erstellen dynamisch ein <p>-Element, um die Eigenschaft event.composed im Shadow DOM anzuzeigen.

Custom Events im Shadow DOM

Custom Events ermöglichen es einer Komponente, der Außenwelt Ereignisse mitzuteilen – „Wert geändert", „Element ausgewählt", „Dialog geschlossen" – ohne ihre Interna preiszugeben. Das ist der Standardweg, auf dem eine Web-Komponente mit der Seite kommuniziert, die sie verwendet. (Siehe Dispatching Custom Events für die detaillierte API-Beschreibung.)

Damit ein Custom Event einen Listener auf dem Host-Element im Light DOM erreicht, müssen zwei Optionen gesetzt sein:

  • composed: true – erlaubt dem Event, die Shadow-Grenze zu überqueren.
  • bubbles: true – erlaubt dem Event, den Baum nach oben zu wandern und Vorfahren-Listener zu erreichen.

Nur bubbles zu setzen lässt das Event im Shadow-Baum bubbeln, aber es stoppt am Shadow-Root. Nur composed zu setzen lässt es die Grenze überqueren, aber es steigt nicht zu Vorfahren auf. Meistens benötigt man beides.

Erstellen und versenden wir ein Custom Event in einem 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>

Ein Klick auf den Button sendet customEvent mit beiden Optionen bubbles: true und composed: true, sodass es die Shadow-Grenze überquert und bis zum Listener auf dem Host (container) im Light DOM hochbubbled. Um Daten zusammen mit dem Event zu übermitteln, verwende die Eigenschaft detail:

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

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

Beachte: Auch wenn das Event den Host erreicht, gilt Retargeting weiterhin – im container-Listener ist event.target das Host-Element, nicht der innere button. Verwende event.composedPath()[0], wenn du das ursprüngliche Ziel benötigst.

Kurzübersicht

Eigenschaft / MethodeWas sie angibt
event.composedtrue, wenn das Event Shadow-Grenzen überqueren kann (schreibgeschützt).
event.composedPath()Array der Knoten, die das Event durchläuft, einschließlich offener Shadow-Bäume, vom innersten Knoten nach außen.
event.target (aus dem Light DOM)Das Host-Element durch Retargeting – niemals der private innere Knoten.
Option bubblesErlaubt einem Custom Event, den Baum nach oben zu wandern.
Option composedErlaubt einem Custom Event, den Shadow-Baum zu verlassen.

Häufige Fallstricke

  • composed: true bei Custom Events vergessen. Ein Custom Event mit nur bubbles stirbt still am Shadow-Root und erreicht die Host-Seite nie – ein häufiger „mein Listener feuert nicht"-Fehler.
  • event.target von außen auslesen. Es wird auf den Host retargeted. Greife auf event.composedPath() zurück, wenn du das tatsächliche innere Ziel benötigst.
  • focus ist nicht composed. Verwende focusin/focusout, wenn Focus-Änderungen den Host erreichen sollen.
  • closed-Shadow-Roots. composedPath() gibt keine Knoten innerhalb eines mode: 'closed'-Roots preis – verlasse dich also nicht darauf, um geschlossene Komponenten zu inspizieren.

Verwandte Kapitel

Fazit

Events im Shadow DOM folgen einigen klaren Regeln: Composed Events überqueren die Grenze, Non-composed Events nicht, und Composed Events werden auf den Host retargeted, damit die Interna privat bleiben. Nutze event.composed, um die Grenzüberschreitbarkeit zu prüfen, event.composedPath(), um den tatsächlichen Pfad zu erhalten, und CustomEvent mit bubbles: true und composed: true, damit deine Komponenten mit der Seite kommunizieren können, die sie hostet.

Übungen

Übung
Welche Methode liefert die Abfolge der DOM-Elemente, die ein Event während seiner Propagation durchläuft?
Welche Methode liefert die Abfolge der DOM-Elemente, die ein Event während seiner Propagation durchläuft?
Übung
Welche Optionen muss ein CustomEvent haben, damit ein Listener auf dem Host-Element im Light DOM es abfangen kann?
Welche Optionen muss ein CustomEvent haben, damit ein Listener auf dem Host-Element im Light DOM es abfangen kann?
Übung
Was zeigt event.target in einem Light-DOM-Listener, wenn ein Klick innerhalb eines offenen Shadow-Baums erfolgt?
Was zeigt event.target in einem Light-DOM-Listener, wenn ein Klick innerhalb eines offenen Shadow-Baums erfolgt?
Was this page helpful?