カスタム要素の使用

ウェブコンポーネント標準の主な特徴の 1 つは、 HTML ページに機能をカプセル化するカスタム要素を作成できることで、カスタムページの機能を提供する要素の長いネストしたバッチを作成する必要がありません。この記事では、 Custom Elements API の使い方を紹介します。

高水準のビュー

ウェブ文書上でカスタム要素を制御するのは CustomElementRegistry オブジェクトです。 — このオブジェクトで、ページへカスタム要素を登録したり、どのようなカスタム要素が登録されているのかを返したりすることができます。

ページにカスタム要素を登録するには、 CustomElementRegistry.define() メソッドを使います。引数に次のものを取ります。

  • 要素に与える名前を表す DOMString。カスタム要素の名前は、ダッシュが使われている名前 (kebab-case) である必要があります。単一の単語にすることはできません。
  • 要素の振る舞いを定義したクラスのオブジェクト。
  • 省略可 extends プロパティを含むオプションオブジェクトです。このプロパティは、もしあれば、要素が継承する組み込み要素を指定します(カスタマイズされた組み込み要素にのみ関係します)。

例えば、カスタムの word-count 要素を定義するには次のようにします。

customElements.define('word-count', WordCount, { extends: 'p' });

word-count 要素は WordCount クラスのオブジェクトで、 <p> 要素を拡張します。

カスタム要素のクラスのオブジェクトは ES 2015 のクラス構文で実装します。例えば、 WordCount は次のように構成します。

class WordCount extends HTMLParagraphElement {
  constructor() {
    // コンストラクターでは常に super を最初に呼び出してください
    super();

    // ここに要素の機能を記述します

    ...
  }
}

これはごく簡単な例ですが、ここでできることはもっとあります。クラスの中でライフサイクルコールバックを定義することができ、要素のライフサイクルの特定の時点で実行されます。例えば、connectedCallback はドキュメント接続要素にカスタム要素が追加されるたびに実行されます。一方 attributeChangedCallback はカスタム要素に属性が追加、削除、変更される時に実行されます。

これらについては、下記のライフサイクルコールバックの使用の節で詳しく学ぶことができます。

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

  • スタンドアロンの自律カスタム要素 — 標準の HTML 要素を継承しません。 HTML 要素としてページ内で記述して使います。例えば、<popup-info> あるいは document.createElement("popup-info") などです。
  • 基礎となる HTML 要素を継承するカスタマイズされた組み込み要素。これらを作成するには、どの要素を拡張するかを指定する必要があり(上記の例で示した通り)、基本要素を記述し、カスタム要素の名前を is 属性(またはプロパティ)で指定することで使用します。例えば、 <p is="word-count">document.createElement("p", { is: "word-count" }) のようにします。

簡単な例での作業

ここで、もう少し簡単な例で、カスタム要素の作成方法をより詳しく説明しましょう。

自律カスタム要素

自律カスタム要素の例を見てみましょう。<popup-info-box> (ライブ例も参照) です。これは画像とテキストを受け取り、ページにアイコンを埋め込みます。アイコンにフォーカスすると、テキストをポップアップ情報ボックスに表示し、さらにコンテキスト内の情報を提供します。

最初に汎用的な HTMLElement クラスを継承して PopUpInfo というクラスを定義する JavaScript ファイルです。

class PopUpInfo extends HTMLElement {
  constructor() {
    // コンストラクターでは常に super を最初に呼び出してください
    super();

    // ここに要素の機能を記述します

    ...
  }
}

前述のコードスニペットはクラスのコンストラクター (constructor()) の定義を含んでいます。ここでは常に super() を最初に呼び出し、正しいプロトタイプチェーンが確立されるようにします。

コンストラクターの内部では、その要素のインスタンスが生成されたときに持つすべての機能を定義します。この場合、カスタム要素にシャドウルートを添付し、いくつかの DOM 操作を使用して要素の内部シャドウ DOM 構造を作成します。それをシャドウルートに添付します。そして最後に、いくつかの CSS をシャドウルートに添付してスタイル付けを行います。

