HTML5Experts.jp

Polymer v1.1のAPIまとめと周辺リソースの紹介

Googleが開発するWeb ComponentsのライブラリPolymerのバージョン1.1が、2015年8月13日にリリースされました。本記事では、Polymer v1.1のAPIの主要なAPIを解説しつつ、その参考情報を紹介していきます。

また、この記事は「Web ComponentsのこれからーPolymer 0.8、X-Tag、Brick、Bosonic」を事前に読むと理解が深まりますが、これからPolymer v1.1を始めてみるということであれば、本記事単体でも参考にしてもらえればと思います。

Polymer v1.1までの変更点

v0.5からv0.8にかけての差分は前回の記事にて紹介しましたが、その1週間後にv0.9がリリースされ、さらに2週間後にv1.0がリリースされています。

v0.8からv1.0にかけてAPIに変更が加えられていますが、v1.0からv1.1へかけては破壊的なAPIの変更はありません。今回は以降で、Polymer v1.x系の主な機能と、参考リソースの紹介をしていきます。

カスタム要素の登録

<dom-module> に <template> と <script> を記述して宣言します。 <script> 内で実行しているPolymer()関数は、 <dom-module> に指定しているID(カスタム要素の名前)を渡し、その他の引数でカスタム要素の振る舞いを決定します。返り値にはカスタム要素のコンストラクタが返却されるので、それを使ってカスタム要素のインスタンスを生成することも可能です。

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

<dom-module id="my-element">

<template> <style> div { color: red; } </style> <div>Shadow DOM in My Element</div> </template>

<script> var MyElement = Polymer({is: 'my-element'}); </script>

</dom-module>

v0.8との違いは、カプセル化されるカスタム要素のスタイルを記述する <style> と、Polymer関数を実行する <script> を <dom-module> の中に書くようになった点です。v0.5までの <polymer-element> を彷彿させます。v1.1では <template> の外側に <style> を書いてもスタイルが適用されますが、パフォーマンスは悪く推奨されません。

カスタムコンストラクタ

カスタム要素のコンストラクタを自前で書きたい場合は、Polymer()関数の引数として、factoryImplキーワードと共にコンストラクタ関数を渡します。カスタムコンストラクタによって、引数を渡すことが可能になります。

<script>
  var MyElement = Polymer({
    is: 'my-element',
    factoryImpl: function(foo, bar) {
      // custom constructor is called
    }
  });

var myInstance = new MyElement('foo', 100); </script>

このfactoryImplはコンストラクタ関数(ここではMyElement)をnewキーワードと共呼び出した場合のみ実行され、HTMLドキュメントで <my-element> が評価された場合には呼ばれません。

ネイティブ要素の継承

ネイティブ要素を継承したカスタム要素を作る場合は、Polymer()関数の引数にextendsキーワードに継承するネイティブのHTML要素を指定します。v0.5まではカスタム要素の継承もサポートされていましたが、Custom Elementsの仕様としてdocument.registerElement()にカスタム要素名を渡せないのと同様、ブラウザネイティブの要素(inputbuttonなど)のみを指定できるようになっています。

<script>
  var MyElement = Polymer({
    is: 'my-element',
    extends: 'button'
  });
</script>

ネイティブ要素を拡張するカスタム要素を使うには、extendsに指定した要素にカスタム要素名をis属性に指定します。JavaScriptからインスタンスを生成する場合は、コンストラクタをnewするか、document.registerElement()の第二引数にカスタム要素名を指定してください。

<button is="my-element">My Element</button>

<script> console.log(new MyElement() instanceof HTMLButtonElement); // => true console.log(document.createElement('button', 'my-element') instanceof HTMLButtonElement); // => true </script>

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

