Master PG

プログラムし続ける...

【Polymer 1.0】カスタムエレメントの登録とライフサイクル


目次


カスタムエレメントの登録

カスタムエレメントを登録するにはPolymer()関数を使用します。この関数の引数にはプロトタイプを渡し、このプロトタイプに対して新しく作成するカスタムエレメントの設定を行います。プロトタイプはisを持つ必要があり、これはカスタムエレメントのHTMLタグ名になります。なお仕様では、カスタムエレメントの名前に必ずダッシュ(-)を含む必要があります


⇒ ソースコード

f:id:masterpg:20160921200703j:plain

my-element.html

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

<script>
  // カスタムエレメントの登録
  MyElement = Polymer({
    // isで指定した名前がカスタムエレメントのHTMLタグ名になる
    is: 'my-element',
    // この関数はライフサイクルコールバックの1つです
    created: function () {
      this.textContent = 'My Element!';
    }
  });
</script>

index.html

<!DOCTYPE html>
<html>
<head>
  <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="my-element.html">
</head>
<body>
<script>
  document.addEventListener('WebComponentsReady', function () {
    // createElementでカスタムエレメントのインスタンスを作成
    var myEl1 = document.createElement('my-element');
    // カスタムエレメントのコンストラクタでインスタンスを作成
    var myEl2 = new MyElement();

    // 作成した2つのカスタムエレメントをbodyに追加
    Polymer.dom(document.body).appendChild(myEl1);
    Polymer.dom(document.body).appendChild(myEl2);
  });
</script>
</body>
</html>

Polymer()関数はブラウザへカスタムエレメントの登録を行い、そしてコンストラクタを返します。このコンストラクタでカスタムエレメントのインスタンスを作成できます。

またPolymer()関数は引数で渡されたプロトタイプをもとにプロトタイプチェーンを設定し、カスタムエレメントとPolymerの基底プロトタイプであるPolymer.Baseを関連付けます。


カスタムコンストラクタの定義

Polymer()関数を実行すると基本的なコンストラクタが返され、これを使用してカスタムエレメントをインスタンス化します。ただし基本的なコンストラクタには独自の引数を渡せないので、このような場合は引数のプロトタイプにfactoryImpl()関数を指定します。


⇒ ソースコード

f:id:masterpg:20160921200812j:plain

my-element.html

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

<script>
  MyElement = Polymer({
    is: 'my-element',
    // カスタムコンストラクタを定義し、独自の引数を受け取れるようにする
    factoryImpl: function (name, age) {
      this.name = name;
      this.age = age;
    },
    // この関数はライフサイクルコールバックの1つです
    attached: function () {
      this.textContent = 'name=' + this.name + ', age=' + this.age;
    }
  });
</script>

index.html

<!DOCTYPE html>
<html>
<head>
  <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="my-element.html">
</head>
<body>
<script>
  document.addEventListener('WebComponentsReady', function () {
    // カスタムエレメントに独自の引数を渡し、インスタンスを作成
    var myEl = new MyElement('taro', 18);
    // 作成したカスタムエレメントをbodyに追加
    Polymer.dom(document.body).appendChild(myEl);
  });
</script>
</body>
</html>

Note: Polymer()関数から返されたコンストラクタを実行すると、内部ではdocument.createElement()を使用してインスタンスを作成し、そのあとユーザーが指定したfactoryImpl()関数を実行します。

カスタムコンストラクタの注意点:

  • factoryImpl()関数はコンストラクタを使用してエレメントを作成した場合にのみ呼び出されます。HTMLタグ名を使用してマークアップで作成されたエレメント(<my-element></my-element>のように)や、document.createElement()を使用して作成されたエレメントの場合はfactoryImpl()関数は呼び出されません。

  • factoryImpl()関数はエレメントが初期化された後(readyコールバックの後)に呼び出されます。readyコールバックについてはreadyコールバックとLocal DOMの初期化を参照ください。


ネイティブHTMLエレメントの継承

