はじめに

私たちを取り巻くWeb技術は、もはや社会的なインフラとしてめまぐるしく進化しています。HTMLやCSSはもちろんのこと、JavaScriptやライブラリ、フレームワークなど、それぞれがニーズにキャッチアップする形で、機能強化を繰り返しています。その中でも、Web技術の中核に位置するにもかかわらず、意外と見過ごされがちなのがHTMLの進化です。本連載は、このHTMLと関連するJavaScript APIにフォーカスして、その新機能を手軽に試していただこうというものです。理解も利用もたやすいHTMLなので、ライトな気持ちで「こんなことができるようになったのか」を感じていただきます。

[NOTE]サンプルについて
本記事の配布サンプルは、以下のURLから入手できます。新機能を試していくので、ブラウザは最新である方がよいでしょう。本記事のサンプルは、執筆時点で最新のChromeで動作することを確認しています。
https://github.com/wateryinhare62/mynavi_html/

連載第13回の目的

この回では、HTMLだけでShadow DOMを実現できるtemplate要素を試します。
Shadow DOMはWeb Componentsを構成する重要な技術ですが、従来はJavaScriptコードを使って構築する必要がありました。そのため、サーバーサイドでHTMLを生成する場合などには利用しにくいという課題がありました。
そこで登場したのが、宣言的Shadow DOMです。JavaScriptコードを使わず、HTMLだけでShadow DOMを構築できます。
ここでは、従来のJavaScriptによるShadow DOMの構築方法とその課題を確認したうえで、宣言的Shadow DOMによる解決方法を試します。これを通して、現在のWeb Componentsで利用されているShadow DOMの構築方法を理解しましょう。作成するサンプルとしては、基本的なWeb Componentsの構造を理解するためのもの、宣言的Shadow DOMを使ったWeb Componentsの、大きく2つを紹介します(図1)。

  • 図1:完成サンプル

    図1:完成サンプル

Web Componentsの基本

Shadow DOMやtemplate要素について説明する前に、まず、それらが関係するWeb Componentsとは何かを確認しておきましょう。

Web Componentsとは

Web Componentsとは、シンプルに表現すると、「ユーザー定義」のHTML要素を実現するための仕組み。
HTMLで利用できる要素は、Living Standardという標準規格で定められています。そのため、たとえば「foo」という独自の要素を使いたいと思っても、そのままではHTMLの要素として利用できません。
Web Componentsを使えば、決められたルールに従って、独自のHTML要素を定義できます。プログラミング言語で、標準では用意されていない関数やメソッドを自分で定義して追加するようなイメージです。
Web Componentsは、再利用可能なUI部品の作成が主な用途となります。たとえば、以下のような用途が挙げられます。

  • 共通のボタンや入力欄:複数のページで同じ見た目や振る舞いを実装したいとき
  • 商品カードやプロフィール表示:データを属性や子要素で渡して統一された形式で表示したいとき
  • タブ、アコーディオン、モーダルウィンドウ:操作や状態管理を部品内にまとめたいとき
  • 動画・地図などの埋め込み部品:複雑になりがちな表示と操作をひとまとまめにしたいとき
  • デザインシステムのコンポーネント:異なるフレームワークやページ間で共通のUIを提供したいとき
  • 配布可能なウィジェット:問い合わせフォームや予約パネルなどを、タグを置くだけで組み込めるようにしたいとき

基本的には、複数のページや異なる開発環境でUI部品を共有したい場合に、Web Componentsが役立ちます。

Web Componentsを構成する技術

Web Componentsは、以下の3つの技術から構成されます。

・カスタム要素:JavaScriptのクラスを独自のHTMLタグとひも付ける
・Shadow DOM:DOM(Document Object Model)とスタイルをカプセル化し、外部からの影響を受けにくくする
・HTMLテンプレート:<template>タグや<slot>タグを使い、再利用可能なマークアップ構造を定義する

あるUI部品をWeb Componentsとして作る場合、基本的には次のような流れになります。

(1)HTMLテンプレートで、UIの構造やスタイルを定義する
(2)カスタム要素として、(1)を利用するHTMLElementの派生クラスを定義し、独自のタグ(たとえば<my-tag>)と結び付ける

さらに、Shadow DOMを利用すると、HTMLテンプレートをカプセル化できます。これにより、外部のCSSなどの影響を受けにくい、独立したUI部品として構築できます。
Web Componentsの全体像を、具体的なサンプルで確認してみましょう。ここでは、名前や表示内容をカスタマイズできるカード要素(<greeting-card>)を作成します(図2)。

  • 図2:コンポーネントの基本的なサンプル

    図2:コンポーネントの基本的なサンプル

UIの構造やスタイルの定義

まずは、HTMLテンプレートに相当するのがリスト1です。

リスト1:basic.html

(1)テンプレートを定義する
<template id="greeting-template">
  <style>
    :host { (1)-1:ホスト要素のスタイル
      display: block;
      max-width: 32rem;
      padding: 1rem;
      border: 1px solid gray;
      border-radius: 0.5rem;
      font-family: sans-serif;
    }
    h2 {    (1)-2
      margin-top: 0;
      color: red;
    }
  </style>
  <article>
    <h2>こんにちは、<span id="name"></span>さん!</h2> (1)-2:id属性の利用
    <slot>カードの内容はありません。</slot>    (1)-3:差し込みスロット
  </article>
