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

Polymerドキュメント(日本語) Data system/Helper elements 〜ヘルパー要素〜

More than 3 years have passed since last update.

目次へ移動

翻訳ドキュメントの管理ページ

Polymerは、一般的なデータバインディングのユースケースに備えて各種カスタム要素を提供しています。:

  • テンプレートリピーター(dom-repeat):配列アイテムごとに、テンプレートのコンテンツでインスタンスを生成します。
  • 配列セレクタ:構造化されたデータの配列において選択状態を管理します。
  • 条件付きテンプレート(dom-if):指定された条件がtrueなら、そのコンテンツをスタンプします。
  • 自動バインディングテンプレート(dom-bind):Polymer要素外部でデータバインディングが利用できます。

2.0向けのヒント:データバインディングのヘルパー要素は、下位バージョンと互換性のあるpolymer.htmlをインポートする際にバンドルされています。レガシーなインポートを利用しない場合、あなたが使用したヘルパー要素を個別にインポートする必要があります。

テンプレートリピーター(dom-repeat)

テンプレートリピーターは、配列のバインドに特化したテンプレートです。配列内の各アイテムごとに、テンプレートのコンテンツのインスタンスを一つ生成します。各インスタンスは、次のプロパティを含む新たなデータバインディングのスコープを作成します。:

  • item:インスタンスの作成に使用された配列のアイテム
  • index:配列内のitemのインデックス(配列がソートまたはフィルタリングされると、indexの値は変化します)

テンプレートリピーターを利用するには二つの方法があります。:

  • Polymer要素内またはPolymerが管理する他のテンプレートの内では<template is="dom-repeat>という省略記法を使用してください。
  <template is="dom-repeat" items="{{items}}">
    ...
  </template>
  • Polymerが管理するテンプレートの外側では、ラッパー要素<dom-repeat>を使用します。
  <dom-repeat>
    <template>
      ...
    </template>
  </dom-repeat>

このフォームでは、通常、プロパティitemsを命令的に設定します。:

  var repeater = document.querySelector('dom-repeat');
  repeater.items = someArray;

Polymerが管理するテンプレートには、Polymer要素のテンプレートや、dom-binddom-ifdom-repeatに属するテンプレート、あるいはTemplatizerによって管理されるテンプレートが含まれます。

ほとんどのケースにおいて、dom-repeatには、一番目(省略形)のフォームを使用することになるでしょう。

テンプレートリピーターは後方互換性を確保するため、レガシーなインポート(polymer.html)によって取り込まれます。もしpolymer.htmlをインポートしない場合は、次のコードに示すようdom-repeat.htmlをインポートして下さい。

例:

<link rel="import" href="components/polymer/polymer-element.html">
<! -- import template repeater -->
<link rel="import" href="components/polymer/src/elements/dom-repeat.html">

<dom-module id="x-custom">
  <template>

    <div> Employee list: </div>
    <template is="dom-repeat" items="{{employees}}">
        <div># [[index]]</div>
        <div>First name: [[item.first]]</span></div>
        <div>Last name: [[item.last]]</span></div>
    </template>

  </template>

  <script>
    class XCustom extends Polymer.Element {

      static get is() { return 'x-custom'; }

      static get properties() {
        return {
          employees: {
            type: Array,
            value() {
              return [
                {first: 'Bob', last: 'Smith'},
                {first: 'Sally', last: 'Johnson'},
              ];
            }
          }
        }
      }

    }

    customElements.define(XCustom.is, XCustom);
  </script>

</dom-module>

itemのサブプロパティの変更通知は、テンプレートのインスタンスに転送され、一般的な変更通知イベントを使用して更新されます。配列itemsが双方向バインディングデリミタを使ってバインドされている場合、個々のアイテムの変更を上に向けて流すこともできます。

テンプレートリピーターが変更を反映するには、配列itemsを監視できるように更新する必要があります。例えば:

// Use Polymer array mutation methods:
this.push('employees', {first: 'Diana', last: 'Villiers'});

// Use Polymer set method:
this.set('employees.2.last', 'Maturin');

// Use native methods followed by notifyPath
this.employees.push({first: 'Barret', last: 'Bonden'});
this.notifyPath('employees');

