「Web Components」って知っていますか?自分でHTMLタグを作れちゃうんです。

「Web Components」は、ウェブのUIをコンポーネント化するための仕様です。AMP や Angular などにも使われています。

この記事は、フロントエンド3要素(HTML、CSS、JavaScript)の基本を理解している方なら、Web Components の感触をつかめる内容となっています。

Web Components とはなんなのか。

Web Components は、再利用可能なカスタム要素を作成し、ウェブアプリの中で利用するための、一連のテクノロジーです。コードの他の部分から独立した、カプセル化された機能を使って実現します。 Web Components | MDN

要するに、すごく簡単に言ってみれば、「HTMLタグを自作する仕組み」です。

HTMLタグには <a> とか <div> とかいろいろありますが、 <my-tag> といった感じで名前を決めて、その見た目や振る舞いを自分で作るというものです。

概要: Web Components は3つの技術からなる

WebComponentsは次の3つの主要な技術から成り立っています。

カスタム要素(Custom Elements)
HTML要素を自作するための JavaScript API
Shadow DOM
通常のDOMとは切り離されカプセル化されたDOMツリーを作成するための JavaScript API
HTML テンプレート(HTML templates)
<template> と <slot> 要素を使用して、レンダリングされないマークアップのテンプレートを作成します。

したがって、Web Componentsを理解するということは、これら3つの技術を理解するということになります。

カスタム要素

カスタム要素は、HTML要素を自作するための一連の JavaScript API です。

カスタム要素の要素名

<div> や <figure> など、全てのHTML要素の名前はアルファベットのみです。これに対し、カスタム要素の要素名は、<my-tag> のように、ハイフン「-」を入れる決まりになっています。

カスタム要素の種類と使用方法

