Using custom elements

Webコンポーネント標準の重要な特徴の一つはカスタム要素を作れることです。それはページの機能を提供する長くネストした要素のバッチではなく、HTMLページ上で機能をカプセル化します。
この記事はカスタム要素APIの使い方を紹介します。

: カスタム要素をデフォルトでサポートするのは Firefox, Chrome, と Edge (76)です。Opera と Safari は今のところ、自律カスタム要素のみサポートしています。

High-level view

Webドキュメント上でカスタム要素をコントロールするのは CustomElementRegistry オブジェクトです。 — このオブジェクトはページへのカスタム要素を登録したり、どんなカスタム要素が登録されているのかを返すなどの操作を行えます。

ページにカスタム要素を登録するには,  CustomElementRegistry.define() メソッドを使います。次の引数を取ることができます:

  • エレメントの名前を表す DOMString 。 カスタム要素の名前には ダッシュを使う必要があります。; 一つの単語の名前をつけられません。
  • 要素の振る舞いを定義した クラス オブジェクト。
  • オプションで, extends属性を含むオプションオブジェクト。組み込み要素をを継承する場合にはそれを指定します。

例えば、次の様に word-count 要素 を定義できます:

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

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

カスタム要素のクラスオブジェクトは ES 2015 のクラスシンタックスで実装します。例えば、WordCount 次の様になります:

class WordCount extends HTMLParagraphElement {
  constructor() {
    // Always call super first in constructor
    super();

    // Element functionality written in here

    ...
  }
}

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

Using the lifecycle callbacks でこれらについてもっと学ぶことができます。.

カスタム要素には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,というクラスを定義します。 自律カスタム要素はほぼいつも HTMLElementを継承します。

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

    // write element functionality in here

    ...
  }
}

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

コンストラクタ内で、インスタンス化された時に要素が持っているすべての機能を定義します。この例ではカスタム要素にshadowルートをアタッチしています。DOM操作を行い、要素内部の shadow DOM構造を作ります。—DOM構造はshadowルートにアタッチされます— そして最後にスタイルを適用するためにCSSをshadowルートにアタッチします。

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

// 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);

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

style.textContent = '.wrapper {' +
// CSS truncated for brevity

// attach the created elements to the shadow dom

shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);

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

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

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

<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."></popup-info>

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

: カスタム要素が機能するためには、DOMの解析を終えた後にカスタム要素を登録するスクリプトが読み込まれる必要があることに注意してください。これは <script> 要素を <body> 要素内の最下部に配置する、または <script> 要素に defer 属性を加えることで解決します。

内部スタイル 対 外部スタイル

上記の例では <style> 要素を用いてShadow DOMにスタイルを適用しました。しかし、<link> 要素から外部のスタイルシートを参照することも可能です。

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

// Apply external styles to the shadow dom
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');

// Attach the created element to the shadow dom
shadow.appendChild(linkElem);

この手法が特に大規模なスタイルシートで推奨されます。これによって、綺麗で、より共有しやすい効率の良いコードになります。

カスタマイズされたビルトイン要素

ここで、もう1つのビルトイン要素の例を見てみましょう — expanding-list (デモはこちら) 。 これにより番号なしリストが展開・縮小するメニューになります。

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

class ExpandingList extends HTMLUListElement {
  constructor() {
    // Always call super first in constructor
    super();

    // write element functionality in here

    ...
  }
}

ここでは要素の詳細な機能については説明しませんが、ソースコードからどのように動作するのかチェックすることができます。これまでと唯一違う点は HTMLElement ではなく、 HTMLUListElement インターフェースを拡張していることです。そのため、独立した要素ではなく、 <ul> 要素の特徴を備えた上に、私たちが定義した機能を持っています。これこそが自律カスタム要素ではなくカスタマイズされたビルトイン要素である理由です。

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

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

Webドキュメント内でビルトイン要素を使用する場合とはやや異なります。

<ul is="expanding-list">

  ...

</ul>

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

: もう一度述べますが、こちらで 完全なソースコード を見ることができます。

Using the lifecycle callbacks

You can define several different callbacks inside a custom element's class definition, which fire at different points in the element's lifecycle:

  • connectedCallback: Invoked each time the custom element is appended into a document-connected element. This will happen each time the node is moved, and may happen before the element's contents have been fully parsed.

    : connectedCallback may be called once your element is no longer connected, use Node.isConnected to make sure.

  • disconnectedCallback: Invoked each time the custom element is disconnected from the document's DOM.
  • adoptedCallback: Invoked each time the custom element is moved to a new document.
  • attributeChangedCallback: Invoked each time one of the custom element's attributes is added, removed, or changed. Which attributes to notice change for is specified in a static get observedAttributes method

Let's look at an example of these in use. The code below is taken from the life-cycle-callbacks example (see it running live). This is a trivial example that simply generates a colored square of a fixed size on the page. The custom element looks like this:

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

The class constructor is really simple — here we attach a shadow DOM to the element, then attach empty <div> and <style> elements to the shadow root:

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

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

The key function in this example is updateStyle() — this takes an element, gets its shadow root, finds its <style> element, and adds width, height, and background-color to the style.

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')};
    }
  `;
}

The actual updates are all handled by the life cycle callbacks, which are placed inside the class definition as methods. The connectedCallback() runs each time the element is added to the DOM — here we run the updateStyle() function to make sure the square is styled as defined in its attributes:

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

The disconnectedCallback() and adoptedCallback() callbacks log simple messages to the console to inform us when the element is either removed from the DOM, or moved to a different page:

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

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

The attributeChangedCallback() callback is run whenever one of the element's attributes is changed in some way. As you can see from its properties, it is possible to act on attributes individually, looking at their name, and old and new attribute values. In this case however, we are just running the updateStyle() function again to make sure that the square's style is updated as per the new values:

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

Note that to get the attributeChangedCallback() callback to fire when an attribute changes, you have to observe the attributes. This is done by specifying a static get observedAttributes() method inside custom element class - this should return  an array containing the names of the attributes you want to observe:

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

This is placed right at the top of the constructor, in our example.

: Find the full JavaScript source here.

ポリフィル 対 クラス

カスタム要素のポリフィルは HTMLElement などのネイティブのコンストラクタに対してパッチを当てることで、単にネイティブのコンストラクタが作成したものとは異なるインスタンスを返すことがあります。

もし constructor や強制的に super を呼び出す必要があるなら、任意の引数を渡して super を呼び出した結果を返すことを忘れないでください。

class CustomElement extends HTMLElement {
  constructor(...args) {
    const self = super(...args);
    // self functionality written in here
    // self.addEventListener(...)
    // return the right context
    return self;
  }
}

もしコンストラクタ内で何も処理が必要ないならば、単に省略することでネイティブの挙動を維持できます。

 constructor(...args) { return super(...args); }

トランスパイラ 対 クラス

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

ライブラリ

カスタム要素を作る際に抽象度を高めることを目的とした、Web Componentsで実装されたライブラリがあります。その内のいくつかを挙げます。snuggsi ツX-TagSlim.jsLitElementSmartStencil