Polymerは現在ネイティブHTMLエレメントの継承のみをサポートしています。(<input><button>のようなエレメントの継承はサポートしますが、カスタムエレメントの継承はサポートしていません。ただし将来的にはサポートされる予定です)。これらのネイティブエレメントの拡張は型拡張カスタムエレメント(type extension custom elements)と呼ばれます。

ネイティブHTMLエレメントを継承するには、Polymer()関数のプロトタイプ引数にextendsを追加し、継承するエレメントのタグ名を指定します。

マークアップで型拡張カスタムエレメントを使用するには、ネイティブタグにis属性を追加し、作成した型拡張カスタムエレメントの名前を指定します。

<input is="my-input">


次にサンプルプログラムを示します:

⇒ ソースコード

f:id:masterpg:20160921200915j:plain

my-element.html

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

<script>
  MyInput = Polymer({
    is: 'my-input',
    // 継承するネイティブエレメントを指定
    extends: 'input',
    created: function () {
      this.value = 'My input';
      this.style.border = '1px solid red';
    }
  });
</script>

index.html

<!DOCTYPE html>
<html>
<head>
  <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="my-input.html">
</head>
<body>
<!-- isアトリビュートを指定すること -->
<input is="my-input">

<script>
  document.addEventListener('WebComponentsReady', function () {
    // createElementでカスタムエレメントのインスタンスを作成
    var myInputEl1 = document.createElement('input', 'my-input');
    console.log(myInputEl1 instanceof HTMLInputElement); // true
    // カスタムエレメントのコンストラクタでインスタンスを作成
    var myInputEl2 = new MyInput();
    console.log(myInputEl2 instanceof HTMLInputElement); // true
  });
</script>
</body>
</html>


ライフサイクルコールバック

Polymerの基底プロトタイプはいくつかのライフサイクルコールバックを実装しています。カスタムエレメント作成者はこれらコールバックのフック(あとから別のプログラムが処理を追加できるような仕組み)メソッドを実装して独自の処理を記述できます。

  • created
  • ready
  • attached
  • detached
  • attributeChanged


次のサンプルを実行してライフサイクルコールバックの実行順や動きを確認して見てください(これらの動作はデバッグログ出力されるので、ブラウザの開発ツールで確認して下さい)。

  • 削除ボタン: カスタムエレメントを画面から削除
  • 追加ボタン: カスタムエレメントを画面へ追加
  • 属性値変更ボタン: カスタムエレメントのuser-name属性の値を変更

⇒ ソースコード

f:id:masterpg:20160921201018p:plain

my-element.html

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

<script>
  MyElement = Polymer({
    is: 'my-element',
    properties: {
      userName: String
    },
    created: function () {
      console.log(this.localName + ' was created');
    },
    ready: function () {
      console.log(this.localName + ' was ready');
    },
    attached: function () {
      console.log(this.localName + ' was attached');
    },
    detached: function () {
      console.log(this.localName + ' was detached');
    },
    attributeChanged: function (name, type) {
      console.log(this.localName + ' attribute ' + name +
        ' was changed to ' + this.getAttribute(name));
    }
  });
</script>

index.html

<!DOCTYPE html>
<html>
<head>
  <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="my-element.html">
</head>
<body>
<div id="container">
  <my-element user-name="taro">My Element!</my-element>
</div>
<div>
  <input type="button" value="削除" onclick="removeElOnClick()">
  <input type="button" value="追加" onclick="addElOnClick()">
</div>
<div>
  <input is="iron-input" id="userNameInput">
  <input type="button" value="属性値変更" onclick="changeAttrOnClick()">
</div>

<script>
  document.addEventListener('WebComponentsReady', function () {
    // 画面ロード時にカスタムエレメントを取得しておきます
    window.myEl = document.getElementsByTagName('my-element').item(0);
  });

  // コンテナからカスタムエレメントを削除します
  function removeElOnClick() {
    var containerEl = document.getElementById('container');
    Polymer.dom(containerEl).removeChild(window.myEl);
  }

  // コンテナにカスタムエレメントを追加します
  function addElOnClick() {
    var containerEl = document.getElementById('container');
    Polymer.dom(containerEl).appendChild(window.myEl);
  }

  // カスタムエレメントのuser-name属性の値を変更します
  function changeAttrOnClick() {
    var userNameInputEl = document.getElementById('userNameInput');
    Polymer.dom(window.myEl).setAttribute('userName', userNameInputEl.value);
  }