詳細については、オブジェクトと配列を監視可能に変更するを参照してください。

dom-repeatテンプレート内のイベントの処理

dom-repeatテンプレートのインスタンスから生成されたイベントを処理する際、イベントが発生した要素と、アイテムを生成したモデルデータをマッピングしたいことが頻繁にあるかもしれません。

<dom-repeat>テンプレートの内部に宣言型イベントハンドラを追加すると、リピーターはリスナーに送られてきた各イベントにmodelプロパティを付加します。modelオブジェクトには、テンプレートのインスタンスを生成するのに使用したスコープデータが含まれており、アイテムのデータはmodel.itemになります。

<link rel="import" href="polymer/polymer-element.html">
<link rel="import" href="polymer/src/elements/dom-repeat.html">

<dom-module id="x-custom">

  <template>
    <template is="dom-repeat" id="menu" items="{{menuItems}}">
        <div>
          <span>{{item.name}}</span>
          <span>{{item.ordered}}</span>
          <button on-click="order">Order</button>
        </div>
    </template>
  </template>

  <script>
    class XCustom extends Polymer.Element {

      static get is() { return 'x-custom'; }

      static get properties() {
        return {
          menuItems: {
            type: Array,
            value() {
              return [
                {name: 'Pizza', ordered: 0},
                {name: 'Pasta', ordered: 0},
                {name: 'Toast', ordered: 0}
              ];
            }
          }
        }
      }

      order(e) {
        e.model.set('item.ordered', e.model.item.ordered+1);
      }
    }

    customElements.define(XCustom.is, XCustom);
  </script>

</dom-module>

modelTemplateInstanceのインスタンスであり、Polymerのデータ関連のAPI:getsetsetPropertiesnotifyPathに加えて、配列変更メソッドを持っています。テンプレートのインスタンスに関連したパスを用いることで、これらAPIをmodelの操作に利用できます。

例えば、上記のコードでは、ユーザーがピザの横にあるボタンをクリックすると、ハンドラは以下のコードを実行します。:

e.model.set('item.ordered', e.model.item.ordered+1);

これによって、itemの(この場合はピザの)注文数を増やします。

modelオブジェクトでは、バインドされたデータのみ利用可能です。dom-repeat内部で、実際にバインドされたプロパティだけがmodelオブジェクトに追加されます。そのため場合によっては、イベントハンドラからプロパティへアクセスが必要な場合、テンプレート内のプロパティにバインドする必要があるかもしれません。例えば、ハンドラがproductIdプロパティにアクセスする必要がある場合、単にそのプロパティを表示に影響を与えないプロパティにバインドします。

  <template is="dom-repeat" items="{{products}}" as="product">
    <div product-id="[[product.productId]]">[[product.name]]</div>
  </template>

dom-repeatテンプレートの外側におけるイベント処理

(addEventListenerを使って)命令的に登録されたリスナーや、特定のdom-repeatテンプレートの親ノードに設定されたリスナーに対して、modelプロパティが付加されることはありません。これらのケースでは、指定された要素から生成されたモデルデータを検索するためにdom-repeatmodelForElementメソッドを利用できます。(またitemForElementindexForElementに相当するメソッドも存在します。)

リストのフィルタリングとソーティング

表示されたリストのアイテムをフィルタリングまたはソートをするには、dom-repeatfilterまたはsort(あるいはその両方)プロパティを指定します:

  • filter:単一の引数(アイテム)をとるfilterコールバック関数を指定します。関数からの返り値がtrueならアイテムを表示して、falseなら省略します。これは標準のArrayfilterAPIに似ていますが、コールバックは引数に一つの配列アイテムしか取らない点に注意してください。パフォーマンス上の理由から、引数indexは含まれません。詳細については、配列インデックスのフィルタリングを参照してください。
  • sort:標準のArraysortAPIに準じて比較関数を指定します。

いずれの場合もその値は、関数オブジェクトでも、ホスト要素上で定義された関数を指示する文字列でも構いません。

デフォルトでは、filter及びsort関数は、次のいずれかが発生した時だけ実行されます。

  • 配列に監視可能な変化が生じた。(例えば、アイテムの追加または削除によって)
  • filterまたはsort関数が変更された。

