自由課題

学んだり、考えたり、試したりしたこと。

Polymerのデータバインディングについて(公式ドキュメント日本語訳)

はじめに

最近気になっていたWeb Componentsを本格的に触り始めようかということで、Web Componentsの機能をフル活用したライブラリであるPolymerの公式ドキュメントを斜め読みしていたところ、データバインディングについて記載があったので訳してみました。

個人的には、Model-View間でのデータバインディング機能がまともにない環境では効率的にアプリを作成することは難しいと思っているので、結構肝のところかなと思います。なお、訳したのはデータバインディングのところだけです。悪しからず。

訳してみた感想としては、(Web標準なので当たり前かもしれないですが)ライブラリ特有の癖がなく、一度学習したらすんなり使えそうな印象を受けました。今度時間をとって実際に試してみようと思います。

なお、本ドキュメントのライセンスは原文と同じくCC BY 3.0であり、原文はPolymer Authorsにより記述され、原文そのものはここにあります。

データバインディング概要

Polymerは双方向のデータバインディングをサポートしています。データバインディングは、アプリケーションのUI(DOM)とその根底にあるデータ(モデル)との間のしっかりとした分離をサポートするためにHTMLとDOM API群を拡張します。モデルに対する更新はDOMに反映され、DOMへのユーザ入力は即座にモデルに伝達されます。

Polymerの要素においては、モデルは常に要素そのものです。 例えば、単純な要素を考えてみましょう:

    <polymer-element name="name-tag">
      <template>
        This is <b>{{owner}}</b>'s name-tag element.
      </template>
      <script>
        Polymer('name-tag', {
          //要素のモデルを初期化します
          ready: function() {
            this.owner = 'Rafael';
          }
        });
    </polymer-element>

ここにあるowner属性はname-tag要素に対応するモデルです。もしowner属性を更新すると:

document.querySelector('name-tag').owner = 'June';

タグの内容が変更されます。 これはJuneのname-tag要素です。

<template> 要素

HTMLテンプレート要素は複製して再利用することが可能な不活性(inert)なHTMLの断片を定義することを可能にします。<template>要素のの内容はブラウザでレンダリングされず, querySelectorで抽出することができないという意味で隠された要素であり、ロードされるためのリソースや実行するためのスクリプトを必要としないという意味で不活性です。

Polymerでは、テンプレートには2つの特別な目的があります:

  • Polymerの要素宣言では最初(トップレベル)の<template>要素がカスタム要素のshadow DOMを定義するために使用されます。

  • Polymerの要素内部では、動的コンテンツをレンダリングするためにデータバインディングとテンプレートを使用することができます。