</script>
</body>
</html>


readyコールバックとLocal DOMの初期化

readyコールバックは

  • カスタムエレメント内の<template>の認識
  • Local DOM内の全てのエレメントの処理(バインディング、属性のデシリアライズ、デフォルト値設定…等々)
  • Local DOM内の各エレメントが持つreadyコールバックを全て実行

のような処理が完了し、カスタムエレメント自身のLocal DOMの準備が整うと呼び出されます。

カスタムエレメント作成の際、Local DOMの内部を操作する必要がある場合はreadyコールバックに実装を行ってください。


⇒ ソースコード

f:id:masterpg:20160921201123j:plain

my-element.html

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

<dom-module id="my-element">
  <!-- Local DOM -->
  <template>
    <div>ヘッダー</div>
    <div>
      <!-- readyコールバックでここにタイトルが設定される -->
      <span id="contentTitle"></span>
      <!-- カスタムエレメント利用側で「name="content-value"」が設定されている場合、
           そのカスタムエレメントのコンテンツがここに流し込まれる -->
      <content select="[name='content-value']"></content>
    </div>
    <div>フッター</div>
  </template>

  <script>
    Polymer({
      is: "my-element",
      ready: function () {
        // Local DOM内にある「id="contentTitle"」のエレメントにタイトルを設定する
        this.$.contentTitle.textContent = 'お題は: ';
      }
    });
  </script>
</dom-module>

index.html

<!DOCTYPE html>
<html>
<head>
  <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="my-element.html">
</head>
<body>
<my-element>
  <!-- Local DOMに流し込むコンテンツ -->
  <span name="content-value">readyコールバックとLocal DOMの初期化</span>
</my-element>
</body>
</html>


初期化の順序

カスタムエレメントの基本的な初期化順は次のようになります:

  • createdコールバック
  • Local DOMの初期化
  • readyコールバック
  • factoryImplコールバック
  • attachedコールバック

上記の順序は保証されていますが、兄弟エレメントの初期化順は保証されていません。次の例でいうと<big-brother><little-brother>の初期化順は不動で、どちらが先に初期化が終了するか分かりません。

<!-- 親カスタムエレメント -->
<my-parent>
  <!-- 兄カスタムエレメント -->
  <big-brother></big-brother>
  <!-- 弟カスタムエレメント -->
  <little-brother></little-brother>
</my-parent>

また、親または兄弟エレメントにアクセスするタイミングには注意が必要です。というのも上記例でいうと<big-brother>は初期化が終わっていても<my-parent><little-brother>の初期化が完了していないことがあるからです。もし<big-brother><my-parent><little-brother>にアクセスしたい場合は、そのタイミングが重要になります。親または兄弟エレメントへのアクセスはattachedコールバックの中でasyncを呼び出し、そのコールバックの中で行ってください。

attached: function() {
   this.async(function() {
      // ここでなら親兄弟エレメントにアクセスできる
   });
}


次に初期化順に関するサンプルプログラムを示します:

⇒ ソースコード

f:id:masterpg:20160921201219j:plain

family-elements.html

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

<!-- 兄カスタムエレメント -->
<dom-module id="big-brother">
  <template>
    <p>{{myName}}</p>
  </template>
  <script>
    Polymer({
      is: 'big-brother',
      properties: {
        myName: {type: String, value: '兄'}
      },
      attached: function () {
        this.async(function () {
          // ここでなら親兄弟エレメントにアクセスできる
          var myParentEl = Polymer.dom(this).parentNode;
          var littleBrotherEl = Polymer.dom(this).nextElementSibling;
          console.log(myParentEl.myName + ', ' + this.myName + ', ' + littleBrotherEl.myName);
        });
      }
    });
  </script>
</dom-module>

