Myšlenkou stínového stromu je zapouzdřit interní implementační detaily komponenty.
Řekněme, že k události kliknutí dojde uvnitř stínového DOM <user-card>
komponent. Ale skripty v hlavním dokumentu nemají ponětí o vnitřních částech stínového DOM, zvláště pokud komponenta pochází z knihovny třetí strany.
Aby tedy zůstaly podrobnosti zapouzdřené, prohlížeč retargetuje událost.
Události, ke kterým dochází ve stínovém DOM, mají jako cíl hostitelský prvek, pokud jsou zachyceny mimo komponentu.
Zde je jednoduchý příklad:
<user-card></user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<p>
<button>Click me</button>
</p>`;
this.shadowRoot.firstElementChild.onclick =
e => alert("Inner target: " + e.target.tagName);
}
});
document.onclick =
e => alert("Outer target: " + e.target.tagName);
</script>
Pokud kliknete na tlačítko, zprávy jsou:
- Vnitřní cíl:
BUTTON
– Interní obsluha události získá správný cíl, prvek uvnitř stínového DOM. - Vnější cíl:
USER-CARD
– obsluha události dokumentu získá jako cíl stínového hostitele.
Retargeting událostí je skvělá věc, protože vnější dokument nemusí vědět o vnitřnostech součástí. Z jeho pohledu se událost stala <user-card>
.
Retargeting nenastane, pokud k události dojde na štěrbinovém prvku, který fyzicky žije ve světlém DOM.
Pokud například uživatel klikne na <span slot="username">
v příkladu níže je cílem události přesně toto span
prvek pro obsluhu stínu i světla:
<user-card id="userCard">
<span slot="username">John Smith</span>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div>
<b>Name:</b> <slot name="username"></slot>
</div>`;
this.shadowRoot.firstElementChild.onclick =
e => alert("Inner target: " + e.target.tagName);
}
});
userCard.onclick = e => alert(`Outer target: ${e.target.tagName}`);
</script>
Pokud dojde ke kliknutí na "John Smith"
, pro vnitřní i vnější ovladače je cílem <span slot="username">
. To je prvek z lehkého DOM, takže žádné přesměrování.
Na druhou stranu, pokud ke kliknutí dojde na prvek pocházející ze stínového DOM, např. na <b>Name</b>
, pak, jak vybublá ze stínového DOM, jeho event.target
je resetováno na <user-card>
.
Bublání, event.composedPath()
Pro účely bublání událostí se používá zploštělý DOM.
Pokud tedy máme štěrbinový prvek a někde v něm dojde k události, pak to probublává až do <slot>
a nahoru.
Úplnou cestu k původnímu cíli události se všemi stínovými prvky lze získat pomocí event.composedPath()
. Jak vidíme z názvu metody, tato cesta je vedena po složení.
Ve výše uvedeném příkladu je zploštělý DOM:
<user-card id="userCard">
#shadow-root
<div>
<b>Name:</b>
<slot name="username">
<span slot="username">John Smith</span>
</slot>
</div>
</user-card>
Takže pro kliknutí na <span slot="username">
, volání na číslo event.composedPath()
vrátí pole:[span
, slot
, div
, shadow-root
, user-card
, body
, html
, document
, window
]. To je přesně nadřazený řetězec z cílového prvku ve zploštělém DOM po složení.
{mode:'open'}
stromy
Pokud byl stínový strom vytvořen s {mode: 'closed'}
, pak složená cesta začíná od hostitele:user-card
a nahoru.
Je to podobný princip jako u jiných metod, které pracují se stínovým DOM. Vnitřnosti uzavřených stromů jsou zcela skryté.
event.composed
Většina událostí úspěšně probublává stínovou hranicí DOM. Existuje jen málo událostí, které ne.
Toto se řídí composed
vlastnost objektu události. Pokud je to true
, pak událost překročí hranici. V opačném případě jej lze zachytit pouze zevnitř stínového DOM.
Pokud se podíváte na specifikaci událostí uživatelského rozhraní, většina událostí má composed: true
:
blur
,focus
,focusin
,focusout
,click
,dblclick
,mousedown
,mouseup
mousemove
,mouseout
,mouseover
,wheel
,beforeinput
,input
,keydown
,keyup
.
Všechny události dotyku a události ukazatele mají také composed: true
.
Některé události mají composed: false
ačkoli:
mouseenter
,mouseleave
(vůbec nebublají),load
,unload
,abort
,error
,select
,slotchange
.
Tyto události lze zachytit pouze u prvků v rámci stejného modelu DOM, kde se nachází cíl události.
Vlastní události
Když odesíláme vlastní události, musíme nastavit obě bubbles
a composed
vlastnosti na true
aby probublával a vycházel ze součásti.
Například zde vytvoříme div#inner
ve stínovém DOM div#outer
a spustit na něm dvě události. Pouze ten s composed: true
přejde mimo dokument:
<div id="outer"></div>
<script>
outer.attachShadow({mode: 'open'});
let inner = document.createElement('div');
outer.shadowRoot.append(inner);
/*
div(id=outer)
#shadow-dom
div(id=inner)
*/
document.addEventListener('test', event => alert(event.detail));
inner.dispatchEvent(new CustomEvent('test', {
bubbles: true,
composed: true,
detail: "composed"
}));
inner.dispatchEvent(new CustomEvent('test', {
bubbles: true,
composed: false,
detail: "not composed"
}));
</script>
Shrnutí
Události překračují hranice stínu DOM pouze v případě, že mají hodnotu composed
příznak je nastaven na true
.
Vestavěné události mají většinou composed: true
, jak je popsáno v příslušných specifikacích:
- Události uživatelského rozhraní https://www.w3.org/TR/uievents.
- Dotkněte se Událostí https://w3c.github.io/touch-events.
- Ukazatelské události https://www.w3.org/TR/pointerevents.
- …A tak dále.
Některé vestavěné události, které mají composed: false
:
mouseenter
,mouseleave
(také nebublá),load
,unload
,abort
,error
,select
,slotchange
.
Tyto události lze zachytit pouze u prvků v rámci stejného modelu DOM.
Pokud odešleme CustomEvent
, pak bychom měli explicitně nastavit composed: true
.
Upozorňujeme, že v případě vnořených komponent může být jeden stínový DOM vnořen do druhého. V takovém případě složené události probublávají všemi stínovými hranicemi DOM. Pokud je tedy událost určena pouze pro bezprostředně obklopující komponentu, můžeme ji také odeslat na stínovém hostiteli a nastavit composed: false
. Pak je mimo stínovou komponentu DOM, ale nepronikne do DOM vyšší úrovně.