// シャドウルートを生成
this.attachShadow({mode: 'open'}); // 'this.shadowRoot' を設定して返す

// (内部の) span 要素を生成
const wrapper = document.createElement('span');
wrapper.setAttribute('class','wrapper');
const icon = wrapper.appendChild(document.createElement('span'));
icon.setAttribute('class','icon');
icon.setAttribute('tabindex', 0);
// アイコンを、定義された属性または既定のアイコンから挿入
const img = icon.appendChild(document.createElement('img'));
img.src = this.hasAttribute('img') ? this.getAttribute('img') : 'img/default.png';

const info = wrapper.appendChild(document.createElement('span'));
info.setAttribute('class','info');
// 属性の中身を取得し、 info の span の中に入れる
info.textContent = this.getAttribute('data-text');

// CSS を作成しシャドウ DOM に割り当てる
const style = document.createElement('style');
style.textContent = '.wrapper {' +
// 簡略化のために CSS は省略

// 生成された要素をシャドウ DOM に添付する
this.shadowRoot.append(style,wrapper);

最後に、カスタム要素を CustomElementRegistry に登録します。前述の define() を使用して、引数で要素名とその機能を定義するクラス名を指定します。

customElements.define('popup-info', PopUpInfo);

これによって要素がページで使えるようになりました。 HTML 中で下記のように使用することができます。

<popup-info img="img/alt.png" data-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."></popup-info>

メモ: こちらで完全な JavaScript ソース を見ることができます。

内部スタイルと外部スタイル

上記の例では <style> 要素を用いてシャドウ DOM にスタイルを適用しましたが、代わりに完全に <link> 要素から外部スタイルシートを参照することが可能です。

例えば、 popup-info-box-external-stylesheet のコードを少し見てみましょう(ソースコードはこちら)。

// 外部スタイルシートをシャドウ DOM に適用
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');

// 生成された要素をシャドウ DOM に添付
shadow.appendChild(linkElem);

なお、 <link> 要素はシャドウルートの描画をブロックしないので、スタイルシートのロード中にスタイル付けされていないコンテンツ (FOUC) が一瞬表示されるかもしれないことに注意してください。

最近のブラウザーの多くは、共通のノードからクローンされた、あるいは同一のテキストを持つ <style> タグに対して、単一のバッキングスタイルシートを共有できるようにする最適化を実装しています。この最適化によって、外部スタイルでも内部スタイルでも性能は同程度になるはずです。

カスタマイズされた組み込み要素

ここで、もう 1 つの組み込み要素の例を見てみましょう。 expanding-list (ライブでも確認してください) です。 これにより番号なしリストが展開・縮小するメニューになります。

まず始めに、これまでと同様の方法でクラス要素を定義します。

class ExpandingList extends HTMLUListElement {
  constructor() {
    // コンストラクターでは常に super を最初に呼び出してください
    super();

    // ここに要素の機能を記述します

    ...
  }
}

ここでは要素の詳細な機能については説明しませんが、ソースコードからどのように動作するのか確認することができます。これまでと唯一違う点は、 HTMLUListElement (en-US) インターフェースを継承しており、 HTMLElement ではないことです。そのため、独立した要素ではなく、 <ul> 要素の特徴を備えた上に、定義した機能を持ちます。これこそが、自律カスタム要素ではなくカスタマイズされた組み込み要素である理由です。

次に、以前と同様に define() を用いて要素を登録しますが、今回はこのカスタム要素がどの要素から継承したのかという情報をオプションとして渡しています。

customElements.define('expanding-list', ExpandingList, { extends: "ul" });

ウェブ文書内で組み込み要素を使用する場合とはやや異なります。

<ul is="expanding-list">

  ...

</ul>

通常のように <ul> を使用していますが、カスタム要素の名前が is 属性で指定されています。

メモ: 繰り返しますが、完全な JavaScript のソースコードはこちらにあります。

ライフサイクルコールバックの使用

