Google IO 2014 で紹介されていた Web Components の実装の Polymer とAngularJSと組み合わせてみた。
Yeoman の AngularJS プロジェクトへの Polymer の組み込む
1: Yeomn の AngularJS のプロジェクトを作成する。
yo angular
2: Bower で Polymer の Paper Element をインストールする
bower install --save Polymer/platform
bower install --save Polymer/paper-elements
3: View に Polymer の Paper Element を組み込む
<link rel="import" href="./bower_components/core-header-panel/core-header-panel.html">
<link rel="import" href="./bower_components/core-toolbar/core-toolbar.html">
<link rel="import" href="./bower_components/paper-tabs/paper-tabs.html">
<core-drawer-panel id="drawerPanel">
<core-header-panel>
<core-toolbar>
<paper-tabs flex selected="all" valueattr="name" self-end >
<paper-tab
ng-repeat="awesomeThing in awesomeThings"
name="{{awesomeThing}}" ng-bind="awesomeThing">
</paper-tab>
</paper-tabs>
</core-toolbar>
</core-header-panel>
</core-drawer-panel>
4: CSS を Paper Element 向けに変更する
html,body {
height: 100%;
margin: 0;
}
.container{
height:100%;
width:100%;
}
core-header-panel {
height: 100%;
}
core-toolbar {
background: #03a9f4;
color: white;
}
5: Grunt で Server と ブラウザを起動する
grunt serve
ブラウザ上に Paper Element の画面が表示された。
ただ、いくつか残点なところが見つかった。
問題1:画面がちらつく
画面を表示すると、Web Componentsのテンプレートが適用される前の画面が一瞬見える。
回避方法: bodyにunresolvedを追加する
<body ng-app="helloApp" unresolved>
(shuheiさん,ありがとうございました)
http://www.polymer-project.org/docs/polymer/styling.html#fouc-prevention
ng-cloakと同じ機能が Polymer にもあった。
問題2: ng-model が使えない
Paper Elements には、<paper-input> などの入力部品が用意されているが、AngularJS の ng-model が使えないようだ。
ng-model の実装を見ると、<input> エレメントのディレクティブに実装されていたので、<paper-input>では、動かない。
回避方法: ディレクティブを作る
<input> エレメントのディレクティブを参考に、<paper-input>のディレクティブを作成することで、ng-modelに対応させることができた。
1: 画面に<paper-input>を追加する
<link rel="import" href="./bower_components/core-header-panel/core-header-panel.html">
<link rel="import" href="./bower_components/core-toolbar/core-toolbar.html">
<link rel="import" href="./bower_components/paper-tabs/paper-tabs.html">
<link rel="import" href="./bower_components/paper-input/paper-input.html">
<core-drawer-panel id="drawerPanel" class="ng-hide" ng-show="init">
<core-header-panel>
<core-toolbar>
<paper-tabs id="tabs" flex selected="all" valueattr="name" self-end >
<paper-tab
ng-repeat="awesomeThing in awesomeThings"
name="{{awesomeThing}}" ng-bind="awesomeThing">
</paper-tab>
</paper-tabs>
</core-toolbar>
<paper-input ng-model="text" ></paper-input>
<paper-input ng-model="text" ></paper-input>
</core-header-panel>
</core-drawer-panel>
2: ディレクティブ作成の準備
Yeomanのコマンド実行し、jsファイルのひな形と app/index.html の修正を行う。
yo angular:directive paper-input
3: ng-model の ディレクティブを作成する
AngularJSの<input>のディレクティブから、ngModelの実装を取り出し、<paper-input>にngModelを適用する。
'use strict';
angular.module('helloApp')
.directive('paperInput', function ($parse,$timeout,$browser) {
return {
restrict: 'E',
require:'?ngModel',
link: function postLink(scope, element, attrs) {
var input = element[0];
// ngModelが設定されていない場合には動かさない
if(attrs.ngModel){
// ngModel の参照値 と inputValueを双方向バインディングする
bindNgModel('ngModel','inputValue');
}
function bindNgModel(attrName,inputName){
var ngModelGet = $parse(attrs[attrName]);
toInput(ngModelGet,attrs[attrName],inputName);
toModel(ngModelGet,attrs[attrName],inputName);
}
// ngModelの値をinputValueに変更を反映する
function toInput(ngModelGet,attrName,inputName){
// placeholderが表示されてしまうのを防ぐ
$timeout(function(){
input[inputName] = ngModelGet(scope);
},350);
var first = true;
// ngModelの値を監視し、変更があった場合には、
// <paper-input>に値を反映する
scope.$watch(attrName,function ngModelWatch() {
// placeholderが表示されてしまうのを防ぐ
if(first){
first = false;
return;
}
var value = ngModelGet(scope);
input[inputName] = value;
});
}
// <paper-input> の変更をngModelに反映する
function toModel(modelGet){
var ngModelSet = modelGet.assign;
var timeout;
var deferListener = function(ev) {
if (!timeout) {
timeout = $browser.defer(function() {
listener(ev);
timeout = null;
});
}
};
var listener = function(event){
ngModelSet(scope, input.inputValue);
scope.$apply();
}
// <paper-input> の値の変更を監視する
input.addEventListener('change',function(event){
deferListener(event);
});
// <paper-input> のキーイベントを監視する
input.addEventListener('keydown',function(event){
var key = event.keyCode;
// ignore
// command modifiers arrows
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
deferListener(event);
});
}
}
};
});
<paper-input>の初期化中に、inputValueに値を設定すると、placeholderが表示されたままになるバグがあるようだ。これも、inputValueの値の設定を少し遅らせることで、回避することができた。
問題3: Polymerのカスタムメソッドの登録にonメソッドを使わなければならない
<paper-tabs> には、選択されたタブが変更されると呼び出される、core-selectイベントがあるが、on メソッドを使ってイベントを登録しなければならない。
var tabs = document.getElementById('tabs')
angular.element(tabs).on('core-select',function(){
// メソッドの中身・・・
});
AngularJSでのイベント登録は、ng-clickなどのようにHTMLに書くように統一したい。
回避方法: ディレクティブを作る
ng-clickを参考にして、core-selectのディレクティブを作成することで、HTMLでイベントを登録できるようになる。
1: ng-core-selectイベントを<paper-tabs>に追加する
<link rel="import" href="./bower_components/core-header-panel/core-header-panel.html">
<link rel="import" href="./bower_components/core-toolbar/core-toolbar.html">
<link rel="import" href="./bower_components/paper-tabs/paper-tabs.html">
<link rel="import" href="./bower_components/paper-input/paper-input.html">
<core-drawer-panel id="drawerPanel" ng-cloak>
<core-header-panel>
<core-toolbar>
<paper-tabs flex selected="all" valueattr="name" self-end
ng-core-select="select($event)">
<paper-tab
ng-repeat="awesomeThing in awesomeThings"
name="{{awesomeThing}}" ng-bind="awesomeThing">
</paper-tab>
</paper-tabs>
</core-toolbar>
<paper-input ng-model="text" ></paper-input>
<paper-input ng-model="text" ></paper-input>
</core-header-panel>
</core-drawer-panel>
2: ng-core-selectイベントを関数(select)をスコープに追加する
'use strict';
angular.module('helloApp')
.controller('MainCtrl', function ($scope,$timeout) {
var self = this;
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
$scope.text = 'ModelInput';
$scope.select = function($event){
console.log('select',$event);
}
});
3: ng-core-select ディレクティブを追加する
YemoanでDirectiveのひな形を生成する。
yo angular:directive ng-core-select
ng-clickのディレクティブを参考に、ng-core-selectに登録された関数をcore-selectイベントから呼び出す。
'use strict';
angular.module('helloApp')
.directive('ngCoreSelect', function ($parse) {
return {
restrict: 'A',
link: function postLink(scope, element, attrs) {
if(attrs.ngCoreSelect){
var clickHandler = $parse(attrs.ngCoreSelect)
// 選択されているItemを保持する
var selectedItem = null;
element.on('core-select',function(event){
// Itemの変更がない場合には処理を終了する
if(selectedItem === event.detail.item){
return;
}
selectedItem = event.detail.item;
// ng-core-selectに登録されている関数を呼び出す
scope.$apply(function() {
clickHandler(scope, {$event: (event)});
});
});
}
}
};
});
これで<paper-tab>をクリックしたタイミングでng-core-selectの関数が呼び出される。