PolymerとAngularJSでコンポーネント間の状態を同期する


@laco0416です。PolymerとAngularJSで一方向データバインディングによるコンポーネントツリーを作り、コンポーネント間で状態を同期してみました。

# 完成品
https://github.com/laco0416/polymer-ng-oneway

![ng-polymer-oneway.gif](https://qiita-image-store.s3.amazonaws.com/0/20253/fcfa073b-8e06-1751-b02a-0fee49af468f.gif "ng-polymer-oneway.gif")

タブを表示するコンポーネントとドロップダウンメニューのコンポーネントは別のPolymerElementです。

```html
<!DOCTYPE html>
<html ng-app="app">
<head>
  <meta charset="UTF-8">
  <script src="bower_components/webcomponentsjs/webcomponents.js"></script>
  <script src="bower_components/angular/angular.js"></script>
  <link rel="import" href="components/tab-header.html"/>
  <link rel="import" href="components/tab-selector.html"/>
</head>
<body ng-controller="AppCtrl as ctrl">
  <tab-header tab-index="{{ ctrl.tabIndex }}"></tab-header>
  <tab-selector tab-index="{{ ctrl.tabIndex }}"></tab-selector>
  <script src="index.js"></script>
</body>
</html>
```

# 何がしたかったのか
* コンポーネントをステートレスにしたい
* 状態を管理するオブジェクトをコンポーネントツリーの頂点に置きたい

一方向のデータバインディングを重ねて、親から子に状態を同期していき、アクションによりコントローラの状態が書き換わり、またそれが伝播するというモデルです

![kobito.1441405558.964470.png](https://qiita-image-store.s3.amazonaws.com/0/20253/a58353b9-7a38-a028-24cc-dfbc42246f2e.png "kobito.1441405558.964470.png")

なんちゃってFluxみたいなモデルですね


# 実装
## コントローラ
コントローラがやることは2つだけです

* 状態を持つ
* イベントを受け取り状態を変化させる

TypeScriptで書いてますがclass使ってるだけの普通のAngularJSです

```ts
angular.module("app", []);

class AppController {
  public tabIndex: number;

  constructor(public $scope: angular.IScope) {
    document.addEventListener("header-tab-selected", (e: CustomEvent) => {
      this.setTabIndex(e.detail);
      this.$scope.$applyAsync();
    });
    document.addEventListener("selector-tab-selected", (e: CustomEvent) => {
      this.setTabIndex(e.detail);
      this.$scope.$applyAsync();
    });
    this.tabIndex = 0;
  }

  setTabIndex(index: number) {
    if (index === this.tabIndex) return;
    this.tabIndex = index;
  }
}

angular.module("app").controller("AppCtrl", AppController);
```

`"header-tab-selected"`と`"selector-tab-selected"`は後述のコンポーネント内で発火するイベントです。

## `<tab-header>`
選択可能なタブを表示するコンポーネントです。このコンポーネントがやることは2つです

* 親の状態をビューに反映する
* アクションによりイベントを発火する

```html
<link rel="import" href="../bower_components/polymer/polymer.html"/>
<link rel="import" href="../bower_components/paper-tabs/paper-tabs.html"/>
<link rel="import" href="../bower_components/paper-tabs/paper-tab.html"/>

<dom-module id="tab-header">
  <template>
    <style>
      paper-tab {
        background: #001388;
        color: #fff;
      }
    </style>
    <paper-tabs id="tabs" selected="[[tabIndex]]">
      <paper-tab>1</paper-tab>
      <paper-tab>2</paper-tab>
      <paper-tab>3</paper-tab>
      <paper-tab>4</paper-tab>
      <paper-tab>5</paper-tab>
    </paper-tabs>
  </template>
</dom-module>
<script>
  Polymer({
    is: "tab-header",
    properties: {
      tabIndex: {
        type: Number,
        value: 0
      }
    },
    ready: function () {
      this.listen(this.$.tabs, "selected-changed", "_selectTab")
    },
    _selectTab: function (e) {
      this.fire("header-tab-selected", this.$.tabs.selected);
    }
  });
</script>
```

`<paper-tabs id="tabs" selected="[[tabIndex]]">`の部分が、一方向のデータバインディングです。Polymerのテンプレート内では`{{}}`が双方向、`[[]]`が一方向のデータバインディング記法です。

```
    ready: function () {
      this.listen(this.$.tabs, "selected-changed", "_selectTab")
    },
    _selectTab: function (e) {
      this.fire("header-tab-selected", this.$.tabs.selected);
    }
```

paper-tabsの`selected-changed`イベントをlistenしてハンドラで`header-tab-selected`イベントを発火しています。

## `<tab-selector>`
見た目が違うだけで役割は`<tab-header>`と全く同じです。

```html
<link rel="import" href="../bower_components/polymer/polymer.html"/>
<link rel="import" href="../bower_components/paper-dropdown-menu/paper-dropdown-menu.html"/>
<link rel="import" href="../bower_components/paper-menu/paper-menu.html"/>
<link rel="import" href="../bower_components/paper-item/paper-item.html"/>

<dom-module id="tab-selector">
  <template>
    <style>
      paper-tab {
        background: #001388;
        color: #fff;
      }
    </style>
    <paper-dropdown-menu label="Tab selector">
      <paper-menu id="menu" selected="[[tabIndex]]" class="dropdown-content">
        <paper-item>1</paper-item>
        <paper-item>2</paper-item>
        <paper-item>3</paper-item>
        <paper-item>4</paper-item>
        <paper-item>5</paper-item>
      </paper-menu>
    </paper-dropdown-menu>
  </template>
</dom-module>
<script>
  Polymer({
    is: "tab-selector",
    properties: {
      tabIndex: {
        type: Number,
        value: 0
      }
    },
    ready: function () {
      this.listen(this.$.menu, "selected-changed", "_selectTab")
    },
    _selectTab: function (e) {
      this.fire("selector-tab-selected", this.$.menu.selected);
    }
  });
</script>
```

## index.html
最後にエントリポイントのHTMLです。ここは各コンポーネントにコントローラの変数の値を一方向データバインディングで渡しているだけです。

```html
<!DOCTYPE html>
<html ng-app="app">
<head>
  <meta charset="UTF-8">
  <title></title>
  <base href="/"/>
  <script src="bower_components/webcomponentsjs/webcomponents.js"></script>
  <script src="bower_components/angular/angular.js"></script>
  <link rel="import" href="components/tab-header.html"/>
  <link rel="import" href="components/tab-selector.html"/>
</head>
<body ng-controller="AppCtrl as ctrl">
  <tab-header tab-index="{{ ctrl.tabIndex }}"></tab-header>
  <tab-selector tab-index="{{ ctrl.tabIndex }}"></tab-selector>
  <script src="index.js"></script>
</body>
</html>
```

# 所感
* ステートレスなコンポーネントのほうがやっぱり作りやすい気がする
* 一方向データバインディングの記法をちゃんと用意してるPolymerえらい
* コンポーネントツリーはAngular2と全く同じ思想なので今からこういうステート管理に慣れておきたい

# 学び
* Polymerの`listen(node, eventName, callbackName)`がクソ便利。