2019年7月18日

Custom elements

独自のメソッドやプロパティ、イベントなどを持つ、独自のクラスで記述されたカスタムHTML要素を作成することができます。

一度カスタム要素が定義されると、組み込みのHTML要素と同じようにそれを使用できます。

HTMLの種類は豊富ですが、無限ではないので、これは素晴らしいことです。<easy-tabs>, <sliding-carousel>, <beautiful-upload> などはありません。他に必要となるタグについて考えてください。

特別なクラスでそれらを定義し、それ以降はあたかもそれがHTMLの一部であるかのように使用することができます。

Custom element(カスタム要素)には2種類あります。:

  1. 自律型カスタム要素(Autonomous custom elements) – 抽象的な HTMLElement クラスを拡張した “すべてが新規” の要素です。
  2. カスタマイズされた組み込み要素(Customized built-in elements) – カスタマイズされた HTMLButtonElement のような、組み込み要素を拡張した要素です。

最初に自律型要素を作成し、その次にカスタマイズされた組み込み要素を作成していきます。

カスタム要素を作るには、ブラウザにいくつかの詳細を教える必要があります: どのように表示するか、要素がページ上に追加されたり削除されたときの何をするか、などです。

これらは特別なメソッドを持つクラスを作ることで行います。メソッドはわずかで、すべてオプションなので簡単です。

これは完全な一覧のスケッチです。:

class MyElement extends HTMLElement {
  constructor() {
    super();
    // 要素が作成されました
  }

  connectedCallback() {
    // ブラウザは要素が document に追加された時にこれを呼びます
    // (要素が繰り返し追加/削除される場合、何度も呼ばれます)
  }

  disconnectedCallback() {
    // ブラウザは要素が document から削除された時にこれを呼びます
    // (要素が繰り返し追加/削除される場合、何度も呼ばれます)
  }

  static get observedAttributes() {
    return [/* 変更を監視する属性名の配列 */];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // 上で挙げたいずれかの属性が変更されたときに呼ばれます
  }

  adoptedCallback() {
    // 要素が新しい document に移動されたときに呼ばれます
    // (document.adoptNode で発生しますが、めったに使われません)
  }

  // その他の要素のメソッドやプロパティ
  // ...
}

この後、要素を登録する必要があります。:

// <my-element> が我々が作った新たなクラスによって提供されることをブラウザに知らせます。
customElements.define("my-element", MyElement);

これで、タグ <my-element> の HTML 要素に対しては、MyElement のインスタンスが作成され、前述のメソッドが呼び出されます。JavaScript で document.createElement('my-element') をするのでもOKです。

Custom element(カスタム要素) の名前はハイフン - を含まなければいけません

カスタム要素の名前にはハイフン - を含む必要があります。e.g. my-elementsuper-button は有効ですが、myelement はだめです。

これは、組み込み要素とカスタムHTML要素間に名前の衝突がないことを保証するためです。

例: “time-formatted”

例えば、日付/時刻に関して、HTML にはすでに <time> が存在します。ですが、それ自体では何もフォーマットは行いません。

言語を意識したフォーマットで時刻を表示する <time-formatted> 要素を作成しましょう。:

<script>
class TimeFormatted extends HTMLElement { // (1)

  connectedCallback() {
    let date = new Date(this.getAttribute('datetime') || Date.now());

    this.innerHTML = new Intl.DateTimeFormat("default", {
      year: this.getAttribute('year') || undefined,
      month: this.getAttribute('month') || undefined,
      day: this.getAttribute('day') || undefined,
      hour: this.getAttribute('hour') || undefined,
      minute: this.getAttribute('minute') || undefined,
      second: this.getAttribute('second') || undefined,
      timeZoneName: this.getAttribute('time-zone-name') || undefined,
    }).format(date);
  }

}

customElements.define("time-formatted", TimeFormatted); // (2)
</script>

<!-- (3) -->
<time-formatted datetime="2019-12-01"
  year="numeric" month="long" day="numeric"
  hour="numeric" minute="numeric" second="numeric"
  time-zone-name="short"
></time-formatted>
  1. このクラスはメソッドを1つだけ持っています(connectedCallback())。ブラウザは、<time-formatted> 要素がページに追加されたとき(あるいは HTML パーサーがそれを検出したとき)にこれを呼び出します。その中では、ブラウザ間で十分にサポートされている組み込みの Intl.DateTimeFormat データフォーマッターを使用しています。
  2. customElements.define(tag, class) で新しい要素を登録します。
  3. その後、どこでもそれを使うことができます。
カスタム要素のアップグレード

ブラウザが customElements.define の前に <time-formatted> を見つけた場合、エラーにはなりません。しかし、要素はまだ知られていないので、非標準のタグのようになります。

このような “未定義の” 要素は CSS セレクタで :not(:defined) としてスタイルすることができます。