【Polymer 1.0】テンプレートエレメント
目次
テンプレートリピーター (dom-repeat)
テンプレートリピーターは配列のバインディングに特化したテンプレートです。このテンプレートは、配列のアイテム毎にテンプレートコンテンツのインスタンスを作成します。また、各インスタンスのバインディングスコープには、次に示す2つのプロパティが追加されます:
item
: テンプレートコンテンツのインスタンス作成に使用される配列のアイテム。index
: 配列のitem
のインデックス (配列がソートまたはフィルタされるとindex
は変化する)。
テンプレートリピーターは型拡張カスタムエレメントであり、ビルトインの<template>
を拡張しています。使用するには<template is="dom-repeat">
と記述します。
<dom-module id="employee-list"> <template> <div>社員リスト:</div> <br> <!-- テンプレートリピーターを使用する --> <template is="dom-repeat" items="{{employees}}"> <div> <!-- 現在の配列アイテムのインデックス(index)を表示 --> <span># <span>{{index}}</span>. </span> <!-- 現在の配列アイテム(item)の内容を表示 --> <span>{{item.first}}</span> <span>{{item.last}}</span> </div> </template> </template> <script> Polymer({ is: 'employee-list', ready: function () { this.employees = [ {first: 'Bob', last: 'Smith'}, {first: 'Sally', last: 'Johnson'}, {first: 'Michael', last: 'Jackson'} ]; } }); </script> </dom-module>
配列アイテムのサブプロパティを変更するには以下の方法で値を変更する必要があります。でないと値を変更してもテンプレートリピーターが生成したコンテンツが更新されません:
- エレメントのプロパティバインディングを使用してサブプロパティの値を変更する。
Polymer.Base
のset()
メソッドを使用してサブプロパティの値を変更する。
サブプロパティ変更の詳細についてはここを参照ください。
配列自身の変更 (push
, pop
, splice
, shift
, unshift
) はPolymerエレメントが提供するメソッドを使用しなければなりません。でないと配列自身の変更を行ってテンプレートリピーターが生成したコンテンツが更新されません。配列自身の変更の詳細についてはここを参照ください。
テンプレートリピーターのイベントハンドリング
dom-repeat
テンプレートの各インスタンスをイベントハンドリングする際、どのエレメントでイベントが発火されたか、またそのエレメント (インスタンス) とひも付く配列のアイテムを取得したいことがあるでしょう。
dom-repeat
テンプレート内のエレメントのイベントリスナはイベントアノテーションで設定できます。テンプレートリピーターは発生したイベントにmodel
プロパティを追加し、これをイベントハンドラで取得できます。model
はテンプレートコンテンツのインスタンス生成時に関連付けられた配列アイテムを格納するモデルで、配列アイテムはイベントハンドラの中でevent.model.item
のようにして取得できます。
<dom-module id="simple-menu"> <template> <template is="dom-repeat" id="menu" items="{{menuItems}}"> <div> <span>{{item.name}}</span> <span>{{item.ordered}}</span> <!-- イベントアノテーションでイベントリスナを設定 --> <button on-click="_orderOnClick">Order</button> </div> </template> </template> <script> Polymer({ is: 'simple-menu', ready: function () { this.menuItems = [ {name: "Pizza", ordered: 0}, {name: "Pasta", ordered: 0}, {name: "Toast", ordered: 0} ]; }, // テンプレートリピーターの各インスタンスが持つ // Orderボタンがクリックされた際に呼び出される。 _orderOnClick: function (event) { // テンプレートリピーターが追加した`model`を取得 var model = event.model; // テンプレートリピーターのインスタンスとひも付く配列の`item`を取得 var item = model.item; // `model.set()`を使用してオーダー数をインクリメント model.set('item.ordered', item.ordered + 1); } }); </script> </dom-module>
このmodel
はPolymer.Base
を継承しているので、set()
、get()
、配列操作のメソッド (push
, pop
, splice
, shift
, unshift
) を全て使用することができます。というよりも、これらのメソッドを利用しないとテンプレートリピーターが生成したコンテンツが更新されないため、使用すべきです。
上記例のようにイベントアノテーションでリスナを設定するのではなく、addEventListener()
でリスナの設定を行うと、イベントにmodel
プロパティが追加されません。このような場合、dom-repeat
テンプレートが提供するmodelForElement(el)
メソッドを利用することでmodel
を取得できます (これに関連したメソッドとしてitemForElement(el)
とindexForElement(el)
も提供されます)。引数のel
にはテンプレートコンテンツのインスタンス生成時に作成されたエレメント (<div>
, <span>
, <button>
など) のいずれかを指定します。
次のサンプルでは、addEventListener()
でリスナの設定を行っているため、modelForElement(el)
でmodel
を取得しています。引数のel
にはevent.target
を渡しています。このevent.target
はテンプレートコンテンツのインスタンス生成時に作成された<button name="order">
になります。
<dom-module id="simple-menu"> <template> <template is="dom-repeat" id="menu" items="{{menuItems}}"> <div> <span>{{item.name}}</span> <span>{{item.ordered}}</span> <button name="order">Order</button> </div> </template> </template> <script> Polymer({ is: 'simple-menu', ready: function () { this.menuItems = [ {name: "Pizza", ordered: 0}, {name: "Pasta", ordered: 0}, {name: "Toast", ordered: 0} ]; }, attached: function () { this.async(function () { // テンプレートの各インスタンスがもつOrderボタンのイベントリスナを設定する var i, child; var menuChildren = Polymer.dom(this.root).children; for (i = 0; i < menuChildren.length; i++) { child = menuChildren[i]; var button = Polymer.dom(child).querySelector('[name="order"]'); if (!button) { continue; } // イベントアノテーションを使用せずaddEventListener()でリスナを設定 button.addEventListener('click', this._orderOnClick.bind(this), false); } }); }, _orderOnClick: function (event) { // テンプレートリピーターが追加した`model`を取得 var model = this.$.menu.modelForElement(event.target); // テンプレートリピーターのインスタンスとひも付く配列の`item`を取得 var item = this.$.menu.itemForElement(event.target); // `model.set()`を使用してオーダー数をインクリメント model.set('item.ordered', item.ordered + 1); } }); </script> </dom-module>
リストのフィルタとソート
表示されているリストをフィルタまたはソートするには、dom-repeat
でfilter
またはsort
プロパティを指定してください (または両方):
filter
: 標準のArray filter()
APIに従ったフィルタコールバック関数を指定します。sort
: 標準のArray sort()
APIに従ったフィルタコールバック関数を指定します。
両方とも設定する値には、関数オブジェクト、またはホストエレメントで定義した関数を文字列で指定できます。
デフォルトでは、指定されたフィルタとソート関数は配列自身が変更された場合にのみ実行されます (配列にアイテムが追加または削除されたような場合)。
配列アイテムのサブプロパティが変更された場合にフィルタとソート関数が実行されるためには、observer
プロパティを指定します。このプロパティには配列アイテムのサブプロパティをスペース区切りで複数指定できます。こうすることで、指定された配列のサブプロパティが変更されるとフィルタとソート関数が実行されます。
次はdom-repeat
のfilter
で指定する関数の例です:
// 管理者または技術者をフィルタリングするためのフィルタ関数 _filterFunc: function (employee) { return employee.admin || employee.engineer; }
observer
は次のように設定します:
<template is="dom-repeat" items="{{employees}}" filter="_filterFunc" observe="admin engineer">
指定されたフィルタ関数は、employee.admin
またはemployee.engineer
が変更されると実行されます:
_adminOnChange: function (event) { ... event.model.set('item.admin', true); }
次のサンプルでは、管理者または技術者のどちらかにチェックがある社員をフィルタして表示します。「管理者または技術者をフィルタ」のチェックボックスでフィルタの有り/無しを設定できます。また各社員の「管理者」と「技術者」のチェックを両方はずすと、この社員は画面から消えます。
(Note: このサンプルではフィルタの設定を上記例で示したようにdom-repeat
のマークアップで行わず、_setupFilter()
の中で行っています)
<dom-module id="employee-list"> <template> <!-- フィルタ有り/無しのチェックボックス --> <div> <span>管理者または技術者をフィルタ: </span> <input id="filterCheckbox" type="checkbox" on-change="_setupFilter" checked> </div> <br> <!-- 社員リストのテンプレートリピーター --> <template is="dom-repeat" id="employeesRepeat" items="{{employees}}"> <!-- 社員名 --> <div>名前: <span>{{item.name}}</span></div> <div> <!-- 管理者か否かのチェックボックス --> <span>管理者: <input type="checkbox" checked="{{item.admin}}" on-change="_adminOnChange"> </span> <!-- 技術者か否かのチェックボックス --> <span>技術者: <input type="checkbox" checked="{{item.engineer}}" on-change="_engineerOnChange"> </span> </div> <br> </template> </template> <script> Polymer({ is: 'employee-list', ready: function () { this.employees = [ {name: 'Sally Johnson', admin: false, engineer: true}, {name: 'Bob Smith', admin: true, engineer: false}, {name: 'Michael Jackson', admin: true, engineer: true} ]; this._setupFilter(); }, // テンプレートリピーターにフィルタ設定を行う関数 _setupFilter: function () { if (this.$.filterCheckbox.checked) { // フィルタを設定(フィルタ関数を文字列で指定している) this.$.employeesRepeat.filter = '_filterFunc'; this.$.employeesRepeat.observe = 'admin engineer'; } else { // フィルタを解除 this.$.employeesRepeat.filter = undefined; } }, // 管理者または技術者をフィルタリングするためのフィルタ関数 _filterFunc: function (employee) { return employee.admin || employee.engineer; }, // 管理者のチェックが変更された場合のハンドラ _adminOnChange: function (event) { var admin = event.target.checked; event.model.set('item.admin', admin); }, // 技術者のチェックが変更された場合のハンドラ _engineerOnChange: function (event) { var engineer = event.target.checked; event.model.set('item.engineer', engineer); } }); </script> </dom-module>
次のサンプルでは、名前順で社員リストをソートします。「Sort name」のチェックボックスでフィルタの有り/無しを設定できます。
<dom-module id="employee-list"> <template> <div> <!-- ソート有り/無しのチェックボックス --> <span>Sort name: </span> <input id="sortCheckbox" type="checkbox" on-change="_setupSort" checked> </div> <br> <!-- 社員リストのテンプレートリピーター --> <template is="dom-repeat" id="employeesRepeat" items="{{employees}}"> <!-- 社員名 --> <div>Name: <span>{{item.name}}</span></div> </template> </template> <script> Polymer({ is: 'employee-list', ready: function () { this.employees = [ {name: 'Sally Johnson'}, {name: 'Bob Smith'}, {name: 'Michael Jackson'} ]; this._setupSort(); }, // テンプレートリピーターにソート設定を行う関数 _setupSort: function () { if (this.$.sortCheckbox.checked) { // ソートを設定(ソート関数を関数オブジェクトで指定している) this.$.employeesRepeat.sort = this._sortFunc; this.$.employeesRepeat.observe = 'name'; } else { // ソートを解除 this.$.employeesRepeat.sort = undefined; } }, // 社員名で社員リストをソートするためのソート関数 _sortFunc: function (employee1, employee2) { if (employee1.name < employee2.name) { return -1; } else if (employee1.name > employee2.name) { return 1; } else { return 0; } } }); </script> </dom-module>
ネストしたテンプレートリピーター
dom-repeat
テンプレートがネストした場合、子スコープから親スコープのプロパティにアクセスしたいことがあります。親スコープのプロパティが子スコープのプロパティに上書きされるものは除いて、その他の親スコープのプロパティにはアクセスすることができます。
例えば、dom-repeat
のスコープにはデフォルトでitem
とindex
プロパティが追加されますが、子スコープでこれと同じ名前のプロパティがある場合、親スコープのプロパティは隠蔽されます。
このような場合は、as
属性を使用してitem
プロパティの別名をつけてください。index
プロパティはindex-as
属性でプロパティの別名を付けてください。
<dom-module id="employee-list"> <template> <div>社員リスト:</div> <br> <!-- 社員リストのテンプレートリピーター ・as属性で`item`の別名を`employee`としている --> <template is="dom-repeat" items="{{employees}}" as="employee"> <div> <!-- 社員リストのインデックス --> <span># <span>{{index}}</span>. </span> <!-- 社員名 ・`item`ではなく別名の`employee`を使用している --> <span>{{employee.name}}</span> </div> <div>部下リスト:</div> <!-- 部下リストのテンプレートリピーター ・親スコープの`employee`にアクセスしている ・index-as属性で`index`の別名を`report_no`としている --> <template is="dom-repeat" items="{{employee.reports}}" index-as="report_no"> <div> <!-- 部下リストのインデックス ・`index`ではなく別名の`report_no`を使用している --> <span># <span>{{report_no}}</span>. </span> <!-- 部下名 --> <span>{{item.name}}</span> </div> </template> <br/> </template> </template> <script> Polymer({ is: 'employee-list', ready: function () { this.employees = [ {name: 'Taro Yamada', reports: [ {name: 'Takeshi Okada'}, {name: 'Torajiro Kuruma'} ]}, {name: 'ichiro Suzuki', reports: [ {name: 'Yoshio Hara'} ]} ]; } }); </script> </dom-module>
配列セレクタ (array-selector)
<array-selector>
エレメントは指定された配列の選択状態を管理してくれます。
<array-selector id="employeeSelector" items="{{employees}}" selected="{{_selectedEmployees}}" multi toggle> </array-selector>
items
プロパティは選択状態を管理したい配列を指定します。選択状態は単一と複数があり、複数選択したい場合はmulti
を指定します。選択状態をトグルさせたい場合はtoggle
を指定します。
配列のアイテムが選択されると、selected
プロパティで指定した名前の変数に、選択されたアイテムが反映されます。selected
に指定した名前は他のエレメントと連携するために使用されます。下にあるサンプルではselected
に指定した_selectedEmployees
が「選択された社員リスト」のdom-repeat
のitems
にも設定されており、選択状態が変化するごとにこのリストも更新されます。
multi
がfalse
の場合、selected
は最後に選択されたアイテムとなります。multi
がtrue
の場合、selected
は選択されたアイテムの配列になります。
次のサンプルでは、Selectボタン付きで複数の社員が表示されます。Selectボタンをタップすると「選択された社員リスト」に選択された社員が現れます。もう一度Selectボタンをタップすると選択状態がトグルされて非選択になり、「選択された社員リスト」から社員が消えます。
<dom-module id="employee-list"> <template> <div>社員リスト:</div> <br> <!-- 社員リストのテンプレートリピーター --> <template is="dom-repeat" id="employeesRepeat" items="{{employees}}"> <div> <!-- 社員の選択/解除のトグルボタン --> <button on-tap="_selectEmployeeOnTap">Select</button> <!-- 社員名 --> <span>{{item.name}}</span> </div> </template> <br> <!-- 社員の選択状態を管理する配列セレクタ --> <array-selector id="employeeSelector" items="{{employees}}" selected="{{_selectedEmployees}}" multi toggle> </array-selector> <!-- 選択された社員リストのテンプレートリピーター --> <div>選択された社員リスト:</div> <template is="dom-repeat" items="{{_selectedEmployees}}"> <div>{{item.name}}</div> </template> </template> <script> Polymer({ is: 'employee-list', ready: function () { this.employees = [ {name: 'Bob Smith'}, {name: 'Sally Johnson'}, {name: 'John Doe'} ]; }, _selectEmployeeOnTap: function (event) { // 取得した配列アイテムを選択状態をトグルする // (<array-selector>に`toggle`が設定されているのでトグルする) this.$.employeeSelector.select(event.model.item); } }); </script> </dom-module>
条件付きテンプレート (dom-if)
ある条件の時にエレメントを表示させたい場合はdom-if
テンプレートを利用できます。dom-if
はHTMLTemplateElement
の型拡張カスタムエレメントです。dom-if
で対象のエレメントを囲い、if
プロパティに指定した条件がtrue
になった場合のみエレメントがDOMに挿入されます。
デフォルトではif
プロパティがfalse
になった場合、囲まれているエレメントが隠されます (ただしDOMツリーには残ります)。この挙動によりif
プロパティが再度true
になった場合はエレメントの再生成が行われないためパフォーマンスが向上します。この挙動を無効にしたい場合はrestamp
プロパティをtrue
に設定してください。この設定によりif
プロパティの変更が起こるたびにエレメントの破棄と生成が行われるためパフォーマンスは低下します。
<dom-module id="employee-list"> <template> ... <div>選択された社員:</div> <div> <span>{{_selectedEmployee.name}}</span> <!-- 選択された社員が管理者(adminがtrue)の場合のみ表示される --> <template is="dom-if" if="{{_selectedEmployee.admin}}"> <span>(管理者)</span> </template> </div> </template> <script> Polymer({ is: 'employee-list', ready: function () { this.employees = [ {name: 'Bob Smith', admin: true}, {name: 'Sally Johnson', admin: false}, {name: 'John Doe', admin: true} ]; }, ... }); </script> </dom-module>
次のサンプルでは、Selectボタン付きで複数の社員が表示されます。Selectボタンをタップすると「選択された社員」に選択された社員が現れます。この社員が管理者の場合のみ「(管理者)」が表示されます。
<dom-module id="employee-list"> <template> <div>社員リスト:</div> <br> <!-- 社員リストのテンプレートリピーター --> <template is="dom-repeat" id="templateList" items="{{employees}}"> <div> <!-- 社員の選択/解除のトグルボタン --> <button on-tap="_selectEmployeeOnTap">Select</button> <!-- 社員名 --> <span>{{item.name}}</span> </div> </template> <br> <!-- 社員の選択状態を管理する配列セレクタ --> <array-selector id="selector" items="{{employees}}" selected="{{_selectedEmployee}}"> </array-selector> <!-- 選択された社員ー --> <div>選択された社員:</div> <div> <span>{{_selectedEmployee.name}}</span> <!-- 選択された社員が管理者(adminがtrue)の場合のみ表示される --> <template is="dom-if" if="{{_selectedEmployee.admin}}"> <span>(管理者)</span> </template> </div> </template> <script> Polymer({ is: 'employee-list', ready: function () { this.employees = [ {name: 'Bob Smith', admin: true}, {name: 'Sally Johnson', admin: false}, {name: 'John Doe', admin: true} ]; }, _selectEmployeeOnTap: function (e) { // エレメントと関連する配列アイテムを取得 var item = this.$.templateList.itemForElement(e.target); // 取得した配列アイテムを選択状態にする this.$.selector.select(item); } }); </script> </dom-module>
条件付きテンプレートの適切な利用ケース
条件付きテンプレートは条件を満たさないとエレメントが生成されないため、初期起動のコストは軽減されます。ただし、条件がめったにtrue
にならない場合にのみ条件付きテンプレートは利用すべきです。あまりに自由に条件付きテンプレートを使用すると実行時に重大なパフォーマンスのオーバーヘッドを引き起こします。
例として、アプリケーションに一般ユーザーが使用する4つの画面 (タブなどで切り替える) と、管理者が使用する管理画面があることを思い浮かべてみてください。条件付きテンプレートを利用して、起動時は初期画面しか生成せず、あとの画面は生成しないようにしたとします。確かに起動は早くなるかもしれませんが、この削減されたコストは別の画面を表示する際のコストへ移動しただけです。これにより画面を切り替える際に画面を生成しなければならないため、単純に画面を切り替えるのにくらべて表示が遅くなってしまいます。シンプルな画面の切替はhidden
属性をバインドすることで行えます (例: <div hidden$="">
属性バインディングの詳細はここを参照)。
ただし、管理者のみが利用できる管理画面に対して条件付きテンプレートを使用することは適切です。ほとんどのユーザーは管理者ではなく、管理画面を開けないため画面切り替えのパフォーマンス低下を受けることはありません。管理画面が比較的重い場合は条件付きテンプレートの使用が特に有効になります。
自動バインディングテンプレート (dom-bind)
Polymerのデータバインディングは、カスタムエレメントのLocal DOMテンプレートの中でのみ動作します。
メインドキュメントなどで、新しいカスタムエレメントを定義せずにPolymerのバインディング使用するには、自動バインディングテンプレート (dom-bind
) を使用してください。自動バインディングテンプレートのバインディングスコープは、自動バインディングテンプレート自身になります。
<!DOCTYPE html> <html> <head> <script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <link rel="import" href="../bower_components/polymer/polymer.html"> </head> <body> <!-- メインドキュメントでPolymerのバインディングを使用するには、 自動バインディングテンプレートでバインディングしたいエレメントを囲んでください --> <template id="employeeList" is="dom-bind"> <template is="dom-repeat" items="{{employees}}"> <div><span>{{item.name}}</span></div> </template> </template> <script> // 自動バインディングテンプレートを取得 var employeeList = document.querySelector('#employeeList'); // テンプレートがDOMツリーを更新するとdom-changeイベントが通知されます employeeList.addEventListener('dom-change', function () { // この時点で自動バインディングテンプレートの準備は完了している }); // Web Componentsの準備が整った際(画面の準備がととのった際)の処理 document.addEventListener('WebComponentsReady', function () { // 自動バインディングテンプレートにデータを設定 employeeList.employees = [ {name: 'Bob Smith'}, {name: 'Sally Johnson'}, {name: 'John Doe'} ]; }); </script> </body> </html>
自動バインディングテンプレートはPolymerエレメントでも利用可能ですが、自動バインディングテンプレートはPolymerエレメントの外で使用すべきです。
dom-changeイベント
テンプレートエレメント (<template>
) がDOMツリーを更新すると、dom-change
イベントが発火されます。
次のサンプルでは、社員リストの先頭にあるチェックボックスを変更するとdom-change
イベントに対するハンドラ (_onDomChange()
) が呼び出されます。イベントが発生する仕組みや、イベントオブジェクトからどのような情報が取得できるか確認してみてください。
<dom-module id="employee-list"> <template> <template is="dom-repeat" id="templateList" items="{{employees}}"> <div> <!-- 管理者チェックボックス --> <input type="checkbox" checked="{{item.admin::change}}"> <!-- 社員名 --> <span>{{item.name}}</span> <!-- 管理者ラベル --> <template is="dom-if" if="{{item.admin}}"> <span>(管理者)</span> </template> </div> </template> </template> <script> Polymer({ is: 'employee-list', listeners: { 'dom-change': '_onDomChange' }, ready: function () { this.employees = [ {name: 'Bob Smith', admin: true}, {name: 'Sally Johnson', admin: false}, {name: 'John Doe', admin: true} ]; }, // ホスト内のいずれかのテンプレートエレメントがDOMツリーを更新すると // この`dom-change`イベントハンドラが呼び出されます。 _onDomChange: function (event) { console.log('■■■ rootTarget ■■■'); console.log(Polymer.dom(event).rootTarget); console.log('■■■ localTarget ■■■'); console.log(Polymer.dom(event).localTarget); } }); </script> </dom-module>