ShadowDOMをなんとなくお勉強するための走り書き（＋ちょっとCustomElement）

これはShadow DOMとは何かを、Custom Elementsのさわりまでを含めて説明するときに書いたメモ書きをまとめたものです。
Shadow DOMはウィジェットコンポーネントや、ブックマークレットツール作るときとかに便利だったりします。

## 第1章 要素を作ってページに追加する

まずは**Shadow DOMを使わない**例です。
1つの`p`要素を持った`div`要素を、ページの最後に追加してみましょう。

```javascript
const { body } = document; // documentオブジェクトに生えているbody要素を変数化する
const div = document.createElement('div'); // div要素を作る

div.insertAdjacentHTML('afterbegin', '<p>Hello Shadow DOM!</p>'); // div要素に新しくHTMLを書き込む
body.append(div); // body要素の最後にdiv要素を追加する
```

## 第2章 Shadow rootを作る

Shadow DOM では、「**Shadow host**」と「**Shadow root**」というキーワードが出てきます。

### `div`要素を Shadow host に変換

Shadow host とは、Shadow root を持つ要素のことです。
さっそく`attachShadow()`メソッドを利用して、追加する`div`要素を Shadow root に変換してみましょう。

```javascript
const { body } = document;
const div = document.createElement('div');

div.attachShadow({ mode: 'open' }); // div要素が Shadow host に変換される

div.insertAdjacentHTML('afterbegin', '<p>Hello Shadow DOM!</p>');

body.append(div);
```

Shadow host に変換すると、`div`要素の中にあった`p`要素が見えなくなります。
とはいえ、`p`要素が削除されたわけではないことが開発者ツールで確認できます。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142709/d117d056-2576-98fa-5208-4472323cd18a.png)

### Shadow root を取得

Shadow root は、**Shadow DOM を扱う領域**です。

通常のDOMツリーと異なり、Shadow root の中にあるDOMツリーを Shadow tree と言います。
Shadow root は`attachShadow()`の返り値、または Shadow host の`shadowRoot`プロパティから参照できます。

```javascript
const { body } = document;
const div = document.createElement('div');

const shadowRoot = div.attachShadow({ mode: 'open' }); // 返り値を取得

div.insertAdjacentHTML('afterbegin', '<p>Hello Shadow DOM!</p>');

body.append(div);


// attachShadow() の返り値で取得したもの
console.log(shadowRoot); // > #shadow-root (open)
// shadowRoot プロパティからの参照
console.log(div.shadowRoot); // > #shadow-root (open)
// どっちも同じもの
console.log(shadowRoot === div.shadowRoot); // > true
```

## 第3章 Shadow root を closed モードで作る

Closed Shadow DOMは、あとから`shadowRoot`を参照することができなくするための方法です。

`attachShadow()`に渡すオプションで、`mode`プロパティに文字列`'closed'`を渡します。
この方法で生成された Shadow root は、最初の`attachShadow()`の返り値以外の手段では取得できなくなります[^1]。

[^1]: そのメリットについてはこちらを参照：[Open vs. Closed Shadow DOM](https://blog.revillweb.com/open-vs-closed-shadow-dom-9f3d7427d1af?source=social.tw)

```javascript
const { body } = document;
const div = document.createElement('div');
const shadowRoot = div.attachShadow({ mode: 'closed' });

div.insertAdjacentHTML('afterbegin', '<p>Hello Shadow DOM!</p>');

body.append(div);


// attachShadow() の返り値で取得したもの
console.log(shadowRoot); // > #shadow-root (closed)
// shadowRoot プロパティからの参照
console.log(div.shadowRoot); // > null
// 不一致
console.log(shadowRoot === div.shadowRoot); // > false
```


## 第4章 【slot編】Shadow root の中にコンテンツを表示してみる

Shadow host になっている要素は自信のコンテンツの中身の代わりに、Shadow root の中身を表示します。
`slot`要素を使うことで、Shadow **host**（`div`要素）の中身を Shadow **root** の中（Shadow DOM）に出力できます。

```javascript
const { body } = document;
const div = document.createElement('div');
const shadowRoot = div.attachShadow({ mode: 'open' });

const slot = document.createElement('slot'); // slot要素を作る

div.insertAdjacentHTML('afterbegin', '<p>Hello Shadow DOM!</p>');

shadowRoot.append(slot); // Shadow root に追加する

body.append(div);
```

`slot`要素を作成し、Shadow root に追加すると、画面に「Hello Shadow DOM!」というコンテンツを持った`p`要素が表示されるようになります。

開発者ツールを使うと、Shadow root の中に`p`要素が**疑似的に**コピーされて表示されているのが確認できます。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142709/725510d0-d260-635b-6343-689f1002ee1c.png)


