shadow DOM の使い方

 

Web コンポーネントにおいてカプセル化 (構造やスタイル、挙動を隠し、同じページの他のコードと分離すること) は重要です。これにより他の場所でのクラッシュを防ぎ、またコードが綺麗になります。Shadow DOM API はこの隠され分離された DOM を付加するための方法を提供しています。この記事では Shadow DOM を使う基本を記述しています。

Note: Shadow DOM は Firefox (version 63以降)、Chrome、Opera、そして Safari でサポートされています。 Edge も実装に取り組んでいます。

High-level view

この記事では DOM (Document Object Model) —ドキュメントにある要素やテキストを表現するノードによって構成された木構造 — に親しんでいる前提で説明します。例として以下の HTML フラグメントを考えます。 

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Simple DOM example</title>
  </head>
  <body>
      <section>
        <img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth.">
        <p>Here we will add a link to the <a href="https://www.mozilla.org/">Mozilla homepage</a></p>
      </section>
  </body>
</html>

このフラグメントによって以下のような DOM 構造が構成されます。

Shadow DOM により、通常の DOM ツリーの要素の下に DOM ツリーを追加し隠すことができます。shadow DOM ツリーは shadow root を根とし、その下には普通の DOM ツリーと同様に任意の要素を追加できます。

以下に shadow DOM における用語を定義します。

  • Shadow host: shadow DOM が追加された、通常の DOM ノード
  • Shadow tree: shadow DOM の中にある DOM ツリー
  • Shadow boundary: shadow DOM と通常の DOM の境界
  • Shadow root: shadow ツリーの根ノード

shadow DOM 内のノードは、子ノードを追加したり属性付けをしたり、個々のノードのスタイルを element.style.foo と定めたり shadow DOM ツリー全体のスタイルを <style> 要素で定めたりなど、通常の DOM のノードと同様に制御できます。ただし、shadow DOM の内部コードによって外部を制御することは出来ません。 

shadow DOM は全く新しいものではなく、例えばブラウザにおいて要素の内部構造をカプセル化するために長年使用されていました。 <video> 要素の例を考えます。DOM で見えるものは <video> 要素のみですが、その shadow DOM の内部ではたくさんのボタンや他の制御コードが含まれています。shadow DOM スペックができたことにより、この機能を実際に操作しカスタム要素で shadow DOM を作ることができるようになりました。 

基本的な使い方

shadow root は Element.attachShadow() メソッドを利用して任意の要素に追加することができます。このメソッドではパラメータとして mode オプションを open または closed の値で取ります。 

let shadow = elementRef.attachShadow({mode: 'open'});
let shadow = elementRef.attachShadow({mode: 'closed'});

open の場合は shadow DOM の内部にメインページに書かれた JavaScript からアクセスできます。以下のように Element.shadowRoot プロパティを利用してアクセスできます。

let myShadowDom = myCustomElem.shadowRoot;

shadow root を mode: closed で追加した場合、外部から shadow DOM にアクセス出来ず、myCustomElem.shadowRoot は null を返します。<video> などの shadow DOM を含む既成の要素は closed になっています。

Note: このブログ記事を見るに、実はclosed shadow DOMを回避するのはそんなに難しいことではなく、また、これを完全に隠すことはその価値の割には面倒です。

shadow DOM をコンストラクタの一部としてカスタム要素に追加することを考えます。

let shadow = this.attachShadow({mode: 'open'});

shadow DOM は、通常の DOM の操作に使われる DOM API で操作することができます。

var para = document.createElement('p');
shadow.appendChild(para);
// etc.

実用例

カスタム要素内のシンプルなshadow DOM を見てみましょう — <popup-info-box> (実行例)。このタグはイメージアイコンとテキストを取り、アイコンをページに埋め込みます。アイコンがフォーカスされるとポップアップが表示され、さらなる情報を提供します。
まずは HTMLElement を拡張して PopUpInfo というクラスを定義します。

class PopUpInfo extends HTMLElement {
  constructor() {
    // Always call super first in constructor
    super();

    // write element functionality in here

    ...
  }
}

クラス定義の際、インスタンスが初期化された時に用意されるあらゆる関数を定義したコンストラクタを定義します。

shadow root の作成

最初に shadow root をカスタム要素に追加します。

// Create a shadow root
var shadow = this.attachShadow({mode: 'open'});

shadow DOM 構造の作成

次に shadow DOM 構造を作ります。

// Create spans
var wrapper = document.createElement('span');
wrapper.setAttribute('class','wrapper');
var icon = document.createElement('span');
icon.setAttribute('class','icon');
icon.setAttribute('tabindex', 0);
var info = document.createElement('span');
info.setAttribute('class','info');

// Take attribute content and put it inside the info span
var text = this.getAttribute('text');
info.textContent = text;

// Insert icon
var imgUrl;
if(this.hasAttribute('img')) {
  imgUrl = this.getAttribute('img');
} else {
  imgUrl = 'img/default.png';
}
var img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);

shadow DOM のスタイル

そのあと、 <style> 要素を作り CSS でスタイルを付けます。

// Create some CSS to apply to the shadow dom
var style = document.createElement('style');

style.textContent = `
.wrapper {
  position: relative;
}

.info {
  font-size: 0.8rem;
  width: 200px;
  display: inline-block;
  border: 1px solid black;
  padding: 10px;
  background: white;
  border-radius: 10px;
  opacity: 0;
  transition: 0.6s all;
  position: absolute;
  bottom: 20px;
  left: 10px;
  z-index: 3;
}

img {
  width: 1.2rem;
}

.icon:hover + .info, .icon:focus + .info {
  opacity: 1;
}`;

shadow DOM を shadow root に追加

最後に作成した全ての要素を shadow root に追加します。

// attach the created elements to the shadow dom
shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);

カスタム要素の使用

クラスを定義すると、定義したようにカスタム要素を使用することができます。(Using custom elements)

// Define the new element
customElements.define('popup-info', PopUpInfo);
<popup-info img="img/alt.png" text="Your card validation code (CVC) is an extra 
                                    security feature — it is the last 3 or 4
                                    numbers on the back of your card.">