Custom Elementsは、Webにコンポーネントモデルを提供します。Custom Elementsの仕様で実現されるのは以下の項目です。:

  • クラスをCustom Elementの名前に関連付けるためのメカニズム。
  • Custom Elementのインスタンスの状態が変化した(例えば、ドキュメントに追加またはドキュメントから削除された)際に呼び出される一連のライフサイクルコールバック。
  • インスタンス上で指定した属性グループのいずれかが変更された際に呼び出されるコールバック。

まとめると、これらの機能を利用することで、状態変化に応じて処理を行う独自のパブリックAPIを持ったエレメントが構築できるようになります。

このドキュメントでは、Polymerに関連したCustom Elementsの概要について説明します。Custom Elementsの詳細な解説は、Custom Elements v1: Reusable Web Componentsを参照してください。

Custom Elementを定義するには、ES6のクラスを作成し、それをCustom Elementの名前に関連付けます。

// Create a class that extends HTMLElement (directly or indirectly)
class MyElement extends HTMLElement { … };

// Associate the new class with an element name
window.customElements.define('my-element', MyElement);

標準のエレメントと同じように、Custom Elementを利用できます。:

<my-element></my-element>

または:

const myEl = document.createElement('my-element');

または:

const myEl = new MyElement();

エレメントのクラスには、その動作(behavior)とパブリックAPIを定義します。クラスは、HTMLElementクラスまたは、そのサブクラスの一つ(例えば、他のCustom Element)を拡張する必要があります。

Custom element names. 仕様上、**Custom Elementの名前は、小文字のASCII文字で始まり、ダッシュ(-)を含めなければなりません。**使用済みの名前は短いリストで管理され、それと一致する名前に使用は禁止されています。詳細は、HTML仕様のCustom elements core conceptsを参照してください。

Polymerは、基本的なCustom Elementの仕様に対して付加的な機能群を提供します。これらの機能をエレメントに付加するには、Polymer Elementの基底クラスPolymer.Elementを拡張します。:

<link rel="import" href="/bower_components/polymer/polymer-element.html">

<script>
  class MyPolymerElement extends Polymer.Element {
    ...
  }

  customElements.define('my-polymer-element', MyPolymerElement);
</script>

Polymerは基本的なCustom Elementに対して以下の機能を付与します。:

  • 一般的なタスクを処理するためのインスタンスメソッド。
  • 対応する属性に応じて特定のプロパティを設定するなど、プロパティと属性を自動的に処理するための機能。
  • <template>の記述を元にエレメントのインスタンスにShadow DOMツリーを生成する。
  • データバインディング、プロパティ変更のオブザーバーや算出プロパティをサポートするデータシステム。

Custom Elementの仕様では、「Custom Elementのリアクション(reactions)」と呼ばれる一連のコールバックが定義されています。これによって、特定のライフサイクルの変化に応じてユーザーコードを実行することができます。

リアクション 説明
constructor エレメントがアップグレードされたとき(つまり、エレメントが作成されたとき(created)、あるいはすでに作成されたエレメントが定義された(defined)とき)に呼び出されます。
connectedCallback エレメントがドキュメントに追加されたときに呼び出されます。
disconnectedCallback エレメントがドキュメントから削除されたときに呼び出されます。
attributeChangedCallback エレメントのいずれかの属性が変更、追加、削除または置換されたときに呼び出されます。

各リアクション(訳注:リアクションはコールバックの仕様上の呼称)は、実装の最初の行で、スーパークラスのコンストラクタまたはリアクションを呼び出す必要があります。constructorの場合には、以下のように単にsuper()を呼び出すだけです。

constructor() {
  super();
  // …
}

その他リアクションについては、スーパークラスのメソッドを呼び出します。これは、Polymerがエレメントにライフサイクルコールバックを導入するために必要になります。

connectedCallback() {
  super.connectedCallback();
  // …
}

エレメントのコンストラクタにはいくつか特別な制限があります。:

  • コンストラクタ本体の最初の行で、superメソッドを引数なしで呼び出さなければなりません。
  • 単純な早期return(returnまたはreturn this)を意図とするのでない限り、コンストラクタにreturn文を含めることはできません。
  • コンストラクタでエレメント自身の属性や子を調べたり追加したりすることはできません。

コンストラクタの制限事項について完全なリストは、WHATWGが公開するHTML仕様のRequirements for custom element constructorsを参照してください。

可能であれば常に、コンストラクタ内ではなく、connectedCallbackより後に遅延して実行するようにしてください。

Custom Elementsの仕様では、ワンタイムの初期化コールバックは提供されていません。そこでPolymerは、エレメントがDOMに初めて追加されたときだけ呼び出されるreadyコールバックを提供しています。

ready() {
  super.ready();
  // When possible, use afterNextRender to defer non-critical
  // work until after first paint.
  Polymer.RenderStatus.afterNextRender(this, function() {
    ...
  });
}