## 第5章 【slot編】Shadow rootの中で、狙ったコンテンツを狙って位置に表示してみる

ただ`slot`要素を埋め込むだけだと、Shadow host の中身が**区別なくすべてコピーされて**いました。
特定の個所に、特定のコンテンツを出力したいような場合には、`slot`要素と`slot`属性を利用します。

:::note warn
`slot`要素と`slot`属性はそれぞれ役割が異なる点に注意が必要です。
:::

`name`属性を持つ`slot`要素は、「同じ値を`slot`属性に持つ要素を表示する」という機能を持つ要素です。

次のコードを実行してみると、`slot`属性を持っていない要素は表示されません。
一方で`slot`属性を持っている要素は、`name`属性が対応している`slot`要素の中身にコピーされています。

```javascript
const { body } = document;
const div = document.createElement('div');
const shadowRoot = div.attachShadow({ mode: 'closed' });

//　slot 要素を作り name属性に name を指定
const slotForName = document.createElement('slot');
slotForName.name = 'name';

//　slot 要素を作り name属性に age を指定
const slotForAge = document.createElement('slot');
slotForAge.name = 'age';

// slot 属性にそれぞれ対応させたい slot 要素の name 属性値を指定する
div.insertAdjacentHTML('afterbegin', `
  <p>slot属性を持たないテキスト</p>
  <p slot="name">山田太郎</p>
  <p slot="age">17歳</p>
`);

// slot 要素を Shadow root に追加
shadowRoot.append(slotForName);
shadowRoot.append(slotForAge);

body.append(div);
```


### 【イメージ】Shadow host化した`div`要素と Shadow root の中身

実際は`shadow-root`要素のようなものは存在しませんが、イメージするならこういう構造になります。
本来のDOMツリー上には存在しない、影のような存在であるところが Shadow DOM の特徴です。

```html
<div>
  <shadow-root>
    <slot name="name"></slot>
    <slot name="age"></slot>
  </shadow-root>
  <p>slot属性を持たないテキスト</p>
  <p slot="name">山田太郎</p>
  <p slot="age">17歳</p>
</div>
```

### 【イメージ】表示上はこんな感じ

`slot`**要素**の中に、`name`属性値と対応する`slot`**属性**値を持つ要素が埋め込まれます。

```html
<div>
  <!-- #shadow-root -->
    <slot name="name"><p slot="name">山田太郎</p></slot>
    <slot name="age"><p slot="age">17歳</p></slot>
  <!-- /#shadow-root -->

  <!-- <p>slot属性を持たないテキスト</p> -->
  <!-- <p slot="name">山田太郎</p>   -->
  <!-- <p slot="age">17歳</p>      -->
</div>
```


## 第6章 Shadow rootの中にコンテンツを表示してみる（template編）

Shadow DOMの良さがグッとわかるタイミングは、`template`要素＋`slot`要素やCustom Elementと組み合わせたときだと思います。
まずは、`template`要素と`slot`要素を活用する方法を試してみましょう。

