-
Notifications
You must be signed in to change notification settings - Fork 33
Data Binding Helper Elements
Polymer provides a set of custom elements to help with common data binding use cases:
- Template repeater. Creates an instance of the template's contents for each item in an array.
- Array selector. Manages selection state for an array of structured data.
- Conditional template. Stamps its contents if a given condition is true.
- Auto-binding template. Allows data binding outside of a Polymer element.
The template repeater is a specialized template that binds to an array. It creates one instance of the template's contents for each item in the array. It adds two properties to the binding scope for each instance:
-
item
. The array item used to create this instance. -
index
. The index ofitem
in the array. (Theindex
value changes if the array is sorted or filtered)
The template repeater is a type-extension custom element
that extends the built-in <template>
element, so it is written as <template is="dom-repeat">
.
Example:
employee_list.html:
<dom-module id="employee-list">
<template>
<div> Employee list: </div>
<template is="dom-repeat" items="{{employees}}">
<div># <span>{{index}}</span></div>
<div>First name: <span>{{item.first}}</span></div>
<div>Last name: <span>{{item.last}}</span></div>
</template>
</template>
</dom-module>
employee_list.dart:
@PolymerRegister('employee-list')
class EmployeeList extends PolymerElement {
EmployeeList.created() : super.created();
@property
List<Employee> employees;
void ready() {
set('employees', [
new Employee('Bob', 'Smith'),
new Employee('Sally', 'Johnson'),
]);
}
}
class Employee extends JsProxy {
@reflectable
String first;
@reflectable
String last;
Employee(this.first, this.last);
}
Notifications for changes to items sub-properties are forwarded to the template instances, which update via the normal structured data notification system .
Mutations to the items
list itself must be performed using methods provided on
Polymer elements, such that the changes are observable to any elements bound to
the same list in the tree. For example:
add('employees', new Employee('Jack', 'Aubrey'));
When handling events generated by a dom-repeat
template instance, you
frequently want to map the element firing the event to the model data that
generated that item.
When you add a declarative event handler inside the <dom-repeat>
template,
the repeater adds a model
property to each event sent to the listener. The model
is the scope data used to generate the template instance, so the item
data is model.item
:
simple_menu.html:
<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="order">Order</button>
</div>
</template>
</template>
</dom-module>
simple_menu.dart:
@PolymerRegister('simple-menu')
class SimpleMenu extends PolymerElement {
SimpleMenu.created() : super.created();
@property
List<MenuItem> menuItems;
void ready() {
set('menuItems', [
new MenuItem('Pizza'),
new MenuItem('Pasta'),
new MenuItem('Toast'),
]);
}
@reflectable
void order(e, [_]) {
var model = new DomRepeatModel.fromEvent(e);
model.set('item.ordered', model.item.ordered + 1);
}
}
class MenuItem extends JsProxy {
@reflectable
String name;
@reflectable
int ordered = 0;
MenuItem(this.name);
}
The model
is an instance of the js Polymer.Base
object, so set
, get
and
the js array manipulation methods are all available on the model
object, and
should be used to manipulate the model.
Note: The model
property is not added for event listeners registered
imperatively (using addEventListener
, or on
), or listeners added to one of
the <dom-repeat>
template's parent nodes. In these cases, you can use
the <dom-repeat>
modelForElement
method to retrieve the
model data that generated a given element. (There are also corresponding
itemForElement
and indexForElement
methods.)
To filter or sort the displayed items in your list, specify a filter
or
sort
property on the dom-repeat
(or both):
-
filter
. Specifies a filter callback function following the standardArray
filter
API. -
sort
. Specifies a comparison function following the standardArray
sort
API.
In both cases, the value can be either a function object, or a string identifying a function defined on the host element.
By default, the filter
and sort
functions only run when the list itself
is mutated (for example, by adding or removing items).
To re-run the filter
or sort
functions when certain sub-fields
of items
change, set the observe
property to a space-separated list of
item
sub-fields that should cause the list to be re-filtered or re-sorted.
For example, for a dom-repeat
with a filter of the following:
@reflectable
isEngineer(item) => item.type == 'engineer' || item.manager.type == 'engineer';
Then the observe
property should be configured as follows:
<template is="dom-repeat" items="{{employees}}"
filter="isEngineer" observe="type manager.type">
Changing a manager.type
field should now cause the list to be re-sorted:
set('employees.0.manager.type', 'engineer');
When nesting multiple dom-repeat
templates, you may want to access data
from a parent scope. Inside a dom-repeat
, you can access any properties available
to the parent scope unless they're hidden by a property in the current scope.
For example, the default item
and index
properties added by dom-repeat
hide any similarly-named properties in a parent scope.
To access properties from nested dom-repeat
templates, use the as
attribute to
assign a different name for the item property. Use the index-as
attribute to assign a
different name for the index property.
<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>
Keeping structured data in sync requires that Polymer understand the path
associations of data being bound. The array-selector
element ensures path
linkage when selecting specific items from an array. The array selector supports
either single or multiple selection.
The items
property accepts an array of user data. Call select(item)
and deselect(item)
to update the selected
property, which may be bound to
other parts of the application. Any changes to sub-fields of the selected
item(s) are kept in sync with items in the items
array.
When multi
is false, selected
is a property representing the last selected
item. When multi
is true, selected
is an array of selected items.
employee_list.html:
<dom-module id="employee-list">
<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>
</dom-module>
employee_list.dart:
@PolymerRegister('employee-list')
class EmployeeList extends PolymerElement {
EmployeeList.created() : super.created();
@property
List<Employee> employees;
void ready() {
set('employees', [
new Employee('Bob', 'Smith'),
new Employee('Sally', 'Johnson'),
]);
}
@reflectable
void toggleSelection(e, target) {
// Have to use js interop to use this today, see
// https://github.com/dart-lang/polymer_interop/issues/7
var item = $['employeeList'].itemForElement(e.target);
$['selector'].select(item);
}
}
class Employee extends JsProxy {
@reflectable
String first;
@reflectable
String last;
Employee(this.first, this.last);
}
Elements can be conditionally stamped based on a boolean property by wrapping
them in a custom HTMLTemplateElement
type extension called dom-if
. The
dom-if
template stamps its contents into the DOM only when its if
property becomes
truthy.
If the if
property becomes falsy again, by default all stamped elements are hidden
(but remain in the DOM tree). This provides faster performance should the if
property become truthy again. To disable this behavior, set the
restamp
property to true
. This results in slower if
switching behavior as the
elements are destroyed and re-stamped each time.
The following is a simple example to show how conditional templates work. Read below for guidance on recommended usage of conditional templates.
Example:
user_page.html:
<dom-module id="user-page">
<template>
All users will see this:
<div>{{user.name}}</div>
<template is="dom-if" if="{{user.isAdmin}}">
Only admins will see this.
<div>{{user.secretAdminStuff}}</div>
</template>
</template>
</dom-module>
user_page.dart:
@PolymerRegister('user-page')
class UserPage extends PolymerElement {
UserPage.created() : super.created();
@property
User user;
}
Since it is generally much faster to hide and show elements rather than destroy and recreate them, conditional templates are only useful to save initial creation cost when the elements being stamped are relatively heavyweight and the conditional may rarely (or never) be true in given usages. Otherwise, liberal use of conditional templates can actually add significant runtime performance overhead.
Consider an app with 4 screens, plus an optional admin screen. If most users
will use all 4 screens during normal use of the app, it is generally better to
incur the cost of stamping those elements once at startup (where some app
initialization time is expected) and simply hide/show the screens as the user
navigates through the app, rather than destroy and re-create all the elements of
each screen as the user navigates. Using a conditional template here may be a
poor choice, since although it may save time at startup by stamping only the
first screen, that saved time gets shifted to runtime latency for each user
interaction, since the time to show the second screen will be slower as it
must create the second screen from scratch rather than simply showing that
screen. Hiding/showing elements is as simple as attribute-binding to the
hidden
attribute (e.g. <div hidden$="{{!shouldShow}}">
), and does not
require conditional templating at all.
However, using a conditional template may be appropriate in the case of an admin screen that's only shown to admin users of an app. Since most users aren't admins, there may be performance benefits to not burdening most of the users with the cost of stamping the elements for the admin page, especially if it is relatively heavyweight.
Polymer data binding is only available in templates that are managed by Polymer. So data binding works inside an element's local DOM template, but not for elements placed in the main document.
To use Polymer bindings without defining a new custom element,
use the dom-bind
element. This template immediately stamps its contents
into the main document. Data bindings in an auto-binding template use the template
itself as the binding scope.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="components/webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="components/polymer/polymer.html">
<link rel="import" href="components/iron-ajax/iron-ajax.html">
</head>
<body>
<!-- Wrap elements with auto-binding template to -->
<!-- allow use of Polymer bindings in main document -->
<template id="t" is="dom-bind">
<iron-ajax url="http://..." last-response="{{data}}" auto></iron-ajax>
<template is="dom-repeat" items="{{data}}">
<div><span>{{item.first}}</span> <span>{{item.last}}</span></div>
</template>
</template>
</body>
<script type="application/dart">
main() {
var t = document.querySelector('#t');
// The dom-change event signifies when the template has stamped its DOM.
t.on['dom-change'].listen((e) {
// auto-binding template is ready.
});
}
</script>
</html>
All of the features in dom-bind
are already available inside a Polymer
element. Auto-binding templates should only be used outside of a Polymer element.
When one of the template helper elements updates the DOM tree, it fires a dom-change
event.
In most cases, you should interact with the created DOM by changing the model data, not by
interacting directly with the created nodes. For those cases where you need to access the
nodes directly, you can use the dom-change
event.