関連のないデータの一部が変更された時に、filtersortを再実行するにはrenderを呼び出してください。例えば、要素にsort関数の動作を変更するsortOrderプロパティがある場合、sortOrderに変更があったときにrender呼び出すことができます。

itemsの特定のサブフィールドに変更があった時に、filterまたはsort関数を再実行するには、サブフィールドitemのスペース区切りのリストにobserveプロパティを設定します。そうすることで、再度フィルタリングやそーとが行われるでしょう。

例えば、dom-repeatで次のようなフィルター処理を行なったとします。:

isEngineer: function(item) {
    return item.type == 'engineer' || item.manager.type == 'engineer';
}

この時、observeプロパティは次のように設定する必要があります。

<template is="dom-repeat" items="{{employees}}"
    filter="isEngineer" observe="type manager.type">

manager.typeフィールドを変更すると、リストが再ソートされるはずです:

this.set('employees.0.manager.type', 'engineer');

ソートとフィルターの動的な変更

observeプロパティを使って、指定したアイテムのサブプロパティをフィルタリングやソートのために監視できます。しかし、場合によっては、他の関係を持たない値に基づきフィルタやソートを動的に変更したいことがあるかもしれません。このような場合には、算出バインディングを使用し、依存関係にあるプロパティが(一つ以上)変更された時に、動的にフィルタまたはソート関数を返すことができます。

<dom-module id="x-custom">

  <template>
    <input value="{{searchString::input}}">

    <!-- computeFilter returns a new filter function whenever searchString changes -->
    <template is="dom-repeat" items="{{employees}}" as="employee"
        filter="{{computeFilter(searchString)}}">
        <div>{{employee.lastname}}, {{employee.firstname}}</div>
    </template>
  </template>

  <script>
    class XCustom extends Polymer.Element {

      static get is() { return 'x-custom'; }

      static get properties() {
        return {
          employees: {
            type: Array,
            value() {
              return [
                { firstname: "Jack", lastname: "Aubrey" },
                { firstname: "Anne", lastname: "Elliot" },
                { firstname: "Stephen", lastname: "Maturin" },
                { firstname: "Emma", lastname: "Woodhouse" }
              ]
            }
          }
        }
      }

      computeFilter(string) {
        if (!string) {
          // set filter to null to disable filtering
          return null;
        } else {
          // return a filter function for the current search string
          string = string.toLowerCase();
          return function(employee) {
            var first = employee.firstname.toLowerCase();
            var last = employee.lastname.toLowerCase();
            return (first.indexOf(string) != -1 ||
                last.indexOf(string) != -1);
          };
        }
      }
    }

    customElements.define(XCustom.is, XCustom);
  </script>
</dom-module>

この例では、searchStringプロパティの値が変更されるたびcomputeFilterが呼び出され、filterプロパティの新しい値を算出します。

配列インデックスによるフィルタリング

Polymer内部における配列の記録(track)方法のために、配列のインデックスはフィルタ関数に渡されません。配列インデックスでアイテムを参照する際の計算量はO(n)です。これをフィルター関数上で実行ことを考えるとパフォーマンスにとても大きな影響が想定されます。

配列インデックスを参照する必要があり、パフォーマンス上の負荷を許容できる場合、次のようなコードを使用できます。:

filter: function(item) {
  var index = this.items.indexOf(item);
  ...
}

フィルター関数はdom-repeatthisの値として呼び出されるので、this.itemsで元の配列にアクセスして、それをインデックスの参照に使用することができます。

この参照は、元の配列のインデックスを返します。このインデックスは、表示された(フィルター・ソートされた)配列のインデックスと一致しない可能性があります。

dom-repeatテンプレートのネスト

複数のdom-repeatテンプレートをネストした際、親のスコープからデータにアクセスしたいかもしれません。 dom-repeatの内部では、現在のスコープ内のプロパティによって隠蔽されない限り、親のスコープで利用可能なすべてのプロパティにアクセスできます。

例えば、dom-repeatによって追加されたデフォルト値のitemindexプロパティは、親のスコープにある同名のプロパティを覆い隠します。

