Help us understand the problem. What is going on with this article?

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

More than 5 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)がクソ便利。
lacolaco
I play Angular and pray for Angular. No Breaking Changes No Life.
https://lacolaco.net
classi
学校の先生・生徒・保護者向けのB2B2Cの学習支援Webサービス「Classi(クラッシー)」 を開発・運営している会社です。
https://classi.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away