Data binding helper elements
Table of contents
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.
Template repeater (dom-repeat)
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:
<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>
<script>
Polymer({
is: 'employee-list',
ready: function() {
this.employees = [
{first: 'Bob', last: 'Smith'},
{first: 'Sally', last: 'Johnson'},
...
];
}
});
</script>
</dom-module>
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
array itself (push
, pop
, splice
, shift
,
unshift
), should be performed using Polymer’s
array mutation methods.
These methods ensure that any elements observing the array are kept in sync.
If you can’t avoid using the native Array.prototype
methods, make sure to
call notifySplices
to ensure that any
elements watching items
are properly updated.
Handling events in dom-repeat
templates
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
:
<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>
<script>
Polymer({
is: 'simple-menu',
ready: function() {
this.menuItems = [
{ name: "Pizza", ordered: 0 },
{ name: "Pasta", ordered: 0 },
{ name: "Toast", ordered: 0 }
];
},
order: function(e) {
var model = e.model;
model.set('item.ordered', model.item.ordered+1);
}
});
</script>
</dom-module>
The model
is an instance of Polymer.Base
, so set
, get
and the 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 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.)
Filtering and sorting lists
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, that takes a single argument (the item) and returns true to display the item, false to omit it. Note that this is similar to the standardArray
filter
API, but the callback only takes a single argument, the array item. For performance reasons, it doesn’t include theindex
argument. See Filtering on array index for more information.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 one of the
following occurs:
- The array itself is mutated (for example, by adding or removing items).
- The
filter
orsort
function is changed.
To re-run the filter
or sort
when an unrelated piece of data changes,
call render
. For example, if your element has a
sortOrder
property that changes how the sort
function works, you can
call render
when sortOrder
changes.
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:
isEngineer: function(item) {
return 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:
this.set('employees.0.manager.type', 'engineer');
Dynamic sort and filter changes
The observe
property lets you specify item sub-properties to
observe for filtering and sorting purposes. However, sometimes you want to
dynamically change the sort or filter based on another unrelated value. In
this case, you can use a computed binding to return a dynamic filter or
sort function when one or more dependent properties changes.
<dom-module id="employee-search">
<template>
<input value="{{searchString::input}}">
<template is="dom-repeat" items="{{employees}}" as="employee"
filter="{{computeFilter(searchString)}}">
<div>{{employee.lastname}}, {{employee.firstname}}</div>
</template>
</template>
<script>
Polymer({
is: "employee-search",
computeFilter: function(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);
};
}
},
properties: {
employees: {
type: Array,
value: function() {
return [
{ firstname: "Jack", lastname: "Aubrey" },
{ firstname: "Anne", lastname: "Elliot" },
{ firstname: "Stepehen", lastname: "Maturin" },
{ firstname: "Emma", lastname: "Woodhouse" }
]
}
}
}
});
</script>
</dom-module>
In this example, whenever the value of the searchString
property changes,
computeFilter
is called to compute a new value for the filter
property.
Filtering on array index
Because of the way Polymer tracks arrays internally, the array index isn’t passed to the filter function. Looking up the array index for an item is an O(n) operation. Doing so in a filter function could have significant performance impact.
If you need to look up the array index and are willing to pay the performance penalty, you can use code like the following:
filter: function(item) {
var index = this.items.indexOf(item);
...
}
The filter function is called with the dom-repeat
as the this
value, so
you can access the original array as this.items
and use it to look up the index.
This lookup returns the items index in the original array, which may not match the index of the array as displayed (filtered and sorted).
Nesting dom-repeat templates
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>
Forcing synchronous renders
Call render
to force a dom-repeat
template to synchronously render any changes to its
data. Normally changes are batched and rendered asynchronously. Synchronous
rendering has a performance cost, but can be useful in a few scenarios:
- For unit testing, to ensure items have rendered before checking the generated DOM.
- To ensure a list of items have rendered before scrolling to a specific item.
- To re-run the
sort
orfilter
functions when a piece of data changes outside the array (sort order or filter criteria, for example).
render
only works with changes made with Polymer’s
array mutation methods.
If you or a third-party library mutate the array without Polymer’s methods,
you need to call notifySplices
to ensure that any elements
watching the array are properly notified.
Improve performance for large lists
By default, dom-repeat
tries to render all of the list items at once. If
you try to use dom-repeat
to render a very large list of items, the UI may
freeze while it’s rendering the list. If you encounter this problem, enable
“chunked” rendering by setting
initialCount
.
In chunked mode,
dom-repeat
renders initialCount
items at first, then renders the rest of
the items incrementally one chunk per animation frame. This lets the UI thread
handle user input between chunks. You can keep track of how many items have
been rendered with the
renderedItemCount
read-only property.
dom-repeat
adjusts the number of items rendered in each chunk to try and
maintain a target framerate. You can further tune rendering by setting
targetFramerate
.
You can also set a debounce time that must pass before a filter
or sort
function is re-run by setting the
delay
property.
Array selector (array-selector)
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.
<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>
<script>
Polymer({
is: 'employee-list',
ready: function() {
this.employees = [
{first: 'Bob', last: 'Smith'},
{first: 'Sally', last: 'Johnson'},
...
];
},
toggleSelection: function(e) {
var item = this.$.employeeList.itemForElement(e.target);
this.$.selector.select(item);
}
});
</script>
</dom-module>
Conditional templates
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:
<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>
<script>
Polymer({
is: 'user-page',
properties: {
user: Object
}
});
</script>
</dom-module>
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$="">
), 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.
Auto-binding templates
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>
var t = document.querySelector('#t');
// The dom-change event signifies when the template has stamped its DOM.
t.addEventListener('dom-change', function() {
// 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.
dom-change event
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.