LoginSignup
78
78

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-06-29

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

78
78
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
78
78