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がリリースされています。
- 0.8 released! – polymer blog
- 0.9 released! – polymer blog
- 1.0 – polymer blog
- 1.1 Release – polymer blog
- Migration guide
v0.8からv1.0にかけてAPIに変更が加えられていますが、v1.0からv1.1へかけては破壊的なAPIの変更はありません。今回は以降で、Polymer v1.x系の主な機能と、参考リソースの紹介をしていきます。
カスタム要素の登録
<dom-module> に <template> と <script> を記述して宣言します。 <script> 内で実行しているPolymer()
関数は、 <dom-module> に指定しているID(カスタム要素の名前)を渡し、その他の引数でカスタム要素の振る舞いを決定します。返り値にはカスタム要素のコンストラクタが返却されるので、それを使ってカスタム要素のインスタンスを生成することも可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<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
キーワードと共にコンストラクタ関数を渡します。カスタムコンストラクタによって、引数を渡すことが可能になります。
1 2 3 4 5 6 7 8 9 10 |
<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()
にカスタム要素名を渡せないのと同様、ブラウザネイティブの要素(input
やbutton
など)のみを指定できるようになっています。
1 2 3 4 5 6 |
<script> var MyElement = Polymer({ is: 'my-element', extends: 'button' }); </script> |
ネイティブ要素を拡張するカスタム要素を使うには、extends
に指定した要素にカスタム要素名をis
属性に指定します。JavaScriptからインスタンスを生成する場合は、コンストラクタをnew
するか、document.registerElement()
の第二引数にカスタム要素名を指定してください。
1 2 3 4 5 6 7 8 |
<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基礎編という記事の「カスタム要素の挙動を設定する」をセクションにて解説しています。ブラウザネイティブではcreatedCallback
・attachedCallback
・detachedCallback
、attributeChangedCallback
の4つが定義されていますが、PolymerではCallback
が省略されている他、ready
というShadow Root配下のDOM構築が完了したタイミングで発火されるハンドラも定義できるようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<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> |
ハンドラの実行順序をまとめると以下のようになります。
created
コールバックready
コールバックfactoryImpl
コールバックattached
コールバック- (
detached
コールバック)
behaviors
を使った振る舞いの制御
カスタム要素の振る舞いを制御するには、これまでのようにPolymer()
に直接ハンドラなどを指定するほか、behaviors
にプロトタイプとなるようなオブジェクトを渡すことでも可能です。オブジェクトで定義できるのは先程登場したライフサイクルコールバック、後述するデフォルト属性やプロパティ、オブザーバー(observer
)、イベントハンドラの登録(listener
)です。
1 2 3 4 5 6 7 8 9 10 11 12 |
<script> var MyBehavior = { ready: function() { console.log('DOM is ready'); } }; Polymer({ is: 'my-element', behaviors: [MyBehavior] }); </script> |
見ての通り、behaviors
にはオブジェクトを配列で指定することが可能です。複数指定されてハンドラが重複する(例えば、複数のオブジェクトでready
が定義されている)場合は、配列上で後から出現するオブジェクトが優先されます(つまり右辺が優先されます)。
カスタム要素のデフォルト属性
カスタム要素のデフォルト属性は、引数のhostAttributes
に定義します。hostAttributes
に指定した属性とその値は、カスタム要素の属性のデフォルト値になります。
1 2 3 4 5 6 7 8 |
<script> var MyElement = Polymer({ is: 'my-element', hostAttributes: { role: 'navigation' } }); </script> |
この時 <my-element> は、デフォルトで <my-element role="navigation"></my-element> として評価されます。
カスタム要素のプロパティ
カスタム要素のプロパティは、引数のproperties
に定義します。プロパティ名をキーに、type
やvalue
などの属性を渡すことでプロパティの特徴を定義します。簡略化された形としては、propertyName: type
という指定も可能です
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<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> |
ここではfoo
とbar
の2つをmy-element
のプロパティとして定義していますが、それぞれobserver
とcomputed
という属性を渡しています。
オブザーバー関数を指定するobserver
observer
はプロパティの変更を監視するオブザーバー関数を指定する属性です。ここではfooChanged
という関数を定義しているので、関数名を文字列で渡しています。v0.5まではpropertyNameChanged
といったような命名規則を用いてオブザーバー関数を定義していましたが、v1.1においてその機能はありません。監視する関数には引数として、変更後の値(newValue
)と変更前の値(oldValue
)が引数と渡されるので、前後値を利用することができます。
コンピュート関数を指定するcomputed
computed
はプロパティの値を動的に算出するための関数を指定する属性です。observer
同様に関数名を文字列で渡しますが、ここではbar
プロパティに対しcomputeBar(foo)
という値を渡しています。この記述によってcomputeBar
関数にはfoo
プロパティが引数として渡され、bar
の値は関数が返却する値を参照します。
HTML属性に反映するかどうかを指定するreflectToAttribute
プロパティの値に変更があっても通常はHTML属性に反映されませんが、reflectToAttribute
にtrue
を指定するとHTMLの属性も更新されるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<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
プロパティにイベントとそのハンドラをハッシュ形式で定義が可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<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つのイベントをサポートしています。
down
: マウスや指によって要素が押下された時up
: マウスや指が押下後に離された時tap
:down
とup
が連続して発生した時track
: マウスや指が押下されたまま移動した時
それぞれのイベントのハンドラに渡されるイベント引数のdetail
プロパティには様々な付加情報が渡されます。詳しくは公式ドキュメントを確認してください。
カスタムイベントの発火
fire
という関数を実行することで、ホスト要素を呼び出し元オブジェクトとしてカスタムイベントを発行することが出来ます。引数にはカスタムイベント名と、ハンドラのdetail
プロパティに渡すデータを指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<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まで有効だった演算子などはサポートされていません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<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>のプロパティとして定義しているfoo
やbar
を、 <input> のvalue
にバインディングしています。これによってfoo
やbar
の値に変更があると、<input>に自動で反映されます。
カーリーブラケット{{}}
とスクエアブラケット[[]]
カーリーブラケット{{}}
とスクエアブラケット[[]]
の差は、データバインディングの方向が一方向に制限されるかどうかの違いです。{{}}
のデータバインディングは双方向か一方向かが記述によって変わりますが、[[]]
は一方向のみであり参照しているプロパティを受け取るのみです。
カーリーブラケット{{}}
で指定した際にバインディングの振る舞いを決定するのは、プロパティのnotify
フラグとreadOnly
フラグです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<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>に埋め込んでいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<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}}"> になります。これによって、ネイティブ要素のプロパティからカスタム要素へのプロパティへのデータバインディングが行われます。
属性とのデータバインディング
属性に対してデータをバインディングするには以下のように$=
を使い、ブラケット{{}}
にはこれまでと同様にカスタム要素のプロパティやコンピュート関数を記述します。
1 2 3 4 5 6 7 8 9 10 11 |
<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)
が動的に実行されます。
ネイティブの属性であるclass
やstyle
には、次のように$=
でバインディングすることが可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!-- 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"
を使って配列を展開しているサンプルです(公式より引用)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<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
の他にはpop
、splice
、shift
、unshift
も同様です。これらの関数を使わないと、配列への変更が検知されずに再描画されません。
Polymerのデータバインディングの更なる詳細についてはData binding – Polymer 1.0を参照してください。
Polymer内部で行うDOM操作
<dom-module>内部のDOMの操作を行う場合は、Shadow RootのDOM APIではなく専用のインターフェースが用意されています。
IDが与えられている要素の参照
要素に対してIDが与えられていると、this.$.id
という形で自動的に参照が生成されます。これはv0.5まで存在していた機能なので、馴染みのある人もいるかと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<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(parent).childNodes
Polymer.dom(node).parentNode
Polymer.dom(node).firstChild
Polymer.dom(node).lastChild
Polymer.dom(node).firstElementChild
Polymer.dom(node).lastElementChild
Polymer.dom(node).previousSibling
Polymer.dom(node).nextSibling
Polymer.dom(node).textContent
Polymer.dom(node).innerHTML
クエリ
Polymer.dom(parent).querySelector(selector)
Polymer.dom(parent).querySelectorAll(selector)
コンテンツの参照
Polymer.dom(contentElement).getDistributedNodes()
Polymer.dom(node).getDestinationInsertionPoints()
属性の操作
Polymer.dom(node).setAttribute(attribute, value)
Polymer.dom(node).removeAttribute(attribute)
Polymer.dom(node).classList
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 のように評価されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<dom-module id="my-element"> <template> <style> :host { display: block; border: 1px solid red; } .foo > ::content .bar { background: orange; } </style> <div class="foo"><content></content></div> </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の各セッションはすべて以下のチャンネルで配信されていますので、興味のある方は是非チェックしてみてください。