カスタム要素には4つのライフサイクルが存在し、Polymerでもそれらをハンドリングできます。ライフサイクルについてはWeb Componentsを構成する4つの仕様 ー Web Components基礎編という記事の「カスタム要素の挙動を設定する」をセクションにて解説しています。ブラウザネイティブではcreatedCallbackattachedCallbackdetachedCallbackattributeChangedCallbackの4つが定義されていますが、PolymerではCallbackが省略されている他、readyというShadow Root配下のDOM構築が完了したタイミングで発火されるハンドラも定義できるようになっています。

<script>
  Polymer({
    is: 'my-element',
    created: function () {
      console.log('my-elementが生成されました');
    },
    attached: function () {
      console.log('my-elementがHTMLドキュメントに追加されました');
    },
    detached: function () {
      console.log('my-elementがHTMLドキュメントから切り離されました');
    },
    attributeChanged: function (name, type) {
      console.log('my-elementの' + name + '属性が変更されました');
    },
    ready: function () {
      console.log('my-elementのDOM構築が完了しました');
    }
  });
</script>

ハンドラの実行順序をまとめると以下のようになります。

  1. createdコールバック
  2. readyコールバック
  3. factoryImplコールバック
  4. attachedコールバック
  5. (detachedコールバック)

behaviorsを使った振る舞いの制御

カスタム要素の振る舞いを制御するには、これまでのようにPolymer()に直接ハンドラなどを指定するほか、behaviorsにプロトタイプとなるようなオブジェクトを渡すことでも可能です。オブジェクトで定義できるのは先程登場したライフサイクルコールバック、後述するデフォルト属性やプロパティ、オブザーバー(observer)、イベントハンドラの登録(listener)です。

<script>
var MyBehavior = {
  ready: function() {
    console.log('DOM is ready');
  }
};

Polymer({ is: 'my-element', behaviors: [MyBehavior] }); </script>

見ての通り、behaviorsにはオブジェクトを配列で指定することが可能です。複数指定されてハンドラが重複する(例えば、複数のオブジェクトでreadyが定義されている)場合は、配列上で後から出現するオブジェクトが優先されます(つまり右辺が優先されます)。

カスタム要素のデフォルト属性

カスタム要素のデフォルト属性は、引数のhostAttributesに定義します。hostAttributesに指定した属性とその値は、カスタム要素の属性のデフォルト値になります。

<script>
  var MyElement = Polymer({
    is: 'my-element',
    hostAttributes: {
      role: 'navigation'
    }
  });
</script>

この時 <my-element> は、デフォルトで <my-element role="navigation"></my-element> として評価されます。

カスタム要素のプロパティ

カスタム要素のプロパティは、引数のpropertiesに定義します。プロパティ名をキーに、typevalueなどの属性を渡すことでプロパティの特徴を定義します。簡略化された形としては、propertyName: typeという指定も可能です

<script>
  Polymer({
    is: 'my-element',
    properties: {
      foo: {
        type: Number,
        value: 0,
        observer: 'fooChanged'
      }
      bar: {
        type: String,
        computed: 'computeBar(foo)'
      }
    }
    fooChanged: function(newValue, oldValue) {
      // ...
    },
    computeBar: function(foo) {
      return String(foo + 100);
    }
  });
</script>

ここではfoobarの2つをmy-elementのプロパティとして定義していますが、それぞれobservercomputedという属性を渡しています。

オブザーバー関数を指定するobserver

observerはプロパティの変更を監視するオブザーバー関数を指定する属性です。ここではfooChangedという関数を定義しているので、関数名を文字列で渡しています。v0.5まではpropertyNameChangedといったような命名規則を用いてオブザーバー関数を定義していましたが、v1.1においてその機能はありません。監視する関数には引数として、変更後の値(newValue)と変更前の値(oldValue)が引数と渡されるので、前後値を利用することができます。

コンピュート関数を指定するcomputed

computedはプロパティの値を動的に算出するための関数を指定する属性です。observer同様に関数名を文字列で渡しますが、ここではbarプロパティに対しcomputeBar(foo)という値を渡しています。この記述によってcomputeBar関数にはfooプロパティが引数として渡され、barの値は関数が返却する値を参照します。

