W3docs

Shadow DOM ed eventi

Come funzionano gli eventi nello Shadow DOM: bubbling oltre il confine, retargeting, event.composedPath(), event.composed e CustomEvent composti.

Un web component costruito con Shadow DOM mantiene la propria struttura interna nascosta dietro un confine shadow. Tale incapsulamento modifica il modo in cui gli eventi si propagano: alcuni attraversano il confine e altri no, e quelli che lo attraversano vengono reindirizzati in modo che il mondo esterno non veda mai i dettagli interni privati. Questo capitolo spiega queste regole affinché i tuoi componenti generino eventi che la pagina host possa effettivamente utilizzare.

Verranno trattati quattro argomenti: come gli eventi fanno bubbling oltre il confine shadow, il retargeting degli eventi, event.composedPath(), il flag event.composed e la generazione di eventi personalizzati che escono dall'albero shadow.

Questo capitolo presuppone che tu conosca già le basi di Shadow DOM e il bubbling e capturing degli eventi. Se i custom event sono nuovi per te, leggi prima Dispatching Custom Events.

Bubbling degli eventi nello Shadow DOM

Il bubbling degli eventi descrive come un evento si propaga verso l'alto nell'albero DOM: viene attivato sull'elemento target, poi su ciascun antenato a turno, finché non raggiunge document. (Per una panoramica completa vedi Bubbling and Capturing.)

All'interno dello Shadow DOM la domanda diventa: l'evento continua il bubbling una volta raggiunta la shadow root, uscendo nel light DOM dell'host? Dipende dal fatto che l'evento sia composed:

  • Gli eventi composed attraversano il confine shadow e continuano il bubbling nel light DOM. La maggior parte degli eventi nativi visibili all'utente sono composed: click, mousedown, keydown, input, pointermove e così via.
  • Gli eventi non composed si fermano alla shadow root e non raggiungono mai l'host. Esempi: focus (usa focusin/focusout se hai bisogno di eventi focus composti), scroll, mouseenter e load.

Per fermare la propagazione di un evento in qualsiasi punto — che sia composed o meno — chiama event.stopPropagation().

Retargeting degli eventi

Questa è la parte che sorprende le persone. Quando un evento composed attraversa il confine, il browser lo reindirizza: per i listener nel light DOM, event.target punta all'elemento host, non all'elemento interno su cui hai effettivamente fatto clic.

Questo è intenzionale. L'incapsulamento sarebbe inutile se il codice esterno potesse leggere i nodi privati del tuo componente tramite event.target. Quindi la pagina host vede "è stato fatto clic su qualcosa all'interno di <my-widget>", non "è stato fatto clic sul terzo <button> nel tuo albero shadow". All'interno dell'albero shadow, event.target punta ancora all'elemento reale.

Se hai bisogno del percorso reale attraverso l'albero shadow, usa event.composedPath() — trattato nel prossimo paragrafo.

Utilizzo di event.composedPath()

Poiché il retargeting nasconde l'elemento interno da event.target, hai bisogno di un altro modo per esaminare il percorso di propagazione reale. event.composedPath() restituisce un array dei nodi attraverso cui è passato l'evento, inclusi i nodi all'interno di qualsiasi albero shadow attraversato, ordinati dall'elemento più interno verso l'esterno fino a window.

Questo è il modo affidabile per rispondere alla domanda "quale elemento interno è stato effettivamente cliccato?" da un listener nel light DOM — ma solo per i componenti la cui shadow root è mode: 'open'. Per una root mode: 'closed', composedPath() si ferma all'host e i nodi interni vengono omessi, preservando la privacy del componente chiuso.

Vediamo come event.composedPath() può essere usato per tracciare la propagazione degli eventi all'interno dello 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>

