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


Google IO 2014 で紹介されていた Web Components の実装の Polymer とAngularJSと組み合わせてみた。

## Yeoman の AngularJS プロジェクトへの Polymer の組み込む
1: Yeomn の AngularJS のプロジェクトを作成する。

```bash:
yo angular 
```

2: Bower で Polymer の Paper Element をインストールする

```bash:
bower install --save Polymer/platform
bower install --save Polymer/paper-elements
```

3: View に Polymer の Paper Element を組み込む

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

```css: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 と ブラウザを起動する

```bash:
grunt serve
```

ブラウザ上に Paper Element の画面が表示された。

#### ただ、いくつか残点なところが見つかった。

## 問題1:画面がちらつく
画面を表示すると、Web Componentsのテンプレートが適用される前の画面が一瞬見える。

### 回避方法: bodyにunresolvedを追加する

```html: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 には、&lt;paper-input&gt; などの入力部品が用意されているが、AngularJS の ng-model が使えないようだ。
ng-model の実装を見ると、&lt;input&gt; エレメントのディレクティブに実装されていたので、&lt;paper-input&gt;では、動かない。

### 回避方法: ディレクティブを作る
&lt;input&gt; エレメントのディレクティブを参考に、&lt;paper-input&gt;のディレクティブを作成することで、ng-modelに対応させることができた。
#### 1: 画面に&lt;paper-input&gt;を追加する

```html: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 の修正を行う。

```bash:
yo angular:directive paper-input
```

#### 3: ng-model の ディレクティブを作成する
AngularJSの&lt;input&gt;のディレクティブから、ngModelの実装を取り出し、&lt;paper-input&gt;にngModelを適用する。

```js: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);
  });

}
      }
    };
  });

```
&lt;paper-input&gt;の初期化中に、inputValueに値を設定すると、placeholderが表示されたままになるバグがあるようだ。これも、inputValueの値の設定を少し遅らせることで、回避することができた。

## 問題3: Polymerのカスタムメソッドの登録にonメソッドを使わなければならない
&lt;paper-tabs&gt; には、選択されたタブが変更されると呼び出される、core-selectイベントがあるが、on メソッドを使ってイベントを登録しなければならない。

```js:
 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イベントを&lt;paper-tabs&gt;に追加する

```html: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)をスコープに追加する
```js: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のひな形を生成する。

```bash:
yo angular:directive ng-core-select
```

ng-clickのディレクティブを参考に、ng-core-selectに登録された関数をcore-selectイベントから呼び出す。

```js: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)});
            });
          });
        }
      }
    };
  });
```
これで&lt;paper-tab&gt;をクリックしたタイミングでng-core-selectの関数が呼び出される。