Polymer.Elementクラスは、readyコールバックの中でエレメントのテンプレートやデータシステムを初期化します。したがって、readyコールバックを上書きする場合には、独自のreadyのどこかでsuper.ready()呼び出す必要があります。

スーパークラスのreadyメソッドから返る(returnする)と、エレメントのテンプレートはインスタンス化され、プロパティの初期値が設定された状態になります。ただし、Light DOMエレメントは、readyが呼び出された時点で割り当てられて(distributed)いないかもしれません。

エレメントのLight DOMの子やプロパティ値のように、動的な値を元にエレメントを初期化する際は、readyコールバックを使用しないでください。代わりに、プロパティの変更に対して処理を行うのであればオブザーバーを設定し、追加・削除される子に対して処理を行うにはobserveNodesメソッドやslotChangedイベントによる監視を行ってください。

関連トピック:

仕様によれば、Custom Elementsは定義する前であっても利用することができます。エレメントの定義が追加されると、既存のエレメントのインスタンスはすべてカスタムクラスにアップグレードされます。

例として、次のようなコードを考えます。:

<my-element></my-element>
<script>
  class MyElement extends HTMLElement { ... };

  // ...some time much later...
  customElements.define('my-element', MyElement);
</script>

このページを解析(parsing)すると、ブラウザはスクリプトを解析して実行する前に<my-element>のインスタンスを生成します。この場合、エレメントはMyElementではなくHTMLElementのインスタンスとして生成されます。エレメントが定義されると、<my-element>のインスタンスはアップグレードされ適切なクラス(MyElement)になります。クラスのコンストラクタは、アップグレードの過程で呼び出され、その後他の待機中のライフサイクルコールバックが続けて呼び出されます。

エレメントをアップグレードさせることで、エレメントの初期化にかかるコストを遅延させながらDOMに追加することができます。これは進歩的な機能の強化といえるでしょう。

エレメントは、Custom Elementの状態として次のいずれかを持っています。:

  • "uncustomized":エレメントには有効な名がありません。これは、ビルトインエレメント(<p><input>)または、Custom Elementになることができない未知のエレメント(<nonsense>)のどちらかです。
  • "undefined":エレメントは(my-elementのような)有効なCustom Element名を持つが、まだ定義はされていません。
  • "custom":エレメントは有効なCustom Element名を持ち、定義もなされ、アップグレードもされています。
  • "failed":エレメントのアップグレードに失敗しました(例えば、クラスが有効でない場合)。

Custom Elementの状態はプロパティとして公開されませんが、エレメントが定義済みかどうかに関わらずスタイルを設定することはできます。

"custom"及び"uncustomized"状態にあるエレメントは、定義済み(defined)であるとみなされます。定義済みのエレメントに対しては、擬似クラスセレクタ:definedを使用することができます。これを利用して、エレメントがアップグレードされる前のプレースホルダ用スタイルを提供できます。:

my-element:not(:defined) {
  background-color: blue;
}

:definedは、Custom Elementsのポリフィルではサポートされていません。 この問題についてはdocumentation on styling を参照してください。

Custom Elementは、HTMLElementだけでなく他のCustom Elementを拡張することもできます。:

class ExtendedElement extends MyElement {
  static get is() { return 'extended-element'; }

  static get properties() {
    return {
      thingCount: {
        value: 0,
        observer: '_thingCountChanged'
      }
    }
  }
  _thingCountChanged() {
    console.log(`thing count is ${this.thingCount}`);
  }
};

customElements.define(ExtendedElement.is, ExtendedElement);

Polymerは現在、ビルトインエレメントの拡張をサポートしていません。の仕様では、buttoninputのようなビルトインエレメントを拡張するためのメカニズムを用意します。仕様では、これらのエレメントを「カスタマイズされたビルトインエレメント(customized built-in elements)」と呼んでいます。カスタマイズされたビルトインエレメントには、多くの利点があります。(例えば、buttoninputのようなビルトインUIエレメントにユーザーアクセシビリティ機能(accessibility feature)を提供することができます)しかし、すべてのブラウザベンダーがカスタマイズされたビルトインエレメントをサポートすることに同意おらず、現時点でPolymerはそれらをサポートしていません。

Custom Elementsを拡張すると、Polymerは propertiesオブジェクトと 配列observersについて特別な扱いをします。エレメントをインスタンス化するとき、Polymerはプロトタイプチェーンを巡回し これらのオブジェクトをフラット化します。そうすることで、スーパークラスによって定義されたプロパティやオブザーバーが、サブクラスにも追加されます。 。

サブクラスはスーバークラスからテンプレートを継承することもできます。詳細は、Inherited templatesを参照してください。

ES6のクラスでは単一継承のみサポートしており、異なるエレメント間でコードを共有するには困難が伴います。クラス式のミックスイン(Mixin)を利用すると、共通のスーパークラスを加えることなくエレメント間でコードを共有できるようになります。