ネストされたdom-repeatテンプレートからプロパティにアクセスするには、as属性を使用してitemのプロパティに別の名前を割り当てます。indexプロパティに別の名前を割り当てるには、index-as属性を使用します。

<div> Employee list: </div>
<template is="dom-repeat" items="{{employees}}" as="employee">
    <div>First name: <span>{{employee.first}}</span></div>
    <div>Last name: <span>{{employee.last}}</span></div>

    <div>Direct reports:</div>

    <template is="dom-repeat" items="{{employee.reports}}" as="report" index-as="report_no">
      <div><span>{{report_no}}</span>.
           <span>{{report.first}}</span> <span>{{report.last}}</span>
      </div>
    </template>
</template>

同期レンダリングを強制

renderを呼び出すことで、データへのどんな変更に対してもdom-repeatテンプレートのレンダリングが同期的に行われるよう強制します。通常、変更はバッチ処理で非同期にレンダリングされます。同期的レンダリングにはパフォーマンス上の負荷があるものの、いくつかのシナリオでは役立つでしょう。:

  • ユニットテストにおいて、生成されたDOMをチェックする前にアイテムがレンダリングされていることを保証する。
  • 特定のアイテムへスクロールする前に、アイテムのリストがレンダリングされていることを保証する。
  • データの一部が配列の外部で変更されたとき(例えば、ソート順序やフィルタ条件など)、sortfilter関数を再実行する。

renderは、Polymerの配列の変更メソッドによって発生するような監視可能な変化だけ検出します。

テンプレートが監視不能な変更を検出するようにするには、テンプレートを強制的に更新するを参照してください 。

テンプレートを強制的に更新

開発者やサードパーティーライブラリが、Polymerのメソッドを使用せず配列を変更する場合、次のいずれかを実行できます。:

  • 配列の変更箇所を正確に把握している場合は、notifySplicesを使用することで、配列を監視するすべての要素に適切に通知されるようにします。

  • 配列のクローンを作成します。

  // Set items to a shallow clone of itself
  this.items = this.items.slice();

データ構造が複雑な場合、深いクローン(deep clone)が必要になることがあります。

  • 変更箇所を正確に把握していない場合は、dom-repeat上でmutableDataプロパティを設定して、配列へのダーティチェックを無効にできます。
  <template is="dom-repeat" items={{items}} mutable-data> ... </template>

mutableDataセットを使用して、配列上でnotifyPathを呼び出すと配列全体が再評価されます。

  //
  this.notifyPath('items');

詳細については、MutableDataミックスインの使用を参照してください。

配列やPolymerデータシステムとの連携に関する詳細は、配列との連携を参照してください。

大規模リストにおけるパフォーマンス向上

デフォルトでは、dom-repeatは、一度にすべてのアイテムリストをレンダリングしようとします。非常に大きなアイテムリストのレンダリングにdom-repeat使用しようとすると、レンダリングの最中UIがフリーズするかもしれません。この問題に直面した場合は、initialCountを設定して「チャンクされた(chunked)」レンダリングを有効にします。チャンクモードでは、 dom-repeatは最初にinitialCountで指定されたアイテムをレンダリングし、残りのアイテムはアニメーションフレーム単位で、チャンクを順番にレンダリングしていきます。これにより、UIスレッドはチャンクの間であってもユーザー入力を処理することができます。renderedItemCountプロパティ(読み取り専用)を使って、すでにレンダリングされたアイテム数を追跡することもできます。

dom-repeatは、各チャンクでレンダリングされるアイテムの数を調整することで、ターゲットのフレームレートを維持します。またtargetFramerateの設定によってレンダリングを調整することもできます。

さらにdelayプロパティを設定することで、filtersort関数が再実行される前に、一定の経過時間(デバウンス時間)を確保することもできます。

配列の選択のデータバインド(array-selector)

構造化されたデータを同期するには、バインドされたデータのパスの関係をPolymerが把握していなければいけません。array-selector要素は、配列内から特定のアイテムが選択された際にパスの結合を保証してくれます。

itemsプロパティは、ユーザーデータの配列をアプリケーションの他の部分に結合されているかもしれないselectedプロパティを更新するためにselect(item)deselect(item)を呼び出します。selectedのアイテム(群)のサブフィールドへの変更は、配列items内のアイテムと同期的に保たれます。