カスタム要素には2つの種類があります。

  • 独自の名前の要素を作る(自律的カスタム要素
  • HTML標準の要素を拡張する(カスタム組み込み要素

「自律的カスタム要素」か「カスタム組込要素」かにより使い方に違いがあります。

自律的カスタム要素は <my-tag>~</my-tag> というように、タグがそのまま要素名になります。

一方、カスタム組み込み要素の場合は、<div is="my-tag">~<div> といったように、元の標準要素に is 属性でカスタム要素名を指定する形で使用します。

カスタム要素の作成と登録

カスタム要素を作成するには、ES2015のクラス構文で HTMLElement を継承したクラスを定義し、customElements.define() で登録を行います。

自律的カスタム要素の作成 – <my-tag> の作成
  1. class MyTag extends HTMLElement {
  2. constructor() {
  3. super(); // super() は必ず最初に呼び出すこと
  4. /* ... */
  5. }
  6. /* ... */
  7. }
  8. customElements.define('my-tag', MyTag);
  9.  
  10. // 補足: 無名クラスを利用することもできます。
  11. customElements.define('my-tag', class extends HTMLElement {
  12. /* ... */
  13. });

既に作成しているカスタム要素を更に拡張することもできます。

自律的カスタム要素の作成 – <my-tag> の拡張
  1. class MyTag2 extends MyTag { /* ... */ }

カスタム組み込み要素を作成するには、継承(extends)するクラスを、HTMLElement を更に具体的にしたクラスにします。それは様々なものがありますが、例えば、次のものがあります。

HTML 要素対応するクラス
<a> 要素HTMLAnchorElement
<div> 要素HTMLDivElement
<table> 要素HTMLTableElement
<button> 要素HTMLButtonElement

更に、customElements.define() で何の組み込み要素をを拡張するのかブラウザに対して明示します。

カスタム組み込み要素の作成 – <button> の拡張
  1. class MyButton extends HTMLButtonElement {
  2. constructor() {
  3. super(); // super() は必ず最初に呼び出すこと
  4. /* ... */
  5. }
  6. /* ... */
  7. }
  8. customElements.define('my-tag', MyButton, {extends: 'button'});

ライフサイクルコールバック

カスタム要素は、作成されてから削除されるまでの生存期間において、特定のタイミングで実行されるコールバックを割り持たせることができます。

  1. class MyTag extends HTMLElement {
  2. constructor() {
  3. super();
  4. /* ... */
  5. }
  6. connectedCallback() { /* ... */ }
  7. disconnectedCallback() { /* ... */ }
  8. attributeChangedCallback(attrName, oldVal, newVal) { /* ... */ }
  9. adoptedCallback() { /* ... */ } }
コールバック呼び出される状況
constructorカスタム要素のインスタンスが作成またはアップグレードされたとき
connectedCallbackカスタム要素がドキュメントの DOM に挿入されたとき
disconnectedCallback要素が DOM から削除されたとき
attributeChangedCallback(attrName, oldVal, newVal)カスタム要素の属性が追加、削除、更新、または置換されたとき
adoptedCallbackカスタム要素が新しい document に移動したとき

カスタム要素の関連するCSS擬似クラス

カスタム要素に関連した特別なCSS擬似クラスがあります。

擬似クラス説明
:defined組込要素と定義されたカスタム要素を含む全ての要素にマッチする擬似要素
:hostShadow DOM で使われた際にそのShadow DOM のホストを選択します。
カスタム要素ないの Shadow DOM で使われた場合は、そのカスタム要素を指します。
→ :host { font-family: monospace; }
:host(セレクタ)ホストがセレクタ
に一致する場合にホストをターゲットにできます。
→ :host([:hover]): opacity: .7;
:host-context(セレクタ)コンポーネントまたはコンポーネントの祖先がセレクタ
に一致する場合にマッチします。
例えば、ドキュメントがダークテーマだった場合にコンポーネントの色を変更するという場合に有効です。
→ :host-context(.darktheme) { color: white; }

Shadow DOM

左から、通常のDOM、Shadow DOM、見かけ上のDOM

Shadow DOM は、カプセル化されたDOMで、JavaScript の API を使用して要素に紐付けを行います。DOMの一種ではありますが、通常のDOMとは別にレンダリングされるため、例えば、CSSのセレクタは重複を恐れる必要がなくなります。

Shadow DOM の特性:

  • 隔離されたDOM: 通常のDOMからは隔離されます。例えば、document.querySelector() でShadow DOM のノードを取得できません。
  • スコープを持ったCSS: Shadow DOM 内のCSSはShadow DOM の外側へは影響を与えません。

Shadow DOM は、セキュリティやブラウザが既に内部的にShadow DOMを使用しているなどの理由から、すべての要素に追加できるわけではありません。追加できるのは次の要素です。

  • 有効な名前を持つカスタム要素(例: <my-tag>
  • <article><section>
  • <aside>
  • <blockquote>
  • <body>
  • <div>
  • <main><header><footer>
  • <h1> 〜 <h6>
  • <nav>
  • <p>
  • <span>

Shadow DOM を作成するには、Element.attachShadow() メソッドを利用します。

Shadow DOM のルートを作成
  1. class MyTag extends HTMLElement {
  2. constructor() {
  3. super();
  4.  
  5. // Shadow DOM のルートを作成します。
  6. let shadowRoot = this.attachShadow({mode: 'open'});
  7.  
  8. // 隔離されたDOMを作成します。
  9. let wrapper = document.createElement('span');
  10. wrapper.setAttribute('class','wrapper');
  11. shadowRoot.appendChild(wrapper);
  12.  
  13. // スコープを持ったCSSを作成します。
  14. let style = document.createElement('style');
  15. style.textContent = `
  16. .wrapper {
  17. position: relative;
  18. }`;
  19. shadowRoot.appendChild(style);
  20. }
  21. }

HTMLテンプレート

<template>

<template>は、HTMLのマークアップを雛形(テンプレート)にHTML要素です。テンプレートにしたいマークアップを <template> 要素で囲むと、そのマークアップはブラウザでは描画されなくなります。これは、後で前述のShadow DOM に追加して使い回しをすると便利です。

<template> の使用(ブラウザではレンダリングされない)
  1. <template id="my-paragraph-template">
  2. <p>My paragraph</p>
  3. <template>

<slot>

Shadow DOM は通常のDOMとは隔離されますが、コンポーネントが <slot> を持っていると、通常のDOM(Shadow DOMに対して Light DOM という)をDOMの境界を超えてShadow DOMに反映させることができるようになります。

<slot> は、カスタム要素を使用するときに Shadow DOM に要素を追加するためのプレースホルダになります。

デフォルトスロットの定義
  1. #shadow-root
  2. <p><slot>デフォルトテキスト</slot></p>

これが、「my-tag」という名前のカスタム要素の Shadow DOMに含まれていた場合、次のように使うことができます。

デフォルトスロットにコンテンツを送る
  1. <my-tag>
  2. <span>こんにちは。</span>
  3. </my-tag>

結果はこのようになります。

slot があった場合の結果
  1. <my-tag>
  2. #shadow-root
  3. <p><slot><span>こんにちは。</span></slot></p>
  4. </my-tag>

名前付きスロットを作成して複数の <slot> を配置できます。

名前付きスロットの定義
  1. <my-tag>
  2. #shadow-root
  3. <article>
  4. <h1><slot>デフォルトのタイトル</slot></h1>
  5. <slot name="content"></slot>
  6. </article>
  7. </my-tag>
名前付きスロットにコンテンツを送る
  1. <my-tag>
  2. こんにちは。
  3. <div slot="content">コンテンツ1</div>
  4. <div slot="content">コンテンツ2</div>
  5. </my-tag>
名前付きスロットの結果
  1. <my-tag>
  2. #shadow-root
  3. <article>
  4. <h1><slot>こんにちは。</slot></h1>
  5. <slot name="content">
  6. <div slot="content">コンテンツ1</div>
  7. <div slot="content">コンテンツ2</div>
  8. </slot>
  9. </article>
  10. </my-tag>

Web Component 実装の流れ

  1. Web Component の機能を持ったクラス(ES2015の構文)を作成(HTMLElement
    • (必要に応じて)Shadow DOM をカスタム要素に紐付け(Element.attachShadow()
    • (必要に応じて)HTML テンプレートを定義(<template><slot>
  2. 作成したカスタム要素を登録(CustomElementRegistry.define()
  3. 準備完了。通常の HTML 要素と同じようにHTMLドキュメントの中で作成したタグを使うことができます。

実装サンプル

簡単なサンプルを用意しました。Bootstrap の Alerts を Web Components で再現してみました。カスタム要素、Shadow DOM、<template><slot> を使用しています。

<template> 要素で作成したテンプレートをカスタム要素の Shadow DOM に追加しています。<template>には、<slot> が含まれていて、Light DOM を Shadow DOM に送り込めるようにしています。

注目して欲しいのが、Shadow DOM に送った Light DOM には、通常のDOM側のCSS([...] .alert-link)が反映されている点です。これは、先程「<slot> でLight DOM を Shadow DOM に送り込んだ」といった意味の表現をしましたが、Light DOM がShadow DOM の一部になったわけではないことを意味します。即ち、カプセル化は保たれているということです。ただし、CSSの全称セレクタ「*」は、DOM の境界を越えるようです。

Q&A

カスタム要素を使う段階で定義がされていなかったらどうなるの?

カスタム要素は標準の要素とは異なり、JavaScript で登録を行います。そのため、DOMにカスタム要素が読み込まれた段階でも、まだカスタム要素が登録されていないこともあり得ます。

まず結論を言いますと、「カスタム要素は、その定義を登録する前に使用できる」です。

HTMLの仕様で、不明な要素は HTMLUnknownElement として扱われます。これは、HTMLの仕様では「undefined」の状態にあります。

しかし、ハイフン「-」を含むカスタム要素として有効な名前の場合は、この限りではなく、HTMLElement として扱われます。まだ、定義がされていない段階では、HTML仕様では「uncustomized」であり、定義がされた場合は標準の要素と同じ「defined」になります。

Shadow DOM を使用したら子要素が表示されなくなった

カスタム要素で Shadow DOM を使用すると、それは通常のDOMコンテンツの代わりにレンダリングされるようになります。Shadow DOM の中で通常のDOM(Light DOM)を反映させるには、<slot> 要素でプレースホルダーを配置する必要があります。

Web Component の使い所

リッチなコンテンツは多数の <div> でネストされるなど複雑になりがちです。Web Components を使用すると、コンポーネントという単位で、コンポーネント利用者に対して複雑な構造を隠すことができ、再利用がしやすくなります。

ブラウザのサポート状況から言えば、Chrome、Opera、Firebox ではサポートが整っていますが、Safri や Edge ではいまいちといったところなようです。対応策として、ポリフィルを使うことができます。