Cliccando il <div> interno vengono elencati tutti i nodi nel percorso composto: inizia con il DIV su cui hai cliccato, poi DIV (l'host #outer), poi BODY, HTML e infine le voci per document e window (che vengono visualizzate come undefined perché non hanno tagName). Le prime voci sono esattamente ciò che event.target nasconde ai listener nel light DOM.

Comprensione di event.composed

La proprietà di sola lettura event.composed è un boolean: true se l'evento può attraversare i confini shadow, false se è confinato al proprio albero shadow. Non è possibile impostarla dopo il fatto — per gli eventi nativi è fissata dalla specifica, mentre per i custom event la si imposta al momento della costruzione dell'evento tramite l'opzione composed.

Questo flag è importante soprattutto quando si costruisce un componente e si deve decidere se i propri custom event debbano uscire. Gli eventi di interazione nativi come click sono composed per impostazione predefinita; i propri CustomEvent non sono composed a meno che non lo si specifichi esplicitamente.

Vediamo come event.composed può essere utilizzato in pratica:

<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 questo esempio, cliccando il pulsante all'interno dello shadow DOM viene attivato un evento click. Creiamo dinamicamente un elemento <p> per visualizzare la proprietà event.composed all'interno dello shadow DOM.

Custom Event nello Shadow DOM

I custom event permettono a un componente di comunicare informazioni al mondo esterno — "valore modificato", "elemento selezionato", "finestra di dialogo chiusa" — senza esporre i propri dettagli interni. Questo è il modo standard in cui un web component comunica con la pagina che lo utilizza. (Vedi Dispatching Custom Events per i dettagli sull'API.)

Affinché un custom event raggiunga un listener sull'elemento host nel light DOM, sono necessarie due opzioni:

  • composed: true — permette all'evento di attraversare il confine shadow.
  • bubbles: true — permette all'evento di risalire l'albero per raggiungere i listener antenato.

Impostando solo bubbles, l'evento fa bubbling all'interno dell'albero shadow ma si ferma alla shadow root. Impostando solo composed, attraversa il confine ma non risale verso gli antenati. Nella maggior parte dei casi si vogliono entrambe.

Creiamo e generiamo un custom event all'interno di uno 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>

Cliccando il pulsante viene generato customEvent con entrambi bubbles: true e composed: true, quindi attraversa il confine shadow e fa bubbling fino al listener sull'host (container) nel light DOM. Per trasmettere dati insieme all'evento, usa la proprietà detail:

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

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

Nota che anche se l'evento raggiunge l'host, il retargeting si applica comunque: nel listener di container, event.target è l'elemento host, non il button interno. Usa event.composedPath()[0] se hai bisogno del target originale.

Riferimento rapido

Proprietà / metodoCosa indica
event.composedtrue se l'evento può attraversare i confini shadow (di sola lettura).
event.composedPath()Array di nodi attraversati dall'evento, inclusi gli alberi shadow aperti, dall'interno verso l'esterno.
event.target (dal light DOM)L'elemento host, a causa del retargeting — mai il nodo interno privato.
Opzione bubblesPermette a un custom event di risalire l'albero.
Opzione composedPermette a un custom event di uscire dall'albero shadow.

Errori comuni

  • Dimenticare composed: true sui custom event. Un custom event con solo bubbles si ferma silenziosamente alla shadow root e non raggiunge mai la pagina host — un frequente bug del tipo "il mio listener non si attiva".
  • Leggere event.target dall'esterno. È reindirizzato all'host. Usa event.composedPath() quando hai bisogno del target interno reale.
  • focus non è composed. Usa focusin/focusout se hai bisogno che le modifiche del focus raggiungano l'host.
  • Shadow root closed. composedPath() non rivelerà i nodi all'interno di una root mode: 'closed', quindi non fare affidamento su di essa per ispezionare componenti chiusi.

Capitoli correlati

Conclusione

Gli eventi nello Shadow DOM seguono alcune regole chiare: gli eventi composed attraversano il confine, quelli non composed no, e gli eventi composed vengono reindirizzati all'host in modo che i dettagli interni restino privati. Usa event.composed per verificare se un evento può attraversare il confine, event.composedPath() per recuperare il percorso reale, e CustomEvent con bubbles: true e composed: true per permettere ai tuoi componenti di comunicare con la pagina che li ospita.

Esercizi

Pratica
Quale metodo permette di recuperare la sequenza di elementi DOM che un evento attraversa durante la sua propagazione?
Quale metodo permette di recuperare la sequenza di elementi DOM che un evento attraversa durante la sua propagazione?
Pratica
Quali opzioni deve avere un CustomEvent affinché un listener sull'elemento host nel light DOM possa intercettarlo?
Quali opzioni deve avere un CustomEvent affinché un listener sull'elemento host nel light DOM possa intercettarlo?
Pratica
Da un listener nel light DOM, a cosa punta event.target quando si fa clic all'interno di un albero shadow aperto?
Da un listener nel light DOM, a cosa punta event.target quando si fa clic all'interno di un albero shadow aperto?
Was this page helpful?