カスタム要素のクラス定義内に、いくつかの異なるコールバックを定義できます。これらは要素のライフサイクルのさまざまな時点で起動します。

  • connectedCallback: 文書に接続された要素にカスタム要素が追加されるたびに呼び出されます。これはそのノードが移動するために発生するので、要素の内容が完全に解釈される前に発生することがあります。

    メモ: connectedCallback は要素の接続が終了したときにも呼び出されることがあります。 Node.isConnected を使用して確認してください。

  • disconnectedCallback: カスタム要素が文書の DOM から切断されるたびに呼び出されます。
  • adoptedCallback: カスタム要素が新しい文書に移動するたびに呼び出されます。
  • attributeChangedCallback: カスタム要素の属性の 1 つが追加、削除、変更されるたびに呼び出されます。どの属性の変更が通知されたかは、 static get observedAttributes() メソッドで指定されます。

これらの使用例を見てみましょう。以下のコードは life-cycle-callbacks の例から引用しています (実行可能なライブでも確認してください)。これは、ページ上に一定の大きさの色のついた四角形を生成する些細な例です。カスタム要素は次のようなものです。

<custom-square l="100" c="red"></custom-square>

クラスのコンストラクターは非常に単純です。ここでは、要素にシャドウ DOM を割り当て、空の <div> および <style> 要素をシャドウルートに追加します。

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

const div = document.createElement('div');
const style = document.createElement('style');
shadow.appendChild(style);
shadow.appendChild(div);

この例の主要な機能は updateStyle() です。これは要素を取得し、シャドウルートを取得し、その <style> 要素を見つけて、width, height, およびbackground-color をそのスタイルに追加します。

function updateStyle(elem) {
  const shadow = elem.shadowRoot;
  shadow.querySelector('style').textContent = `
    div {
      width: ${elem.getAttribute('l')}px;
      height: ${elem.getAttribute('l')}px;
      background-color: ${elem.getAttribute('c')};
    }
  `;
}

実際の更新はすべて、メソッドとしてクラス定義内に配置されているライフサイクルコールバックによって処理されます。 connectedCallback() は、要素が DOM に追加されるたびに実行されます。ここでは、 updateStyle() 関数を実行して、正方形がその属性で定義されたスタイルになっていることを確認します。

connectedCallback() {
  console.log('Custom square element added to page.');
  updateStyle(this);
}

disconnectedCallback() および adoptedCallback() コールバックは、要素が DOM から削除されるか、別のページに移動されたときに通知する単純なメッセージをコンソールに記録します。

disconnectedCallback() {
  console.log('Custom square element removed from page.');
}

adoptedCallback() {
  console.log('Custom square element moved to new page.');
}

attributeChangedCallback() コールバックは、要素の属性の 1 つが何らかの方法で変更されるたびに実行されます。そのプロパティからわかるように、属性、属性の名前、および古い属性値と新しい属性値を個別に操作することができます。ただし、この場合は、 updateStyle() 関数を再度実行して、新しい値に従って正方形のスタイルが更新されるようにします。

attributeChangedCallback(name, oldValue, newValue) {
  console.log('Custom square element attributes changed.');
  updateStyle(this);
}

ある属性が変更されたときに起動する attributeChangedCallback() コールバックを取得するには、その属性を監視する必要があることに注意してください。これは、カスタム要素クラス内で static get observedAttributes() メソッドを定義することによって行われます。これは、監視したい属性の名前を含む配列を返すようにしてください。

static get observedAttributes() { return ['c', 'l']; }

この例では、これはコンストラクターの最上部に配置されています。

メモ: 完全な JavaScript のソースはこちらから探してください。

トランスパイラーとクラス

古いブラウザーを対象とした Babel 6 や TypeScript では、 ES2015 のクラス構文は期待通りにトランスパイルされない可能性があることに注意してください。 Babel 7 もしくは Babel 6 の babel-plugin-transform-builtin-classes を使用すると、 TypeScript で古いブラウザーではなく ES2015 をターゲットとすることができます。

ライブラリー

カスタム要素を作る際に抽象度を高めることを目的とした、ウェブコンポーネントで実装されたライブラリーがあります。これらのライブラリーには、 FASTElement, snuggsi, X-Tag, Slim.js, Lit, Smart, Stencil, hyperHTML-Element, DataFormsJS, Custom-Element-Builder などがあります。