HTML
HTML5
WebComponents

Custom elementsはES6のclass記法で定義可能になりそうです

More than 1 year has passed since last update.

2016年4月8日付けのCustom Elements W3C Editor's Draftでは、ES6のclass構文を使ったカスタムタグの定義方法が書かれていました。それが正式に採用されるかどうかわかりませんが、この仕様書で提案されている内容を、サンプルコードを元にして簡単に紹介してみたいと思います。

カスタムタグの基本的な定義方法

以下のようなカスタムタグを作りたいとします。

<flag-icon country="jp"></flag-icon>

このカスタムタグを定義するために、FlagIconクラスを作ります。その際、HTMLElementクラスを継承します。

class FlagIcon extends HTMLElement {

  constructor() {
    super();
    this._countryCode = null;
  }

  static get observedAttributes() {
    return ["country"];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // observedAttributes によって、 name はいつも "country" になる
    this._countryCode = newValue;
    this._updateRendering();
  }

  get country() {
    return this._countryCode;
  }

  set country(v) {
    this.setAttribute("country", v);
  }

  _updateRendering() {
    // 見た目の更新処理
  }

}

ポイントは以下です。

  • コンストラクタ内で必ず親のコンストラクタを呼び出す(super()のこと)。
  • observedAttributes()メソッドの戻り値で、監視する属性の名前を返す。これにより、attributeChangedCallback()メソッドがコールバックされるようになる。
  • プログラム的に独自属性値を読み書きするために、get及びsetによってプロパティを作っておく。

countryプロパティのsetterメソッドで、this._countryCodeに代入していないのは、this.setAttribute()メソッドで値をセットすれば、attributeChangedCallback()メソッドがコールバックされて間接的に代入される、という動作になるっぽいです。attributeChangedCallback()メソッドは、属性値が変更、追加、削除、または置き換えられた際に呼び出されます。

attributeChangedCallback()メソッド以外にも、以下のコールバックメソッドがあります。

カスタムタグの登録方法

従来のカスタムタグの登録方法は、document.registerElement()関数を使うことでした。

var FlagIcon = document.registerElement("flag-icon");
document.body.appendChild(new FlagIcon());

今回のEditor's Draftでは、CustomElementRegistryインタフェースが規定され、そのインスタンスがWindowオブジェクトにて提供されるようになります。具体的には、customElementsプロパティがWindowオブジェクトに追加されます。

先ほど定義したFlagIconクラスを使って、flag-iconカスタムタグを登録するための方法は以下のようになります。

customElements.define("flag-icon", FlagIcon);

カスタムタグの動的な利用方法

登録したカスタムタグを動的に利用するための方法として、まずDOM APIを使った方法は以下となります。

const flagIcon = document.createElement("flag-icon");
flagIcon.country = "jp";
document.body.appendChild(flagIcon);

DOM APIを使わずに、直接FlagIconクラスのインスタンスを生成する方法もあります。

const flagIcon = new FlagIcon();
flagIcon.country = "jp";
document.body.appendChild(flagIcon);

どちらでも良いです。

既存タグを拡張したカスタムタグの定義方法

例えばbuttonタグを拡張してカスタムタグを作る際には、親となるタグを表すHTMLButtonElementクラスを継承して作ります。

class PlasticButton extends HTMLButtonElement {

  constructor() {
    super();

    this.addEventListener("click", () => {
      // 素敵なアニメーション効果を描画するなど。
    });
  }

}

このPlasticButtonクラスを使ってplastic-buttonカスタムタグを登録するには、以下のようにします。

customElements.define("plastic-button", PlasticButton, {extends: "button"});

「HTMLButtonElementクラスを継承してるんだから、customElements.define()でわざわざ指定することないだろ!」って思ってしまうかもしれませんが、上記のようにbuttonタグを継承すると明記してあげることが必要です。

そして、このPlasticButtonクラスを使って拡張されたbuttonタグを使いたい場合は、以下のようにします。

<button is="plastic-button">Click Me!</button>

動的に作りたい場合は、以下のようにします。

const plasticButton = document.createElement("button", {is: "plastic-button"});
plasticButton.textContent = "Click Me!";
document.body.appendChild(plasticButton);

以下のようにDOM APIを使わない場合でも大丈夫です。

const plasticButton = new PlasticButton();

console.log(plasticButton.localName); // "button"と出力されます
console.log(plasticButton.getAttribute("is")); // "plastic-button"と出力されます

plasticButton.textContent = "Click Me!";
document.body.appendChild(plasticButton);

間違って以下のようにしてしまいそうになりますが、これだとHTMLUnknownElementになってしまうので、注意しましょう。

<plastic-button>Click Me!</plastic-button> // これはNG

型の昇格

カスタムタグの定義のタイミングと要素の生成のタイミングによっては、その要素の型が昇格する場合があります。少しエッジケースになりますが、その例を以下に紹介します。

<!DOCTYPE html>
<title>昇格のエッジケースの例</title>

<example-element></example-element>

<script>
  "use strict";

  const inDocument = document.querySelector("example-element");
  const outOfDocument = document.createElement("example-element");

  // 要素を定義する前は、両方ともHTMLElementです
  console.assert(inDocument instanceof HTMLElement);
  console.assert(outOfDocument instanceof HTMLElement);

  // ここで型を定義します
  class ExampleElement extends HTMLElement {}
  customElements.define("example-element", ExampleElement);

  // 型定義の後、document内の要素は昇格されています
  console.assert(inDocument instanceof ExampleElement);
  console.assert(!(outOfDocument instanceof ExampleElement));

  // 要素をdocument内に移動させます
  document.body.appendChild(outOfDocument);

  // 要素をdocument内に移動させたタイミングで、その要素も昇格します
  console.assert(outOfDocument instanceof ExampleElement);
</script>

まとめ

個人的にはdocument.registerElement()の方法よりもclass記法での定義方法の方がわかりやすいかなと思いますので、この仕様が採用されて主要ブラウザにて実装が進むことを期待しています。