13
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-09-04

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

完成品

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)がクソ便利。
13
15
0

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
13
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?