記事一覧、製品一覧など、同じ構成のものを複数動的出力（JavaScriptで読み込み、出力）する作るとき特に便利です。

1. `template#template`要素の中に出力するときの**構造のみのHTML**を記述する
2. 表示させたい「情報」だけを実際に使う出力したい情報を`div#card`要素に記述する
3. `div#card`要素にShadow rootを作る（Shadow host化）（A）
4. `tempalte#tempate`要素の中身を`closeNode(true)`で複製し、Shadow rootに追加する（B）

以上のステップを踏むと、`div#card`要素の中に、「情報」を持った`table`要素が表示されます。

```html
<template id="template">
  <table>
    <tbody>
      <tr>
        <th>ID</th>
        <td><slot name="user-id"></slot></td>
      </tr>

      <tr>
        <th>名前</th>
        <td><slot name="user-name"></slot></td>
      </tr>

      <tr>
        <th>本文</th>
        <td><slot name="user-description"></slot></td>
      </tr>

      <tr>
        <th>投稿日</th>
        <td><slot name="user-date"></slot></td>
      </tr>
    </tbody>
  </table>
</template>

<div id="card">
  <span slot="user-id">@kawarabeEcma</span>
  <span slot="user-name">河童エクマ</span>
  <span slot="user-description">人間の姿にがんばって化けている働く河童。</span>
  <span slot="user-date">1月1日</span>
</div>

<script>
const template = document.querySelector('#template');
const div = document.querySelector('#card');
// (A)
const shadow = div.attachShadow({
  mode: 'closed',
});

// (B)
shadow.append(template.content.cloneNode(true));
</script>
```


## 第7章 複数のShadow rootを作ってみる

6章では1つの`template`要素と、1つのShadow host（「情報」だけが書かれたマークアップ）の組み合わせを行いました。

ここでは1つの`template`要素と、複数のShadow hostを組み合わせてみます。
`class`属性と`for...of`を使って、ループ文で処理してみた例がこちら！

```html
<template id="template">
  <table>
    <tbody>
      <tr>
        <th>ID</th>
        <td><slot name="user-id"></slot></td>
      </tr>

      <tr>
        <th>名前</th>
        <td><slot name="user-name"></slot></td>
      </tr>

      <tr>
        <th>本文</th>
        <td><slot name="user-description"></slot></td>
      </tr>

      <tr>
        <th>投稿日</th>
        <td><slot name="user-date"></slot></td>
      </tr>
    </tbody>
  </table>
</template>


<div class="card">
  <span slot="user-id">@kawarabeEcma</span>
  <span slot="user-name">河童エクマ</span>
  <span slot="user-description">こんエクマ～！</span>
  <span slot="user-date">1月1日</span>
</div>


<div class="card">
  <span slot="user-id">@hogehoge</span>
  <span slot="user-name">田中太郎</span>
  <span slot="user-description">おはようおはよう</span>
  <span slot="user-date">1月2日</span>
</div>


<div class="card">
  <span slot="user-id">@piyopiyo</span>
  <span slot="user-name">山田太郎</span>
  <span slot="user-description">おはこんばんにちは</span>
  <span slot="user-date">1月3日</span>
</div>


<script>
const template = document.querySelector('#template');
const cards = document.querySelectorAll('.card');

for (const div of cards) {
  const shadow = div.attachShadow({
    mode: 'closed',
  });

  shadow.append(template.content.cloneNode(true));
}
</script>
```


## 第8章 オリジナルの要素を使ってみる（Custom Elements編）

続いて、自分だけの新しい要素が作れるCustom Elementを試してみしょう。
先ほどは`class`属性を使って自主的にDOM探索をしてShadow rootを作っていましたが、Custom Elementを使うことでDOM探索をする必要もなくなります。

※ オリジナルの要素名は、必ずハイフン（`-`）を１つ含める必要があります。