HTML属性に反映するかどうかを指定するreflectToAttribute

プロパティの値に変更があっても通常はHTML属性に反映されませんが、reflectToAttributetrueを指定するとHTMLの属性も更新されるようになります。

<script>
  Polymer({
    is: 'my-element',
    properties: {
      foo: {
        type: String,
        reflectToAttribute: true
      }
    },
    ready: function() {
      this.foo = 'Hello!';
      // this.setAttribute('foo', 'Hello!');と同等の振る舞いが得られる
    }
  });
</script>

この時、readyの関数が実行されたタイミングで <my-element></my-element> は <my-element foo="Hello!"></my-element> となり、setAttribute('foo', 'Hello!')を実行したときと同様の振る舞いが得られます。

イベントハンドラの登録

v0.5以前まではイベントハンドラの登録をブラケットを使って宣言的に定義していましたが、v1.1からはブラケットを使わなくなった他、listenersプロパティにイベントとそのハンドラをハッシュ形式で定義が可能です。

<dom-module id="my-element">

<template> <button on-click="clickHandler">Button</button> </template>

<script> var MyElement = Polymer({ is: 'my-element', listeners: { 'tap': 'myElementTapHandler' }, clickHandler: function(e, detail) { // button click handler }, myElementTapHandler: function(e, detail) { // my-element tap handler } }); </script>

</dom-module>

ジェスチャーイベントのハンドリング

先程のサンプルでtapというDOMネイティブには存在しないイベントに対してmyElementTapHandlerというハンドラを登録していますが、Polymerは自前で実装するにはやや面倒な4つのイベントをサポートしています。

それぞれのイベントのハンドラに渡されるイベント引数のdetailプロパティには様々な付加情報が渡されます。詳しくは公式ドキュメントを確認してください。

カスタムイベントの発火

fireという関数を実行することで、ホスト要素を呼び出し元オブジェクトとしてカスタムイベントを発行することが出来ます。引数にはカスタムイベント名と、ハンドラのdetailプロパティに渡すデータを指定します。

<dom-module id="my-element">

<template> <button on-click="clickHandler">Button</button> </template>

<script> var MyElement = Polymer({ is: 'my-element', clickHandler: function(e, detail) { this.fire('foo', { bar: 100 }); } }); </script>

</dom-module>

<my-element></my-element>

<script> var myElement = document.querySelector('my-element'); myElement.addEventListener('foo', function(e) { console.log(e.detail.bar); // => 100 }); </script>

ここでは <my-element> のボタンをクリックした時にfooというカスタムイベントを発行し、付加データとして{ bar: 100 }という値を渡しています。

データバインディング

データバインディングは以前と同様にカーリーブラケット{{}}およびスクエアブラケット[[]]で指定します。ブラケットではプロパティおよびコンピュート関数を評価することが可能で、v0.5まで有効だった演算子などはサポートされていません。

<dom-module id="my-element">

<template> <input value="{{foo}}" type="text"> <input value="[[bar]]" type="text"> </template>

<script> Polymer({ is: 'my-element', properties: { foo: String, bar: Number } }); </script>

</dom-module>

ここでは<my-element>のプロパティとして定義しているfoobarを、 <input> のvalueにバインディングしています。これによってfoobarの値に変更があると、<input>に自動で反映されます。

カーリーブラケット{{}}とスクエアブラケット[[]]

カーリーブラケット{{}}とスクエアブラケット[[]]の差は、データバインディングの方向が一方向に制限されるかどうかの違いです。{{}}のデータバインディングは双方向か一方向かが記述によって変わりますが、[[]]は一方向のみであり参照しているプロパティを受け取るのみです。

カーリーブラケット{{}}で指定した際にバインディングの振る舞いを決定するのは、プロパティのnotifyフラグとreadOnlyフラグです。

<script>
  Polymer({
    is: 'my-element',
    properties: {
      foo: {
        type: String,
        notify: true
      },
      bar: {
        type: Number,
        readOnly: true
      }
    }
  });
</script>

notifyフラグはプロパティの変更をホスト要素に通知するかどうか、readOnlyフラグはプロパティが読み取り専用かどうかを決定します。この例ではnotify: trueなので、 <my-element foo="{{value}}"></my-element> としたときにfooに変更があるとvalueへ値が反映されます。

ネイティブ要素との双方向データバインディング

ネイティブ要素との双方向データバインディングも target-prop="{{hostProp::target-change-event}}" という構文でサポートしています。ここでは<input>に入力された値に括弧を付与して<div>に埋め込んでいます。

<dom-module id="my-element">

<template> <input value="{{foo::input}}" type="text"> <div>{{computeBar(foo)}}</div> </template>

<script> Polymer({ is: 'my-element', properties: { foo: { type: String } }, computeBar: function(foo) { return 「${foo}」; } }); </script>

</dom-module>

入力された値を参照するタイミングはinputイベントで、バインドするホストプロパティはfooなので、 input value="{{foo::input}}"> になります。これによって、ネイティブ要素のプロパティからカスタム要素へのプロパティへのデータバインディングが行われます。

属性とのデータバインディング

属性に対してデータをバインディングするには以下のように$=を使い、ブラケット{{}}にはこれまでと同様にカスタム要素のプロパティやコンピュート関数を記述します。

<template>

<!-- Attribute binding --> <my-element selected$="{{value}}"></my-element> <!-- results in <my-element>.setAttribute('selected', this.value); -->

<!-- Property binding --> <my-element selected="{{value}}"></my-element> <!-- results in <my-element>.selected = this.value; -->

</template>

プロパティとのデータバインディングには <my-element foo={{value}}> と記述することでdocument.querySelector('my-element').foo = valueを振る舞いますが、HTMLの属性にバインディングするためには <my-element foo$={{value}}> のように$=で指定することで document.querySelector('my-element').setAttribute('foo', value) が動的に実行されます。

ネイティブの属性であるclassstyleには、次のように$=でバインディングすることが可能です。

<!-- class -->
<div class$="{{foo}}"></div>

<!-- style --> <div style$="{{background}}"></div>

<!-- href --> <a href$="{{url}}">

<!-- label for --> <label for$="{{bar}}"></label>

<!-- dataset --> <div data-bar$="{{baz}}"></div>

HTML上の属性名とDOMのプロパティ名が異なる場合には留意が必要でしょう。data-*の場合、data-fooへの実際のプロパティはelement.dataset.fooとなるため、data-foo={{bar}}ではバインディングできず、element.setAttribute('data-foo', bar)となるdata-foo$={{bar}}を使う必要があります。

Templateのリピート

配列のデータは <template is="dom-repeat"> を使ってリピートし、要素ひとつひとつを参照することが出来ます。以下はis="dom-repeat"を使って配列を展開しているサンプルです(公式より引用)。

<dom-module id="employee-list">

<template> <div> Employee list: </div> <template is="dom-repeat" items="{{employees}}"> <div># <span>{{index}}</span></div> <div>First name: <span>{{item.first}}</span></div> <div>Last name: <span>{{item.last}}</span></div> </template> </template>

<script> Polymer({ is: 'employee-list', ready: function() { this.employees = [ {first: 'Bob', last: 'Smith'}, {first: 'Sally', last: 'Johnson'} ]; } }); </script>

</dom-module>

<template is="dom-repeat"> に加えて展開したい配列をitems属性で指定します。配列の要素の数だけ <template > の内部が繰り返されますが、この中では要素を表すitemと配列のインデックスを示すindexという二つの変数を参照できます。

itemsで参照している配列に変更を加える場合には、インスタンスの関数をそのまま使うのではなくPolymerのインスタンスが備えている専用の関数を使います。例えばこのサンプルのemployeesに値を追加する場合は、 this.employees.push({...})ではなくthis.push('employees', {...})とする必要があります。pushの他にはpopspliceshiftunshiftも同様です。これらの関数を使わないと、配列への変更が検知されずに再描画されません。

Polymerのデータバインディングの更なる詳細についてはData binding – Polymer 1.0を参照してください。

Polymer内部で行うDOM操作

<dom-module>内部のDOMの操作を行う場合は、Shadow RootのDOM APIではなく専用のインターフェースが用意されています。

IDが与えられている要素の参照

要素に対してIDが与えられていると、this.$.idという形で自動的に参照が生成されます。これはv0.5まで存在していた機能なので、馴染みのある人もいるかと思います。

<dom-module id="my-element">

<template> This is my-element <span id="foo"></span>! </template>

<script> Polymer({ is: 'my-element', ready: function() { this.$.foo.textContent = this.foo; } }); </script>

</dom-module>

また、動的に生成された要素を参照する場合はthis.$$(selector)という関数が用意されています。これにCSSセレクタを渡すと、セレクタにマッチする最初の要素が返却されます。

Polymer.dom()を使ったDOM操作

Polymer内部のDOM操作を行う場合は、Shadow Root配下の要素のDOM APIを直接実行するのではなく、Polymer.dom()という専用のAPIが用意されています。Polymer.dom()の引数にはNodeを渡し、返却されるオブジェクトには、ネイティブのDOM APIと同等の振る舞いをするAPIが用意されています。いずれもDOMのAPIと同じ命名と引数で設計されていますが、サブセットとして用意されているに過ぎず、完全に互換性があるわけではないことに注意してください。

親子関係

クエリ

コンテンツの参照

属性の操作

Polymer.domから行う追加や削除といったDOM操作は、パフォーマンスを考慮し遅延して実行するようになっています。そのため、操作後のノードの座標やgetComputedStyle()を使ったスタイルの取得をする場合は、操作をPolymer.dom.flush()を使って適時実行し、反映します。

カスタム要素のスタイリング

CSSのスタイリングは、これまでと同様にShadow DOMをスタイリングする記法が適用されます。PolymerはShadow DOMの実装状況で振るまいが変わらないように、Shady DOMという軽量なポリフィルを採用しているため、CSSセレクタをネイティブと同様に書くことはできません。

例えば、<content>を参照する::contentセレクタです。Shadow DOMがブラウザネイティブに実装されていれば単一でも評価されるはずですが、Shady DOMの互換性として::contentセレクタには何らかの親セレクタを指定する必要があります。これは ::content .bar と記述された場合にShady DOMで実現しているスコープを再現できなくなるためです。この場合、 .foo > ::content .bar と書く必要があり、実際には .foo > .bar のように評価されます。

<dom-module id="my-element">

<template> <style> :host { display: block; border: 1px solid red; } .foo > ::content .bar { background: orange; } </style>

&lt;div class="foo"&gt;&lt;content&gt;&lt;/content&gt;&lt;/div&gt;

</template>

<script> Polymer({ is: 'my-element' }); </script>

</dom-module>

まとめ

Polymer v1.1の主な機能について解説しました。v0.5 → v0.8 → v0.9 → v1.0…とAPIに多くの変更が加えられてきましたが、メジャーリリースになったことで、これまでのような大きな変更は( おそらく )なくなるでしょう。

最後に、今年9月の14日〜15日にオランダのアムステルダムにて開催されたPolymer Summit 2015にて、参考リソースが多く公開されたのでそれを紹介します。

Polymer Codelabs

Polymer Codelabsは、Polymerを使ったアプリケーションを作成していくチュートリアル集です。Polymer Summitに合わせて11個のチュートリアルが公開されています。こちらも英語ですが、環境のセットアップから丁寧に解説されています。

セッション動画リスト

Polymer Summit 2015の各セッションはすべて以下のチャンネルで配信されていますので、興味のある方は是非チェックしてみてください。