JavaScript
HTML5
AngularJS
WebComponents
Polymer

Polymer (WebComponents) と AngularJS の組み合わせ

More than 3 years have passed since last update.

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 を組み込む


app/views/main.html

<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 向けに変更する


app/styles/main.css

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を追加する


app/index.html

  <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>を追加する


app/views/main.html

<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を適用する。


app/scripts/directives/paper-input.js


'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>に追加する


app/views/main.html

<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)をスコープに追加する


app/scripts/controllers/main.js

'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イベントから呼び出す。


app/directives/ng-core-select.js

'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の関数が呼び出される。