配列セレクタ(array selector)は、一つまたは複数の選択をサポートします。multifalseの場合、selectedプロパティは最後に選択したアイテムを表します。 multitrueの場合、selectedプロパティは選択されたアイテム群の配列になります。

配列セレクタは、後方互換性を確保するためにレガシー(polymer.html)インポートに含まれています。 polymer.htmlをインポートしない場合は、以下のコードに示すようにarray-selector.htmlをインポートしてください。

<link rel="import" href="components/polymer/polymer-element.html">
<! -- import template repeater -->
<link rel="import" href="components/polymer/src/elements/dom-repeat.html">
<!-- import array selector -->
<link rel="import" href="components/polymer/src/elements/array-selector.html">

<dom-module id="x-custom">

  <template>

    <div> Employee list: </div>
    <template is="dom-repeat" id="employeeList" items="{{employees}}">
        <div>First name: <span>{{item.first}}</span></div>
        <div>Last name: <span>{{item.last}}</span></div>
        <button on-click="toggleSelection">Select</button>
    </template>

    <array-selector id="selector" items="{{employees}}" selected="{{selected}}" multi toggle></array-selector>

    <div> Selected employees: </div>
    <template is="dom-repeat" items="{{selected}}">
        <div>First name: <span>{{item.first}}</span></div>
        <div>Last name: <span>{{item.last}}</span></div>
    </template>

  </template>

  <script>
    class XCustom extends Polymer.Element {

      static get is() { return 'x-custom'; }

      static get properties() {
        return {
          employees: {
            type: Array,
            value() {
              return [
                {first: 'Bob', last: 'Smith'},
                {first: 'Sally', last: 'Johnson'},
                // ...
              ];
            }
          }
        }
      }

      toggleSelection(e) {
        var item = this.$.employeeList.itemForElement(e.target);
        this.$.selector.select(item);
      }
    }

    customElements.define(XCustom.is, XCustom);
  </script>

</dom-module>

条件付きテンプレート(dom-if)

要素は、ブーリアンプロパティに基づいて条件付きでスタンプすることができます。これを実現するには、dom-ifと呼ばれる独自のHTMLTemplateElement型の拡張を使って要素をラップします。dom-ifテンプレートは、そのifプロパティがtrueになった時だけそのコンテンツをDOM内にスタンプします。

ifプロパティが再度falseになった場合、デフォルトでは、スタンプされたすべての要素は非表示になります(ただし、DOMツリーには残ります)。この仕組みによって、ifプロパティが再びtrueになった際、より高速なパフォーマンスを実現します。この動作を無効にするには、restampプロパティをtrueに設定します。この場合には、要素は毎回破棄され再スタンプされるので、ifによる切り替え動作は遅くなります。

条件付きテンプレートを使用する方法は二つあります。:

  • Polymer要素または他のPolymerの管理するテンプレート内では、省略記法<template is="dom-repeat">を使用してください。
  <template is="dom-if" if="{{condition}}">
    ...
  </template>
  • Polymerの管理するテンプレートの外側では、ラッパー要素<dom-if>を使用します。
  <dom-if>
    <template>
      ...
    </template>
  </dom-if>

このフォームでは、通常、itemsプロパティは命令的に設定します。:

  var conditional = document.querySelector('dom-if');
  conditional.if = true;

Polymerが管理するテンプレートには、Polymer要素のテンプレートや、dom-binddom-ifdom-repeatに属するテンプレート、あるいはTemplatizerによって管理されるテンプレートが含まれます。

ほとんどのケースにおいて、dom-repeatには、一番目(省略形)のフォームを使用することになるでしょう。

テンプレートリピーターは後方互換性を確保するため、レガシーなインポート(polymer.html)によって取り込まれます。もしpolymer.htmlをインポートしない場合は、次のコードに示すようarray-selector.htmlをインポートして下さい。

以下は、条件付きテンプレートがどのように動作するのかを示す簡単な例です。条件付きテンプレートの推奨された利用法は後述のガイダンスを参照してください。

例:

<link rel="import" href="components/polymer/polymer-element.html">
<! -- import conditional template -->
<link rel="import" href="components/polymer/src/elements/dom-if.html">

