技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

Shadow DOM について

概要

Shadow DOM を実際に作成した上で、どの様な動作をするのかを検証してみる。

white-azalea.hatenablog.jp

コレの続き。

まずはタグを作る

簡単なカスタムタグを用意して 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 囲ってるのか知らんのでなんとも言えんけど。