クラス式のミックスインは基本的にクラスのファクトリーとして機能する関数です。以下の例のように、スーパークラスを引数として渡すことで、関数はミックスインのメソッドを使いスーパークラスを拡張した新たなクラスを生成します。

const fancyDogClass = FancyMixin(dogClass);
const fancyCatClass = FancyMixin(catClass);

エレメントにミックスインを追加するには以下のように記述します。:

class MyElement extends MyMixin(Polymer.Element) {
  static get is() { return 'my-element' }
}

もしわかりにくい場合には、2つのステップに分けて考えるといいかもしれません。:

// Create new base class that adds MyMixin's methods to Polymer.Element
const polymerElementPlusMixin = MyMixin(Polymer.Element);

// Extend the new base class
class MyElement extends polymerElementPlusMixin {
  static get is() { return 'my-element' }
}

継承の階層は次のようになります。:

MyElement <= polymerElementPlusMixin <= Polymer.Element

Polymer.Elementだけでなく、任意のエレメントクラスにミックスインを適用することができます:

class MyExtendedElement extends SomeMixin(MyElement) {
  ...
}

複数のミックスインを順々に(in sequence)適用することもできます。:

class AnotherElement extends AnotherMixin(MyMixin(Polymer.Element)) { … }

ミックスインはクラスを受け取り、サブクラスを返すシンプルな関数です。:

MyMixin = function(superClass) {
  return class extends superClass {
    constructor() {
      super();
      this.addEventListener('keypress', e => this.handlePress(e));
    }

    static get properties() {
      return {
        bar: {
          type: Object
        }
      };
    }

    static get observers() {
      return [ '_barChanged(bar.*)' ];
    }

    _barChanged(bar) { ... }

    handlePress(e) { console.log('key pressed: ' + e.charCode); }
  }
}

以下は、ES6のアロー関数を使った例です。:

MyMixin = (superClass) => class extends superClass {
  ...
}

ミックスインは、通常のエレメントのクラスのように、プロパティ、オブザーバーやメソッドを定義することができます。また、他のミックスインと合成することをもできます。:

MyCompositeMixin = (base) => class extends MyMixin2(MyMixin1(base)) {
  ...
}

ミックスインは単に継承チェーンにクラスを追加するだけなので、通常の継承に関するルールは全て適用されます。例えば、ミックスインクラスにはコンストラクタを定義でき、superを使ってスーパークラスメソッドを呼び出すことができるといった点です。

ミックスインをドキュメント化しましょう Polymerビルドツールとlintツールには、プロパティが使用するミックスインとエレメントを解析するために追加のドキュメンテーションタグがいくつか必要です。ドキュメンテーションタグがなければ、ツールは警告を記録されます。ミックスインのドキュメント化の詳細は、Class mixinsを参照してください。

作成したミックスインを他のグループと共有したり、公開したりする場合には、以下の手順が推奨されます。:

  • Polymer.dedupingMixin関数を使用して、一度しか適用できないミックスインを作成する。

  • 同じ名前を持つ可能性があるミックスインやクラスが他者のものと衝突しないように、ミックスインに一意の名前空間を提供します。

dedupingMixin関数は、他のミックスインで使用されているミックスインが意図せず複数回適用することを避けるのに役立ちます。例えば、 MixinAMixinBMixinCを含んでいて、 MixinAだけでなくMixinBを直接利用する場合には:

class MyElement extends MixinB(MixinA(Polymer.Element)) { ... }

この時点で、あなたのエレメントにはそのプロトタイプチェーンにMixinBの2つのコピーが存在することになります。dedupingMixin はミックスイン関数を引数にとりますが、返り値のミックスイン関数においては重複が回避されます。:

dedupingMixinB = Polymer.dedupingMixin(mixinB);

dedupingミックスインには2つの利点があります。第一に、このミックスインを使用するたびに、生成されたクラスが記憶され、繰り返し使用された場合にはこの基底クラスのオブジェクトが返されます。これはオブジェクトAの僅かばかりの最適化になります。

さらに重要なことは、dedupingミックスインは、すでに基底クラスのプロトタイプチェーンのどこかでミックスインが利用されたかどうかをチェックします。そして利用済みの場合には、ミック単に基底クラスを返します。上記の例では、 両方のmixinBに変えてdedupingMixinBを利用した場合、ミックスインは一度だけ適用されることになります。

次のサンプルコードは、dedupingミックスインを利用して名前空間を生成する一つの例です。:

// Create my namespace, if it doesn't exist
if (!window.MyNamespace) {
  window.MyNamespace = {};
}

MyNamespace.MyMixin = Polymer.dedupingMixin((base) =>

  // the mixin class
  class extends base {
    ...
  }
);

詳細情報:Web Fundamentals上のCustom Elements v1:reusable web components