```html
<hoge-piyo>
  <span slot="user-id">@kawarabeEcma</span>
  <span slot="user-name">河童エクマ</span>
  <span slot="user-description">こんエクマ～！</span>
  <span slot="user-date">1月1日</span>
</hoge-piyo>


<hoge-piyo>
  <span slot="user-id">@hogehoge</span>
  <span slot="user-name">田中太郎</span>
  <span slot="user-description">おはようおはよう</span>
  <span slot="user-date">1月2日</span>
</hoge-piyo>


<hoge-piyo>
  <span slot="user-id">@piyopiyo</span>
  <span slot="user-name">山田太郎</span>
  <span slot="user-description">おはこんばんにちは</span>
  <span slot="user-date">1月3日</span>
</hoge-piyo>


<template id="template">
  <table>
    <tbody>
      <tr>
        <th>ID</th>
        <td><slot name="user-id"></slot></td>
      </tr>

      <tr>
        <th>名前</th>
        <td><slot name="user-name"></slot></td>
      </tr>

      <tr>
        <th>本文</th>
        <td><slot name="user-description"></slot></td>
      </tr>

      <tr>
        <th>投稿日</th>
        <td><slot name="user-date"></slot></td>
      </tr>
    </tbody>
  </table>
</template>


<script>
/** hoge-piyo要素の振る舞いを定義するclassです */
class HogePiyoHTMLElement extends HTMLElement {
  /** 要素が生まれたときに何をするかを書くところ */
  constructor() {
    super(); // おまじない。詳しくはclassの継承。

    const template = document.querySelector('#template');
    const shadow = this.attachShadow({
      mode: 'closed',
    });

    shadow.append(template.content.cloneNode(true));
  }
}

// 作ったオリジナルの要素をブラウザに覚えてもらう
customElements.define('hoge-piyo', HogePiyoHTMLElement);
</script>
```

`class`を使って`hoge-piyo`要素が生み出されたとき、自動的に`template#template`要素を使って画面に出力する、という動きが定義されました。
これは元々HTMLに書かれていた`hoge-piyo`要素のみならず、JavaScriptで作ったものにも、しっかり自動的に反応してくれます。

```javascript
const newHogePiyo = document.createElement('hoge-piyo');

newHogePiyo.insertAdjacentHTML('afterbegin',  = `
  <span slot="user-id">@ponyoponyo</span>
  <span slot="user-name">佐藤太郎</span>
  <span slot="user-description">あたらしいやつ</span>
  <span slot="user-date">1月4日</span>
`;
document.body.append(newHogePiyo); // ちゃんとテンプレにはまって表示される
```


## 第9章 Shadow DOMの中身をスタイリングしたい

通常のCSSはShadow rootの中に影響を及ぼさず、Shadow rootの中にあるCSSはその外に影響しません。
これはShadow DOMの最も大きな特徴だと思います。
これまで`iframe`要素で実現してきた埋め込み系ウィジェットや、ブックマークレットなどで操作パネルなどを画面上に表示させたい場合に、既存スタイルに影響されず安心してスタイリングができます。

Shadow rootの中をスタイリングするには、Shadow rootの中で`style`要素を埋め込むか、`link`要素で外部CSSを読み込むしかありません。
Shadow rootの中の`link`要素はレンダリングをブロックしないため、外部CSSを読み込もうとするとFOUC現象が発生する可能性があります。同様の`textContent`を持つ`style`要素が複数個所にいた場合でも、ブラウザがうまく最適化してくれるので、どちらの実装方法でもパフォーマンスは同様です[^2]。

[^2]: [shadow DOM の使い方](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_shadow_DOM)

なお、Shadow hostになっている要素をShadow rootの中からスタイリングしたい場合は、`:host`というセレクタが利用できます。

```css
:host {
  background: red;
}
```