<!-- 親カスタムエレメント -->
<dom-module id="my-parent">
  <template>
    <p>{{myName}}</p>
    <content></content>
  </template>
  <script>
    Polymer({
      is: 'my-parent',
      properties: {
        myName: {type: String, value: '親'}
      }
    });
  </script>
</dom-module>

<!-- 弟カスタムエレメント -->
<dom-module id="little-brother">
  <template>
    <p>{{myName}}</p>
  </template>
  <script>
    Polymer({
      is: 'little-brother',
      properties: {
        myName: {type: String, value: '弟'}
      }
    });
  </script>
</dom-module>

index.html

<!DOCTYPE html>
<html>
<head>
  <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="family-elements.html">
</head>
<body>
<!-- 親カスタムエレメント -->
<my-parent>
  <!-- 兄カスタムエレメント -->
  <big-brother></big-brother>
  <!-- 弟カスタムエレメント -->
  <little-brother></little-brother>
</my-parent>
</body>
</html>


カスタムエレメントの属性

カスタムエレメントに独自のHTML属性を追加したい場合、Polymer()関数のプロトタイプ引数にhostAttributesを設定します。ここでの指定はキーが属性となり、値がその属性に設定される値となります。次の例でいうとstring-attributeが属性となり'Value'がその属性に設定される値となります。

属性にBoolean型の値を設定した場合について説明します。属性にBoolean型の値を設定するとカスタムエレメント利用側のマークアップに属性が出現する/しないという挙動になります。trueを設定すると利用側のマークアップに属性が出現しますがtrueという値は設定されていません。falseを設定するとは属性自体が出現しません。

カスタムエレメントに属性を追加する例:

<script>
  Polymer({
    is: 'x-custom',
    hostAttributes: {
      'string-attribute': 'Value',
      'boolean-attribute': true,
      'tabindex': 0
    }
  });
</script>

カスタムエレメント利用側のマークアップの結果:

<x-custom string-attribute="Value" boolean-attribute tabindex="0"></x-custom>


次はカスタムエレメントに属性を追加するサンプルプログラムです:

⇒ ソースコード

f:id:masterpg:20160921201323j:plain

x-custom.html

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

<script>
  Polymer({
    is: 'x-custom',
    hostAttributes: {
      'string-attribute': 'Value',
      'boolean-attribute': true,
      'tabindex': 0
    }
  });
</script>

index.html

<!DOCTYPE html>
<html>
<head>
  <script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="x-custom.html">
</head>
<body>
<x-custom></x-custom>
</body>
</html>


Class-styleコンストラクタ

カスタムエレメントのプロトタイプチェインの設定はしたいが、そのカスタムエレメントをすぐにはブラウザへ登録したくない場合Polymer.Class()関数を使用してください。Polymer.Class()関数はPolymer()関数と同じプロトタイプ引数を取り、プロトタイプチェインを設定しますが、ブラウザへの登録は行いません。Polymer.Class()関数が返すコンストラクタはdocument.registerElement()の引数に渡して実行することで、カスタムエレメントをブラウザに登録することができます。この後、このコンストラクタはカスタムエレメントをインスタンス化するのに使用することができます。

カスタムエレメントの定義と登録をワンステップで行いたい場合は、Polymer()関数を使用してください。


⇒ ソースコード

f:id:masterpg:20160921201431j:plain

my-element.html

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

<script>
  // Class-styleコンストラクタを使用したカスタムエレメントを定義
  var MyElement = Polymer.Class({
    is: 'my-element',
    created: function () {
      this.textContent = 'My element!';
    }
  });
</script>

index.html

<!DOCTYPE html>
<html>
<head>
  <script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="my-element.html">
</head>
<body>
<script>
  document.addEventListener('WebComponentsReady', function () {
    // ブラウザへカスタムエレメントを登録
    document.registerElement('my-element', MyElement);

    // この2つの記法は同じ意味
    var myEl1 = new MyElement();
    var myEl2 = document.createElement('my-element');

    // 作成した2つのカスタムエレメントをbodyに追加
    Polymer.dom(document.body).appendChild(myEl1);
    Polymer.dom(document.body).appendChild(myEl2);
  });
</script>
</body>
</html>