概要
Shadow DOM を実際に作成した上で、どの様な動作をするのかを検証してみる。
コレの続き。
まずはタグを作る
簡単なカスタムタグを用意して EchoTag.js
class EchoTag extends HTMLElement { // コンストラクタ constructor() { super(); this._innerText = null; } // この変数に宣言された属性は、追加/削除/更新されたときに attributeChangedCallback が呼ばれる static observedAttributes = ["message"]; // 属性値が更新されたら呼び出されるコールバック。 attributeChangedCallback(name, oldValue, newValue) { this._innerText = newValue; this._updateRendering(); } // このタグが親のタグに配置されると呼び出されるコールバック connectedCallback() { this._updateRendering(); } // JavaScript でタグ作成したときの属性パラメータ get message() { return this._innerText; } set message(v) { this.setAttribute("message", v); } // カスタム関数。ココでは Shadow DOM を宣言して、span タグを作って表示するだけ。 _updateRendering() { this.attachShadow({mode: 'open'}); // this.innerHTML = null; // append child. const spanElement = document.createElement("span"); spanElement.innerHTML = this._innerText; // this.append(spanElement); this.shadowRoot.append(spanElement); } } // カスタム要素として、ブラウザに登録する customElements.define("echo-tag", EchoTag);
表示してみる。
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="./EchoTag.js"></script> </head> <body> <div><span>Example!</span></div> <echo-tag message="Welcome to custom tag!"></echo-tag> </body> </html>
簡単に Python3 で python -m http.server 8000
すると
この #shadow-root
の中身が Shadow DOM の中身である。
Shadow DOM の特性
まず Shadow DOM がどんな特性をもつのか確認する
style 透過性
試しに Style をイジってみる。
index.html
にスタイルを追記する。
<style> span { color: red; } </style>
Shadow DOM モードを Open にしても( this.attachShadow({mode: 'open'});
)通らない。
つまり、カスタムタグはスタイルの境界になるという事だ。
いわずもがな Close モードにしてみたのは次のコード
class EchoTag extends HTMLElement { // コンストラクタ constructor() { super(); this._innerText = null; this.shadow = this.attachShadow({mode: 'closed'}); } // 中略 _updateRendering() { this.shadow.innerHTML = null; // append child. const spanElement = document.createElement("span"); spanElement.innerHTML = this._innerText; this.shadow.append(spanElement); } } // カスタム要素として、ブラウザに登録する customElements.define("echo-tag", EchoTag);
結果は変わらず。
要素の透過性
次に、Console から document.getElementsByTagName
をしてみる。
これも getElementsByTagName
の境界としても機能しているご様子。
イベントの透過性
EchoTag
のソースを一部改変すると
// このタグが親のタグに配置されると呼び出されるコールバック connectedCallback() { this._updateRendering(); this.addEventListener('temp', () => { console.log('Handled at EchoTag'); }); } // カスタム関数。ココでは Shadow DOM を宣言して、span タグを作って表示するだけ。 _updateRendering() { this.shadow.innerHTML = null; // append child. const divElement = document.createElement("div"); divElement.addEventListener('temp', () => { console.log('Called parent listener!'); }); const spanElement = document.createElement("span"); spanElement.innerHTML = this._innerText; spanElement.addEventListener('click', () => { spanElement.dispatchEvent(new CustomEvent('temp', { bubbles: true })) }); spanElement.addEventListener('temp', () => { console.log('Called self dispatch event!'); }); divElement.appendChild(spanElement); this.shadow.append(divElement); }
これで実行してみると
親タグまでしか伝播しない。
もっというと、echo-tag
の内側に Shadow DOM 境界があるので、その外側に伝播しなかったということである。
Shodow DOM の外側にイベントをバブリングする
追加パラメータ入れるとShadowDOMを貫通できる。
// このタグが親のタグに配置されると呼び出されるコールバック connectedCallback() { this._updateRendering(); this.addEventListener('temp', () => { console.log('Handled at EchoTag'); }); } // カスタム関数。ココでは Shadow DOM を宣言して、span タグを作って表示するだけ。 _updateRendering() { this.shadow.innerHTML = null; // append child. const divElement = document.createElement("div"); divElement.addEventListener('temp', () => { console.log('Called parent listener!'); }); const spanElement = document.createElement("span"); spanElement.innerHTML = this._innerText; spanElement.addEventListener('click', () => { spanElement.dispatchEvent(new CustomEvent('temp', { bubbles: true, composed: true })) }); spanElement.addEventListener('temp', () => { console.log('Called self dispatch event!'); }); divElement.appendChild(spanElement); this.shadow.append(divElement); }
Open Closed の違い
コレまでだと、Open モードでも境界として機能していることがわかる。
では close
モードは何が違うんだ?という話になるが、Open モードだとこれが許される。
const target = document.querySelector('echo-tag') target.shadowRoot.querySelector('span').innerText = 'HAHAHA';
つまり、ある程度の境界はありつつも、外側からのインジェクションを受け付ける。
ここで、closed
モードで実行してみる。
constructor() { super(); this._innerText = null; this.shadow = this.attachShadow({mode: 'closed'}); }
この状態で、もう一度同じことをしてみると
中の要素にアクセスできなくなる。
弱点
このソース、ShadowDOM を次のように作成した。
constructor() { super(); this.shadow = this.attachShadow({mode: 'closed'}); }
これ、shadow
ならアクセスできるんじゃね?と思ってやってみた。
やれちゃった図
LWC とか標準でどんな名前の変数に shadow dom 囲ってるのか知らんのでなんとも言えんけど。