:::note
💡 2023年現在は`style`要素を設置せずに、ShadowDOM自体に適応できます
```js
const sheet = new CSSStyleSheet();
const shadow = node.attachShadow({ mode: "closed" });

sheet.replaceSync(`
  :host {
    background: red;
  }
`);

shadow.adoptedStyleSheets = [sheet];
```

See also: [Document: adoptedStyleSheets property - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets)
:::

これでShadow hostになっている要素の背景が赤くなります。

ただし、すでに通常のCSSでShadow host化したにスタイルが当たっている場合はそちらのほうが優先度が高くなることに注意が必要です。

### 第4章のコードでスタイリングしてみよう。

次の例では、うまく文字の色が赤くなりません。
少し難しい部分ですが、これは**実際に`p`要素がShadow rootの中にいるわけではない**ためです。

あくまでもこの例で登場する`style`要素は、Shadow rootの中にいるものです。そのため、「Shadow hostそのもの」と、そのShadow rootの中身しか装飾できません。

```html
<div>
  <!-- #shadow-root -->
    <slot><p>こんえくま！🥒</p></slot>   <!-- このp要素はここにいるように見えるけれど、実際にはいないのでCSSが当たらない -->
    <style>
      /* shadow-rootの中しかCSSを当てられず、`slot`要素の中身にはCSSが当たらない */

      :host {
        /* ルートの`div`要素を装飾できる */
      }
    </style>
  <!-- /#shadow-root -->

  <!-- <p>こんえくま！🥒</p> -->
</div>
```

`template`要素との組み合わせのタイミングで`style`要素を作成するか、`template`要素の中に`style`要素ごと記述することで、一緒にShadow rootの中に設置していく必要があります。
それを考慮したうえで`template`要素の構造を考えてみましょう。

```javascript
// 第４章の例
const div = document.createElement('div');
const slot = document.createElement('slot');
const style = document.createElement('style'); // style要素を作成
const shadow = div.attachShadow({
  mode: 'closed',
});

div.insertAdjacentHTML('afterbegin', '<p>こんえくま！🥒</p>');
// CSSを記述
style.textContent = `
  :host {
    background: pink;
  }
  slot {
    color: blue; /* 当たる */
  }
  p  {
    color: red; /* うまく当たらない */
  }
`;
shadow.append(slot);
shadow.append(style); // Shadow rootに追加
document.body.append(div);
```

```js
// Custom Elementの例
class HogePiyoHTMLElement extends HTMLElement {
  cssText = `
    :host {
      display: block;
      background: pink;
    }
    slot {
      color: blue; /* 当たる */
    }
    span  {
      color: red; /* うまく当たらない */
    }
  `

  constructor() {
    super();

    const template = document.querySelector('#template');
    const style = document.createElement('style'); // style要素を作成
    const shadow = this.attachShadow({
      mode: 'closed',
    });

    // CSSを記述
    style.textContent = this.cssText;
    shadow.append(template.content.cloneNode(true));
    shadow.append(style); // Shadow rootに追加
  }
}

// 作ったオリジナルの要素をブラウザに覚えてもらう
customElements.define('hoge-piyo', HogePiyoHTMLElement);
```


### おまけ：Custom Elementの良さ

独自の命名規則で属性を定義できたり、アクセサを使うことでIDL属性のように実装することができるところも面白いですが、１番の魅力は1つの要素だけで1つのUIとして完結させられるところではないでしょうか。

たとえば、`input`要素や`textarea`要素はHTMLに記述するだけで、文字が書けてサイズも変更できるものが生まれます。
`video[controls]`要素も置いただけで操作可能なプレイヤーがすぐに生成されますが、これらのような独自の機能をもったものはShadow DOMが使われています。

Google Chromeの開発者ツールでは、次の手順でその実装の構造（Shadow hostの中身）を確認できるようになります。
実際に`input`要素などを確認してみてください。

