シャドウツリーの背後にある考え方は、コンポーネントの内部実装の詳細をカプセル化することです。
例えば、<user-card>
コンポーネントのShadow DOM内でクリックイベントが発生したとします。しかし、メインドキュメントのスクリプトは、特にコンポーネントがサードパーティライブラリから提供されている場合、Shadow DOMの内部については何も知りません。
そのため、詳細をカプセル化するために、ブラウザはイベントを *再ターゲティング* します。
Shadow DOMで発生するイベントは、コンポーネントの外側からキャッチされた場合、ホスト要素をターゲットとして持ちます。
簡単な例を次に示します。
<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>
ボタンをクリックすると、メッセージが表示されます。
- 内部ターゲット:
BUTTON
- 内部イベントハンドラは、Shadow DOM内の要素である正しいターゲットを取得します。 - 外部ターゲット:
USER-CARD
- ドキュメントイベントハンドラは、シャドウホストをターゲットとして取得します。
イベントの再ターゲティングは、外部ドキュメントがコンポーネントの内部を知る必要がないため、非常に便利です。外部ドキュメントの観点からは、イベントは<user-card>
で発生しました。
スロットされた要素(物理的にライトDOMに存在する要素)でイベントが発生した場合、再ターゲティングは発生しません。
例えば、下の例で<span slot="username">
をクリックした場合、シャドウとライトの両方のハンドラでイベントターゲットはまさにこのspan
要素になります。
<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>
「John Smith」をクリックした場合、内部と外部の両方のハンドラでターゲットは<span slot="username">
になります。これはライトDOMの要素であるため、再ターゲティングは行われません。
一方、Shadow DOMに由来する要素(例えば、<b>Name</b>
)をクリックした場合、Shadow DOMからバブルアップする際に、そのevent.target
は<user-card>
にリセットされます。
バブリング、event.composedPath()
イベントのバブリングの目的では、フラット化されたDOMが使用されます。
したがって、スロットされた要素があり、その内部でイベントが発生した場合、そのイベントは<slot>
まで、そして上方にバブルアップします。
すべてのシャドウ要素を含む元のイベントターゲットへの完全なパスは、event.composedPath()
を使用して取得できます。メソッドの名前からもわかるように、そのパスは合成後に取得されます。
上記の例では、フラット化されたDOMは次のようになります。
<user-card id="userCard">
#shadow-root
<div>
<b>Name:</b>
<slot name="username">
<span slot="username">John Smith</span>
</slot>
</div>
</user-card>
そのため、<span slot="username">
をクリックした場合、event.composedPath()
の呼び出しは、[span
, slot
, div
, shadow-root
, user-card
, body
, html
, document
, window
]という配列を返します。これは、合成後のフラット化されたDOMにおけるターゲット要素からの親チェーンと完全に一致します。
{mode:'open'}
ツリーに対してのみ提供されます。シャドウツリーが{mode: 'closed'}
で作成された場合、合成されたパスはホスト: user-card
から上方に始まります。
これは、Shadow DOMで動作する他のメソッドと同様の原理です。クローズドツリーの内部は完全に隠されています。
event.composed
ほとんどのイベントは、Shadow DOMの境界を正常にバブルアップします。しかし、そうではないイベントもいくつかあります。
これは、composed
イベントオブジェクトのプロパティによって制御されます。これがtrue
の場合、イベントは境界を越えます。それ以外の場合は、Shadow DOMの内側からしかキャッチできません。
UI Events仕様を見ると、ほとんどのイベントはcomposed: true
になっています。
blur
、focus
、focusin
、focusout
、click
、dblclick
、mousedown
、mouseup
、mousemove
、mouseout
、mouseover
、wheel
,beforeinput
、input
、keydown
、keyup
。
すべてのタッチイベントとポインターイベントにもcomposed: true
が設定されています。
ただし、composed: false
のイベントもあります。
mouseenter
、mouseleave
(まったくバブルアップしません)、load
、unload
、abort
、error
、select
,slotchange
.
これらのイベントは、イベントターゲットが存在する同じDOM内の要素でのみキャッチできます。
カスタムイベント
カスタムイベントをディスパッチする場合は、バブルアップしてコンポーネントの外に出るように、bubbles
とcomposed
プロパティの両方をtrue
に設定する必要があります。
例えば、ここではdiv#outer
のShadow DOMにdiv#inner
を作成し、その上で2つのイベントをトリガーします。composed: true
が設定されているものだけが、ドキュメントの外側に到達します。
<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>
まとめ
イベントは、composed
フラグがtrue
に設定されている場合にのみ、Shadow DOMの境界を越えます。
組み込みイベントのほとんどは、関連する仕様で説明されているように、composed: true
になっています。
- UI Events https://www.w3.org/TR/uievents。
- Touch Events https://w3c.github.io/touch-events。
- Pointer Events https://www.w3.org/TR/pointerevents。
- …など。
composed: false
の組み込みイベントの一部
mouseenter
、mouseleave
(バブルアップもしません)、load
、unload
、abort
、error
、select
,slotchange
.
これらのイベントは、同じDOM内の要素でのみキャッチできます。
CustomEvent
をディスパッチする場合は、明示的にcomposed: true
を設定する必要があります。
入れ子になったコンポーネントの場合、1つのShadow DOMが別のShadow DOMに入れ子になる可能性があることに注意してください。その場合、合成されたイベントはすべてのShadow DOM境界をバブルアップします。したがって、イベントが直近の包含コンポーネントのみに向けられている場合は、シャドウホストでディスパッチし、composed: false
を設定することもできます。そうすると、コンポーネントのShadow DOMの外に出ますが、上位レベルのDOMにはバブルアップしません。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen…)を使用してください。