<dom-module id="x-custom">

  <template>

    <!-- All users will see this -->
    <my-user-profile user="{{user}}"></my-user-profile>


    <template is="dom-if" if="{{user.isAdmin}}">
      <!-- Only admins will see this. -->
      <my-admin-panel user="{{user}}"></my-admin-panel>
    </template>

  </template>

  <script>
    class XCustom extends Polymer.Element {

      static get is() { return 'x-custom'; }

      static get properties() {
        return {
          user: Object
        }
      }

    }

    customElements.define(XCustom.is, XCustom);
  </script>

</dom-module>

条件付きテンプレートを使用すると多少のオーバーヘッドが発生するため、CSSを使用することで容易に表示/非表示にできるような小さなUI要素には使用すべきでありません。

代わりに、読み込み時間を改善させたり、ページのメモリ容量を減らすために条件付きテンプレートを使って下さい。例えば:

  • ページ中のセクションをレイジーロードする。最初の描画時に必要のないページ中の一部要素は、dom-ifを使用してその定義が読み込みを終えるまで非表示にすることができます。この条件付きテンプレートの利用法に関しては、ケーススタディ:ショップアプリで説明しています。

  • 大規模サイトや複雑なサイトにおいてメモリの使用量を削減します。複雑なビューを複数持つシングルページアプリケーション(SPA)では、restampプロパティが設定されたdom-ifの中に各ビューを置くのは有効かもしれません。これにより、ユーザーが表示を切り替える(その箇所のDOMを再生成する)たびに、ある程度のレイテンシは犠牲になりますが、メモリの利用効率が改善されます。

条件付きテンプレートをどんな場面で利用するかについて、どんな場合にも当てはまる画一的な指針はありません。サイトのプロファイリングは、条件付きテンプレートの効果的な使い所を把握するために役立つでしょう。

自動バインディングテンプレート(dom-bind)

Polymerのデータバインディングは、Polymerによって管理されるテンプレート内だけで使用できます。したがって、データバインディングは、要素のDOMテンプレート内(あるいはdom-repeatdom-ifテンプレート内)では動作しますが、メインドキュメントに配置された要素では機能しません。

新たにカスタム要素を定義することなくPolymerのバインディングを利用するには、<dom-bind>要素を使用します。このテンプレートは、その子のテンプレート情報の内容をメインドキュメントに即座にスタンプします。自動バインディングテンプレートによるデータバインディングは、バインディングスコープとして<dom-bind>要素そのものを利用します。

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script src="components/webcomponentsjs/webcomponents-lite.js"></script>
  <link rel="import" href="polymer/src/elements/dom-bind.html">
  <link rel="import" href="polymer/src/elements/dom-repeat.html">

</head>
<body>
  <!-- Wrap elements with auto-binding template to -->
  <!-- allow use of Polymer bindings in main document -->
  <dom-bind>
    <template>

      <template is="dom-repeat" items="{{data}}">
        <div>{{item.name}}: {{item.price}}</div>
      </template>

    </template>
  </dom-bind>
  <script>
    var autobind = document.querySelector('dom-bind');

    // The dom-change event signifies when the template has stamped its DOM.
    autobind.addEventListener('dom-change', function() {
      console.log('template is ready.')
    });

    // set data property on dom-bind
    autobind.data = [
      { name: 'book', price: '$5.00'},
      { name: 'pencil', price: '$1.00'},
      { name: 'flux capacitor', price: '$8,000,000.00'}
    ];
  </script>
</body>
</html>

dom-bindの全ての機能は、Polymer要素の中であればすでに使用できます。自動バインディングテンプレートは、Polymer要素の外部のみで利用すべきです。

同期的レンダリングの強制dom-repeatと同様、dom-bindは、renderメソッドとmutableDataプロパティを提供しています。(同期レンダリングを強制テンプレートを更新で説明した通りです。)

dom-changeイベント

あるテンプレートのヘルパー要素がDOMツリーを更新すると、dom-changeイベントが発生します。

多くのケースでは、生成したノードと直接やりとりするのではなく、モデルデータの変更によって生成したDOMとやりとりするべきです。ノードに直接アクセスする必要がある場合には、dom-changeイベントを使用することができます。

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