1. 開発者ツールを開く（F12、Command+Option+l）
2. 「Settings」を開く（歯車のマーク）
3. 「Preferences」の「Elements」の「Show user agent shadow DOM」にチェックを入れる

第8章などの`hoge-piyo`要素の例では、Custom Elementが生成されたら決められたテンプレートに従って表示してくれるように`customElement.define()`でブラウザと約束しましたが、これらの要素もそれと同じ理論で「あらかじめブラウザがそういう風に機能を提供するようになっている」というイメージです。

こうすると要素が生まれたタイミングで`attachShadow`をすることや`template`の中身をShadow rootに埋め込んだりすることをブラウザが自動的にやってくれるので、ループ文も必要なくなります。

## 第10章 最後に

ここまで書いてきた内容の全部入りのコードを用意しました。

これをいじくりまわしてShadow DOMで遊んでみましょう！🥒

```html
<style>
#list {
  display: flex;
  flex-wrap: wrap;
}

hoge-piyo {
  flex: 1;
  margin: 10px;
}
</style>

<div id="list">
  <hoge-piyo>
    <span slot="user-id"><a href="https://twitter.com/kawarabeEcma">@kawarabeEcma</a></span>
    <span slot="user-name">河童エクマ</span>
    <span slot="user-description">こんエクマ～！</span>
    <span slot="user-date">1月1日</span>
  </hoge-piyo>

  <hoge-piyo>
    <span slot="user-id">@hogehoge</span>
    <span slot="user-name">田中太郎</span>
    <span slot="user-description">おはようおはよう</span>
    <span slot="user-date">1月2日</span>
  </hoge-piyo>

  <hoge-piyo>
    <span slot="user-id">@piyopiyo</span>
    <span slot="user-name">山田太郎</span>
    <span slot="user-description">おはこんばんにちは</span>
    <span slot="user-date">1月3日</span>
  </hoge-piyo>
</div>

<template id="template">
<style>
  :host {
    background: cyan;
  }

  table {
    border-spacing: 4px;
  }

  th,
  td {
    padding: 10px;
  }

  th {
    white-space: nowrap;
    background: #efefef;
  }
</style>

  <table>
    <tbody>
      <tr>
        <th>ID</th>
        <td><slot name="user-id"></slot></td>
      </tr>

      <tr>
        <th>名前</th>
        <td><slot name="user-name"></slot></td>
      </tr>

      <tr>
        <th>本文</th>
        <td><slot name="user-description"></slot></td>
      </tr>

      <tr>
        <th>投稿日</th>
        <td><slot name="user-date"></slot></td>
      </tr>
    </tbody>
  </table>
</template>

<script>
{
  console.log('hoge-piyo要素の動きを定義します');
  class HogePiyoHTMLElement extends HTMLElement {
    constructor() {
      super();

      const template = document.querySelector('#template');
      const shadow = this.attachShadow({
        mode: 'closed'
      });

      shadow.append(template.content.cloneNode(true));
      console.log('hoge-piyo要素が生まれたよ', this);
    }

    connectedCallback() {
      console.log('hoge-piyo要素が追加されたよ', this);
    }

    disconnectedCallback() {
      console.log('hoge-piyo要素がDOMから削除されたよ', this);
    }
  }

  console.log('hoge-piyo要素をブラウザに知ってもらいます');
  customElements.define('hoge-piyo', HogePiyoHTMLElement);
}

{
  console.log('新しいhoge-piyoを作成してページに追加します。 ---');

  const list = document.querySelector('#list');
  const newHogePiyo = document.createElement('hoge-piyo');

  newHogePiyo.insertAdjacentHTML('afterbegin', `
    <span slot="user-id">@ponyoponyo</span>
    <span slot="user-name">佐藤太郎</span>
    <span slot="user-description">あたらしいやつ</span>
    <span slot="user-date">1月4日</span>
  `);
  list.append(newHogePiyo); // ちゃんとテンプレにはまって表示される
}
</script>
```