</template>
(2)コンポーネントを配置する
<greeting-card name="太郎" style="margin-bottom: 1rem;">  (2)-1
  <p>メッセージはありません。</p>   (2)-2
</greeting-card>
<greeting-card name="花子" style="margin-bottom: 1rem;">
  <p>メッセージがあります。</p>
</greeting-card>
<greeting-card></greeting-card>

大きく、(1)HTMLテンプレートの定義、(2)コンポーネントの配置に分かれています。

(1)HTMLテンプレートの定義
HTMLテンプレートとは、コンポーネントのひな型となるものです。<template>要素で定義し、ここではid="greeting-template"として識別しています。
なお、本記事のテーマはtemplate要素を使った宣言的Shadow DOMですが、ここではまだ従来の方法でShadow DOMを構築します。テンプレートの中にはstyle要素やarticle要素などを配置し、コンポーネントの構造やスタイルを定義しています。
(1)-1の:host疑似クラスは、Shadow DOMを保持する要素(ホスト要素といいます)を指定します。ここでは、コンポーネント自身が角丸のボックスとして表示されるようにスタイルを設定しています。
(1)-2では、h2要素の文字色を赤にしています。これは、コンポーネント内部のスタイルが外部のh2要素に影響しないことを確認するためのものです。図2のように、コンポーネント内部のh2要素は赤くなりますが、外部のh2要素は赤くなりません。これはShadow DOMによるカプセル化の働きです。詳しくは、後ほどJavaScriptコードを説明するときに取り上げます。
同じく(1)-2のspan要素にはid="name"を指定しています。この要素には、後ほどJavaScriptコードから名前を設定します。Shadow DOM内の要素は外部のDOMから分離されるため、id属性もコンポーネント内部で利用できます。
(1)-3のslot要素は、テンプレートの中にコンテンツを差し込むための場所です。コンポーネントタグ(ここでは<greeting-card>)の内側に記述した子要素を、ここに表示します。
slot要素を使うことで、コンポーネントの枠組みを変えることなく、利用側からコンテンツの一部を渡せます。

[NOTE]Shadow DOMとLight DOM
Shadow DOMは、文字どおり「影のDOM」という意味です。これに対して、Shadow DOMではない通常のDOMをLight DOMと呼びます。Light DOMという用語は、通常のDOMだけを指す場合にはあまり使われませんが、Shadow DOMとの違いを明確にしたい場合によく使われます。

(2)コンポーネントの配置
ここでは、<greeting-card>という標準のHTMLにはない要素を使っています。これがカスタム要素です。標準のp要素やdiv要素と同じように、HTMLの中に配置できます。
name属性を使ってコンポーネントに値を渡したり、前述のslot要素を利用して、コンポーネントの内容を利用側から渡したりできます。
たとえば、コンポーネントの子要素であるp要素は、名前のないslotに表示されます。子要素がない場合は、slot要素の中に記述した「カードの内容はありません。」が代わりに表示されます。これはフォールバックコンテンツといいます。
なお、空白や改行も子要素として扱われます。そのため、コンテンツを完全に省略する場合は、この例のように<greeting-card></greeting-card>と開きタグと閉じタグを連続して記述します。

カスタム要素の定義

続いて、カスタム要素に相当するJavaScriptコードを見ていきましょう。テンプレートの名前(id="greeting-template")とコンポーネントの名前(<greeting-card>)は異なります。この2つを結び付けるのが、カスタム要素の役割です。

リスト2:basic.js

(1)コンポーネントのクラスを定義する
class GreetingCard extends HTMLElement {
  (1)-1:コンストラクタ
  constructor() {
    super();
    const template = document.getElementById('greeting-template');
    const content = template.content.cloneNode(true);
    this.attachShadow({ mode: 'open' }).append(content);    (1)-2:アタッチ
  }
  (1)-3:コールバック
  connectedCallback() {
    const name = this.getAttribute('name') || '名無し';
    this.shadowRoot.getElementById('name').textContent = name;  (1)-4
  }
}
(2)クラスをタグ名'greeting-card'で登録する
customElements.define('greeting-card', GreetingCard);   (2)-1:登録

こちらも大きく、(1)カスタム要素のクラス定義と、(2)クラスの登録に分かれています。

(1)カスタム要素のクラス定義
クラスは、HTML要素の基底クラスであるHTMLElementを継承して定義します。ここではGreetingCardクラスを定義しています。
このクラスでは、コンストラクタ(constructor)でテンプレートをShadow DOMとして組み込み、ホスト要素への接続時に呼び出されるコールバック(connectedCallback)で、コンポーネントに渡された名前を設定しています。
(1)-1のコンストラクタでは、document.getElementByIdでHTMLテンプレート(id="greeting-template")を取得し、cloneNodeメソッドでその内容を複製します。
続いて、attachShadowメソッドを使ってホスト要素にShadow DOMを作成し、複製したテンプレートの内容を追加しています。
(1)-3のconnectedCallbackでは、コンポーネントのname属性を取り出し、その値をテンプレート内のid="name"の要素に設定しています。

(2)クラスの登録
(1)で定義したGreetingCardクラスを、HTMLのタグ名に結び付けるのが(2)-1のdefineメソッドです。
customElementsは、カスタム要素を定義するための組み込みオブジェクトです。そのdefineメソッドにタグ名greeting-cardとクラスGreetingCardを渡すことで、<greeting-card>というカスタム要素を定義します。 このような流れで、Web Componentsによるカスタム要素を実現できます。