注 `<template>`要素はHTML標準の中で新しい要素です。テンプレートをPolymerの外部で使用する際にはHTML5Rocksの[HTML's New  Template Tag](http://www.html5rocks.com/tutorials/webcomponents/template/)を参照してください。

データバインディングとテンプレートの連携

テンプレートはそれそのものでも有用です。Polymerは宣言的で、双方向のデータバインディングをテンプレートに追加しています。データバインディングはテンプレートのデータモデルとしてオブジェクトを割り当て、バインド(結びつける)させることを可能にします。バインドされたテンプレートは以下のことを可能にします:

これがどう動作するかを確認するために、データバインディングを用いたPolymer要素の例を示します:

    <polymer-element name="greeting-tag">
      <!-- 一番外側のテンプレートが要素のshadow DOMを定義します -->
      <template>
        <ul>
          <template repeat="{{s in salutations}}">
            <li>{{s.what}}: <input type="text" value="{{s.who}}"></li>
          </template>
        </ul>
      </template>
      <script>
        Polymer('greeting-tag', {
          ready: function() {
            // 要素のデータモデルを初期化します
            // (挨拶の言葉の配列)
            this.salutations = [
              {what: 'Hello', who: 'World'},
              {what: 'GoodBye', who: 'DOM APIs'},
              {what: 'Hello', who: 'Declarative'},
              {what: 'GoodBye', who: 'Imperative'}
            ];
          }
        });
      </script>
    </polymer-element>

通常どおり、'要素の定義'(訳注:この訳の範囲外)で見たようにこのカスタム要素は要素のshadow DOMを定義するための外側の<template>要素を内包します。

そのテンプレート内部に、二重の中括弧{{ }}に囲まれた式を含んだ2番目のテンプレートがあります。

    <template id="greeting" repeat="{{s in salutations }}">
      <li>{{ s.what }}: <input type="text" value="{{ s.who }}"></li>
    </template>

このテンプレートでは何が起こっているのでしょうか?

  • repeat="{{ s in salutations }}"はテンプレートにsalutations配列の各要素に対応するDOMの断片(かインスタンス)を生成するよう指示します。

  • テンプレートのコンテンツは各インスタンスがどんなものであるかを定義します。この場合、インスタンスはテキストノードを持つ<li>と、この子要素として<input>を含みます。

  • {{ s.what }}{{ s.who }}という式はsalutations配列のオブジェクトに対するデータバインディングを生成します。

{{ }}の内部の値はPolymer式です。この節の例では、式は(salutationsのような)JavaScriptオブジェクトでも(salutations.whoのような)パスでもかまいません。(式はリテラルといくつかの演算子を含むことができます--式(訳注:この訳の範囲外)を参照してください)

<greeting-tag>要素の生成時には、salutations配列は以下のように初期化されます:

this.salutations = [
  { what: 'Hello', who: 'World' },
  { what: 'Goodbye', who: 'DOM APIs' },
  { what: 'Hello', who: 'Declarative' },
  { what: 'Goodbye', who: 'Imperative' }
];

これは単なるJavaScriptのデータです: データを特別な監視可能な(observable)なオブジェクトにインポートすることは必要ありません。 this.salutations配列はテンプレートのモデルとして提供されます。

テンプレートはモデルを生成・変更した時に動作します。結果は以下です。

ScreenShot

そしてこの時DOMはこんなふうになっています:

ScreenShot

テンプレートがドキュメントのテンプレートの位置の直後に4つのインスタンスを即座に生成していることがわかります。

動的な、双方向データバインディング

サーバサイドのテンプレートとは異なり、Polymerデータバインディング動的です。もしモデル内の値を変更したら、DOMは変更を監視しており対応して更新します。以下のサンプルはモデルを更新するメソッドを追加しています。ボタンを押すと、DOMにモデルデータがすぐに反映されることを確認することができます。

<link rel="import" href="/components/polymer/polymer.html">

<polymer-element name="greeting-tag">
  <!-- 一番外側のテンプレートが要素のshadow DOMを定義します -->
  <template>
    <ul>
      <template repeat="{{s in salutations}}">
        <li>{{s.what}}: <input type="text" value="{{ s.who }}"></li>
      </template>
    </ul>
    <button on-click="{{updateModel}}">Update model</button>
  </template>
  <script src="greeting-tag.js"></script>
</polymer-element>

しかし、DOMは単にモデル内のデータを監視しているわけではありません。ユーザ入力を収集するDOM要素をバインドすると、それはモデルに収集した値をプッシュします。

ScreenShot

注 モデルデータが変更された時にカスタムされた振る舞いを発動するために'ウォッチャを変更し、ブロックを監視する`(訳注:この訳の範囲外)ことができます。

最後に、salutations配列に対して要素の追加・削除を行ったときに何が起こるかを確認してみましょう:

ScreenShot

repeat属性は配列の各要素に1つのインスタンスが存在することを保証します。salutationsの中の2つの要素を削除し、代わりに1つの要素を追加します。<template>は対応する2つのインスタンスを削除し、正しい位置に新しく1つを生成します。

想像はつきましたか? データバインディングはHTMLを用いて、データがどこに行くかについての情報を含むHTMLと、ドキュメントの構造を制御する構文を用いて、独自のHTMLを作成することを可能にします--全ては提供するデータ次第です。

イベントハンドリングとデータバインディング

データバインディングにより、'宣言的イベントマッピング'(on-_event_handlers)(訳注:この訳の範囲外)を用いて簡単にイベントハンドラを追加することができます。

    <template>
      <ul>
        <template repeat="{{s in stories}}">
          <li on-click={{selectStory}}>{{s.headline}}</li>
        </template>
      </ul>
    </template>

モデルデータを更新するためや、テンプレートによりレンダリングされていないデータの断片にアクセスするために、しばしばテンプレートインスタンスを生成するために用いられたモデルデータとイベントを特定したくなる時があるでしょう。

イベントのtarget.templateInstance.model 属性からモデルデータを取得することができます。テンプレート内部でアクセスできるどの識別子も.modelオブジェクトの属性として使用可能です。

例えば、selectStoryメソッドはこのような感じになるでしょう:

selectStory: function(e, detail, sender) {
  var story = e.target.templateInstance.model.s;
  console.log("Clicked " + story.headline);
  this.loadStory(story.id); // accessing non-rendered data from the model
}