JavaScript
AngularJS
Polymer

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

More than 3 years have passed since last update.

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


完成品

https://github.com/laco0416/polymer-ng-oneway

ng-polymer-oneway.gif

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

<!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

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


実装


コントローラ

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


  • 状態を持つ

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

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

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つです


  • 親の状態をビューに反映する

  • アクションによりイベントを発火する

<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>と全く同じです。

<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です。ここは各コンポーネントにコントローラの変数の値を一方向データバインディングで渡しているだけです。

<!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)がクソ便利。