はじめに
Polymer 1.0がリリースされました。WebComponentsの普及のためにPolymerには頑張ってもらわないといけませんが、そのためには現在Webで大きなシェアを占めるフレームワーク、AngularJSとの共存が不可欠です。その共存の理想と現実、未来について個人的な考えをまとめます。
そもそも対立構造ではない
何故か一部のプログラマの中では「AngularJS vs Polymer」になっているような空気を時々感じますが、本来PolymerはAngularJSと競合する技術スタックではありません。
Polymer、というよりWebComponents(特にCustom Element)はAngularJSやその他のMVCフレームワークよりも低いレイヤーのWeb技術で、むしろ共存できなければならないものです。
Polymer+Angularのあるべき姿
Polymerが提供するのはCustom Elements(独自タグ)の定義を簡単にするAPIです。PolymerのAPIを利用して定義された独自タグはWebComponents仕様に準拠しているがゆえに、同じWebComponents技術を利用するレイヤー上では再利用可能です。この再利用性を高めるのにWebComponentsのHTML Imports技術が大きく寄与しています。
<!DOCTYPE html>
<html>
<head>
<script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<link rel="import" href="dom-element.html">
</head>
<body>
<dom-element></dom-element>
</body>
</html>
WebComponentsのAPIにより新たに定義されたタグは、アプリケーション側から見た時にHTML5標準のタグと何か差があるかというと、無い、あってはならないというのが理想です。
ここでAngularが<dom-element>
にバインディングを行うのと、<div>
にバインディングを行うのに、なにか違いがあってはならないのです。
Polymer+Angularの現在
とはいえ、未だPolymerとAngularの世界は重なっておらず、ng-model
はPolymerの世界には通用しません。
次のPolymerElement(Polymerにより定義されたCustom Element)<input-binding>
はinput要素のvalueをバインドして表示するだけの要素です
<link rel="import" href="../bower_components/polymer/polymer.html"/>
<link rel="import" href="../bower_components/paper-input/paper-input.html"/>
<link rel="import" href="../bower_components/iron-input/iron-input.html"/>
<dom-module id="input-binding">
<template>
<paper-input-container>
<input is="iron-input" bind-value="{{text}}"
placeholder="text">
</paper-input-container>
<strong>{{text}}</strong>
</template>
</dom-module>
<script>
Polymer({
is: "input-binding",
properties: {
text: {
type: String,
notify: true
}
}
});
</script>
PolymerElementのテンプレート内部でinput要素のvalue
にバインドするにはbind-value
要素にプロパティをセットします。PolymerElementが持つプロパティはproperties
で定義され、text
プロパティはString
型で、外部に変更通知をnotify
することが記述されています。
一方、AngularJSでinputの値をバインドするにはng-model
を使うのが一般的です。
<input style="width: 100%" type="text" ng-model="text"/>
<p>{{ text }}</p>
ここで、<input-binding>
のtext
をAngularでバインドし、<p>
に表示したいときどうしたらいいでしょうか?
<input-binding text="{{ text }}"></input-binding>
<p>{{ text }}</p>
一見双方向バインディングされているように見えますが、PolymerElementに対するフックがAngularJSには(当然ながら)定義されていないので、text
属性が<input-binding>
の内部から書き換えられたとしてもそのイベントは{{text}}
に伝播することはありません。
ですが、PolymerElementが何も情報を発していないわけではなく、PolymerはPolymer語で、
「text
プロパティが変更されたらtext-changed
イベントを発火する」という仕事をしています。
必要なのはPolymer語のイベントを聞き取り、Angular語に翻訳する「通訳」です。
Directiveを使って通訳する
一つの解決策として、Polymer語を翻訳するDirectiveを定義してみました。polymer-attr
属性をPolymerElementに付与することで、内部の変更をAngularのScopeに反映します。
app.directive("polymerAttr", function () {
return {
restrict: 'A',
link: function (scope, element, attr) {
var eventName = attr.polymerAttr + '-changed';
element.on(eventName, function (event) {
scope[attr.polymerAttr] = event.detail.value;
scope.$apply();
});
}
};
});
これは<property>-changed
に関してはすべてのPolymerElementに適用できる汎用のDirectiveです。
これを付与し、text
プロパティの変更をAngular側で受け取ります。
<input-binding text="{{ text }}" polymer-attr="text"></input-binding>
<p>{{ text }}</p>
これによってアプリケーション開発者はpolymer-attr
を使うだけで、PolymerとAngularの垣根なく双方向バインディングを行うことが可能になりました。
Polymer+Angularの今後
今回挙げた「通訳」の例はたったひとつのイベントに対する解決策であり、他にもいろいろなDSLに対する通訳が必要ですが、それでもこれだけの記述でPolymerとAngularの世界をつなぐことができることがわかりました。
Polymerは本質的にはWebComponentsのpolyfillであり、糖衣構文です。Polymerは1.0になりましたが、そのAPIの使いやすさは正直なところそれほど高くないです。しかし競争相手となるライブラリがMozillaのX-Tagくらいしかなく、X-TagはPolymer以上に使いにくいAPIなので困ったものです。
ですが、この状態は本来WebComponentsと親和性の高いES6のclass記法が使えるようになれば大きく変わることが期待できます。
WebComponentsが普及した先では、再利用可能なタグを開発し配布するコンポーネントエンジニアと、それを利用するアプリケーションエンジニアに二分され、後者はそれがHTML5のタグなのか自分でインポートしたタグなのか気にすることなく自由にアプリケーションを開発できるようになります。この構図は自分がインストールしたjQueryプラグインがjQuery標準のAPIと同じように扱えるのと全く同じです。そして、AngularJSが今ではjQueryと共存できているように、WebComponentsもいずれはもっと自然にAngularと共存できるようになるはずです。具体的には、Angular2のコンポーネントシステムがWebComponentsに準拠することでPolymerとAPIを共通化できるのではないかと考えています。
まとめ
WebComponents技術は今後必ずWebアプリケーション開発の中で重要なパーツになります。その取っ掛かりとしてPolymerに今から触れておくのは決して無駄な投資にはならないでしょう。
「Angularを使っているから」と避けていた方は1.0リリースをきっかけに手を出してみてはどうでしょうか?