【Web Componentsを学ぶ】Shadow DOM編

# はじめに
この記事ではWeb Componentsを利用する準備のためにShadow DOMを紹介する記事です。Web Componentsとは他のコードから独立して利用できる再利用可能なコンポーネント(要素)を指します。
Web Componentを利用するために必要な他の要素として[Custom Elementについての記事](https://qiita.com/KokiSakano/items/1ffa65a1adb3f7c11112)、[HTML Templateについての記事](https://qiita.com/KokiSakano/items/c8f75f4741beca9f02c5)も書きましたので併せてご覧ください。また、Web Components自体の実装は[こちらの記事](https://qiita.com/KokiSakano/items/334e0b3af7d72ee4d230)で行いました。

# Shadow DOMとは
Shadow DOMはカプセル化を行ってくれる機能で、他のコードから独立したDOMを作ることができます。隠された(Shadow) DOMというわけです。Shadow DOMとなった要素は外部からの変更は疎か取得を行うこともできません。Shadow DOMに対して通常のDOMはLight DOMと呼ばれています。

# Shadow DOMを作る
特定の要素に対して`attachShadow`を呼び出すことでその要素をShadow DOM化させることができます。要素自体をShadow Host、要素にぶら下がる構造をShadow Treeと言います。
`attachShadow`は引数にオブジェクトを持ち、`mode`と`delegatesFocus`をキーに持つことができます。`mode`は`'open'`と`'closed'`の2種類のバリューを取ります。`'open'`は外部のJavaScriptからの呼び出しが可能に、`'closed'`は不可能にする設定です。`delegatesFocus`はフォーカスに関する設定で真偽値をバリューに取ります。`true`にした場合はフォーカス不可のShadow DOM内部の要素がクリックされた場合Shadow DOMのフォーカス可能な最初の要素がフォーカスされるようになります。
`attachShadow`の返り値は`ShadowRoot`オブジェクトです。このオブジェクトは[`DocumentFragment`](https://developer.mozilla.org/ja/docs/Web/API/DocumentFragment)を継承しているので通常のNodeと同じように扱うことができます。もちろん専用のプロパティやメソッドも実装されていて`mode`や`getAnimation`などを使えます。
セキュリティの観点などから`attachShadow`は全ての要素に対して与えることはできないことに注意して下さい。与えられる要素は`article`, `aside`, `blockquote`, `body`, `div`, `footer`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `header`, `main` `nav`, `p`, `section`, `span`です。
具体例を用いて挙動を解説します。
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="rNKQZEL" data-editable="true" data-user="kokisakano" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kokisakano/pen/rNKQZEL">
  ShadowDOM5</a> by KokiSakano (<a href="https://codepen.io/kokisakano">@kokisakano</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

この例ではShadow Hostをshadowをidとする`div`要素に、追加した`p`がShadow Treeとなります。次に以下のコードを用いてDOM内の`p`の個数と、shadow持つ`div`から再度`ShadowRoot`を取得してみます。
```js
console.log(document.getElementsByTagName('p').length);
console.log(document.getElementById('shadow').shadowRoot.innerHTML);
```
`p`が存在しているのにDOM内の`p`の個数は0と出力されてしまいました。これはShadow DOMがもつ他のコードと独立させる特徴のためです。この取得方法ではLight DOMからしか取得できないというわけです。外部からのアクセスではこのようにShadow DOMの中身はないものとして扱われます。
`ShadowRoot`の取得は`attachShadow`の返り値の他にShadow Hostの`shadowRoot`プロパティにアクセスすることで行えます。そのため二行目の結果は`"<p>I live in shadow DOM</p>"`となります。`attachShadow`の引数に渡したobjectの`mode`を`closed`とした場合はnullが出力されます。`closed`にした場合は`attachShadow`以外で`shadowRoot`を取得できないので大切に持っておく必要があります。

# Shadow DOMとCSS
Shadow DOMはLight　DOMのCSSの影響を受けないです。同様にShadow DOM内部のCSSはLight DOMに影響はありません。
Light　DOMで`p`のcolorをredに設定した場合でもShadow DOMの`p`のcolorは変化がないままです(importantをつけているのは効果がないことを強調するためです)。
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="yLEQxVr" data-editable="true" data-user="kokisakano" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kokisakano/pen/yLEQxVr">
  ShadowDOM1</a> by KokiSakano (<a href="https://codepen.io/kokisakano">@kokisakano</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

同様にShadow DOM内で`p`のcolorをredに設定した場合でもLight　DOMの`p`のcolorは変化がないままです。
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="BaVGOPq" data-editable="true" data-user="kokisakano" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kokisakano/pen/BaVGOPq">
  ShadowDOM2</a> by KokiSakano (<a href="https://codepen.io/kokisakano">@kokisakano</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

二つの例でShadow DOM内外でcssが分離されていることを確認できました。しかし、実は定義自体は分離されているものの継承は分離されていません。
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="mdKQGaL" data-editable="true" data-user="kokisakano" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kokisakano/pen/mdKQGaL">
  ShadowDOM2</a> by KokiSakano (<a href="https://codepen.io/kokisakano">@kokisakano</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

上記のようにcolorなどのstyleはshadowDOMであっても親から継承されることに注意する必要があります。継承を避けたい場合はShadow DOM内の要素に明示的にstyleを当てるようにします。
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="VwdVGRy" data-editable="true" data-user="kokisakano" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kokisakano/pen/VwdVGRy">
  ShadowDOM4</a> by KokiSakano (<a href="https://codepen.io/kokisakano">@kokisakano</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# まとめ
Web Componentsを利用するために必要なShadow DOMを学びました。この機能を用いて他のコードから分離したカプセル化した要素を作成することができるようになります。