@laco0416です。PolymerとAngularJSで一方向データバインディングによるコンポーネントツリーを作り、コンポーネント間で状態を同期してみました。
完成品
タブを表示するコンポーネントとドロップダウンメニューのコンポーネントは別の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>
何がしたかったのか
- コンポーネントをステートレスにしたい
- 状態を管理するオブジェクトをコンポーネントツリーの頂点に置きたい
一方向のデータバインディングを重ねて、親から子に状態を同期していき、アクションによりコントローラの状態が書き換わり、またそれが伝播するというモデルです
なんちゃって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)
がクソ便利。