2016-04-27 10:01:40 -07:00
|
|
|
<!--
|
|
|
|
|
@license
|
|
|
|
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
|
|
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
|
|
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
|
|
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
|
|
|
Code distributed by Google as part of the polymer project is also
|
|
|
|
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
|
|
|
|
|
The `dom-repeat` element is a custom `HTMLTemplateElement` type extension that
|
|
|
|
|
automatically stamps and binds one instance of template content to each object
|
|
|
|
|
in a user-provided array. `dom-repeat` accepts an `items` property, and one
|
|
|
|
|
instance of the template is stamped for each item into the DOM at the location
|
|
|
|
|
of the `dom-repeat` element. The `item` property will be set on each instance's
|
|
|
|
|
binding scope, thus templates should bind to sub-properties of `item`.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
<dom-module id="employee-list">
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
|
|
|
|
<div> Employee list: </div>
|
|
|
|
|
<template is="dom-repeat" items="{{employees}}">
|
|
|
|
|
<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 will be forwarded to template
|
|
|
|
|
instances, which will update via the normal structured data notification system.
|
|
|
|
|
|
|
|
|
|
Mutations to the `items` array itself should me made using the Array
|
|
|
|
|
mutation API's on `Polymer.Base` (`push`, `pop`, `splice`, `shift`,
|
|
|
|
|
`unshift`), and template instances will be kept in sync with the data in the
|
|
|
|
|
array.
|
|
|
|
|
|
|
|
|
|
Events caught by event handlers within the `dom-repeat` template will be
|
|
|
|
|
decorated with a `model` property, which represents the binding scope for
|
|
|
|
|
each template instance. The model is an instance of Polymer.Base, and should
|
|
|
|
|
be used to manipulate data on the instance, for example
|
|
|
|
|
`event.model.set('item.checked', true);`.
|
|
|
|
|
|
|
|
|
|
Alternatively, the model for a template instance for an element stamped by
|
|
|
|
|
a `dom-repeat` can be obtained using the `modelForElement` API on the
|
|
|
|
|
`dom-repeat` that stamped it, for example
|
|
|
|
|
`this.$.domRepeat.modelForElement(event.target).set('item.checked', true);`.
|
|
|
|
|
This may be useful for manipulating instance data of event targets obtained
|
|
|
|
|
by event handlers on parents of the `dom-repeat` (event delegation).
|
|
|
|
|
|
|
|
|
|
A view-specific filter/sort may be applied to each `dom-repeat` by supplying a
|
|
|
|
|
`filter` and/or `sort` property. This may be a string that names a function on
|
|
|
|
|
the host, or a function may be assigned to the property directly. The functions
|
|
|
|
|
should implemented following the standard `Array` filter/sort API.
|
|
|
|
|
|
2016-09-19 15:19:22 -07:00
|
|
|
In order to re-run the filter or sort functions based on changes to sub-fields
|
|
|
|
|
of `items`, the `observe` property may be set as a space-separated list of
|
|
|
|
|
`item` sub-fields that should cause a re-filter/sort when modified. If
|
|
|
|
|
the filter or sort function depends on properties not contained in `items`,
|
|
|
|
|
the user should observe changes to those properties and call `render` to update
|
|
|
|
|
the view based on the dependency change.
|
|
|
|
|
|
|
|
|
|
For example, for an `dom-repeat` with a filter of the following:
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
|
isEngineer: function(item) {
|
|
|
|
|
return item.type == 'engineer' || item.manager.type == 'engineer';
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then the `observe` property should be configured as follows:
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
<template is="dom-repeat" items="{{employees}}"
|
|
|
|
|
filter="isEngineer" observe="type manager.type">
|
|
|
|
|
```
|
|
|
|
|
|
2016-04-27 10:01:40 -07:00
|
|
|
-->
|
|
|
|
|
|
2017-02-14 11:22:29 -08:00
|
|
|
<link rel="import" href="../../polymer-element.html">
|
2017-02-14 10:25:44 -08:00
|
|
|
<link rel="import" href="../lib/templatize.html">
|
|
|
|
|
<link rel="import" href="../lib/debounce.html">
|
|
|
|
|
<link rel="import" href="../lib/flush.html">
|
2016-04-27 10:01:40 -07:00
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function() {
|
2016-08-15 19:17:29 -07:00
|
|
|
'use strict';
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
class DomRepeat extends Polymer.Element {
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-27 13:23:29 -07:00
|
|
|
static get template() { return null; }
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-11-28 16:33:58 -08:00
|
|
|
static get config() {
|
2016-10-21 13:54:05 -07:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fired whenever DOM is added or removed by this template (by
|
|
|
|
|
* default, rendering occurs lazily). To force immediate rendering, call
|
|
|
|
|
* `render`.
|
|
|
|
|
*
|
|
|
|
|
* @event dom-change
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* An array containing items determining how many instances of the template
|
|
|
|
|
* to stamp and that that each template instance should bind to.
|
|
|
|
|
*/
|
|
|
|
|
items: {
|
|
|
|
|
type: Array
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The name of the variable to add to the binding scope for the array
|
|
|
|
|
* element associated with a given template instance.
|
|
|
|
|
*/
|
|
|
|
|
as: {
|
|
|
|
|
type: String,
|
|
|
|
|
value: 'item'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The name of the variable to add to the binding scope with the index
|
|
|
|
|
* for the inst. If `sort` is provided, the index will reflect the
|
|
|
|
|
* sorted order (rather than the original array order).
|
|
|
|
|
*/
|
|
|
|
|
indexAs: {
|
|
|
|
|
type: String,
|
|
|
|
|
value: 'index'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The name of the variable to add to the binding scope with the index
|
|
|
|
|
* for the inst. If `sort` is provided, the index will reflect the
|
|
|
|
|
* sorted order (rather than the original array order).
|
|
|
|
|
*/
|
|
|
|
|
itemsIndexAs: {
|
|
|
|
|
type: String,
|
|
|
|
|
value: 'itemsIndex'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A function that should determine the sort order of the items. This
|
|
|
|
|
* property should either be provided as a string, indicating a method
|
|
|
|
|
* name on the element's host, or else be an actual function. The
|
|
|
|
|
* function should match the sort function passed to `Array.sort`.
|
|
|
|
|
* Using a sort function has no effect on the underlying `items` array.
|
|
|
|
|
*/
|
|
|
|
|
sort: {
|
|
|
|
|
type: Function,
|
|
|
|
|
observer: '_sortChanged'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A function that can be used to filter items out of the view. This
|
|
|
|
|
* property should either be provided as a string, indicating a method
|
|
|
|
|
* name on the element's host, or else be an actual function. The
|
|
|
|
|
* function should match the sort function passed to `Array.filter`.
|
|
|
|
|
* Using a filter function has no effect on the underlying `items` array.
|
|
|
|
|
*/
|
|
|
|
|
filter: {
|
|
|
|
|
type: Function,
|
|
|
|
|
observer: '_filterChanged'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* When using a `filter` or `sort` function, the `observe` property
|
|
|
|
|
* should be set to a space-separated list of the names of item
|
|
|
|
|
* sub-fields that should trigger a re-sort or re-filter when changed.
|
|
|
|
|
* These should generally be fields of `item` that the sort or filter
|
|
|
|
|
* function depends on.
|
|
|
|
|
*/
|
|
|
|
|
observe: {
|
|
|
|
|
type: String,
|
|
|
|
|
observer: '_observeChanged'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* When using a `filter` or `sort` function, the `delay` property
|
|
|
|
|
* determines a debounce time after a change to observed item
|
|
|
|
|
* properties that must pass before the filter or sort is re-run.
|
|
|
|
|
* This is useful in rate-limiting shuffing of the view when
|
|
|
|
|
* item changes may be frequent.
|
|
|
|
|
*/
|
|
|
|
|
delay: Number,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Count of currently rendered items after `filter` (if any) has been applied.
|
|
|
|
|
* If "chunking mode" is enabled, `renderedItemCount` is updated each time a
|
|
|
|
|
* set of template instances is rendered.
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
renderedItemCount: {
|
|
|
|
|
type: Number,
|
|
|
|
|
notify: true,
|
|
|
|
|
readOnly: true
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Defines an initial count of template instances to render after setting
|
|
|
|
|
* the `items` array, before the next paint, and puts the `dom-repeat`
|
|
|
|
|
* into "chunking mode". The remaining items will be created and rendered
|
|
|
|
|
* incrementally at each animation frame therof until all instances have
|
|
|
|
|
* been rendered.
|
|
|
|
|
*/
|
|
|
|
|
initialCount: {
|
|
|
|
|
type: Number,
|
|
|
|
|
observer: '_initializeChunking'
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* When `initialCount` is used, this property defines a frame rate to
|
|
|
|
|
* target by throttling the number of instances rendered each frame to
|
|
|
|
|
* not exceed the budget for the target frame rate. Setting this to a
|
|
|
|
|
* higher number will allow lower latency and higher throughput for
|
|
|
|
|
* things like event handlers, but will result in a longer time for the
|
|
|
|
|
* remaining items to complete rendering.
|
|
|
|
|
*/
|
|
|
|
|
targetFramerate: {
|
|
|
|
|
type: Number,
|
|
|
|
|
value: 20
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_targetFrameTime: {
|
|
|
|
|
type: Number,
|
|
|
|
|
computed: '_computeFrameTime(targetFramerate)'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
observers: [
|
|
|
|
|
'_itemsChanged(items.*)'
|
|
|
|
|
]
|
2016-04-27 10:01:40 -07:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
constructor() {
|
|
|
|
|
super();
|
2016-04-27 10:01:40 -07:00
|
|
|
this._instances = [];
|
|
|
|
|
this._pool = [];
|
|
|
|
|
this._limit = Infinity;
|
2016-10-21 13:54:05 -07:00
|
|
|
this._renderDebouncer = null;
|
2016-12-06 15:39:15 -08:00
|
|
|
this._itemsIdxToInstIdx = {};
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
disconnectedCallback() {
|
|
|
|
|
super.disconnectedCallback();
|
2016-04-27 10:01:40 -07:00
|
|
|
this.__isDetached = true;
|
|
|
|
|
for (var i=0; i<this._instances.length; i++) {
|
|
|
|
|
this._detachInstance(i);
|
|
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
connectedCallback() {
|
|
|
|
|
super.connectedCallback();
|
2016-04-27 10:01:40 -07:00
|
|
|
// only perform attachment if the element was previously detached.
|
|
|
|
|
if (this.__isDetached) {
|
|
|
|
|
this.__isDetached = false;
|
2016-08-03 15:52:31 -07:00
|
|
|
var parent = this.parentNode;
|
2016-04-27 10:01:40 -07:00
|
|
|
for (var i=0; i<this._instances.length; i++) {
|
|
|
|
|
this._attachInstance(i, parent);
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2017-01-31 13:00:06 -08:00
|
|
|
_ensureTemplatized() {
|
2016-04-27 10:01:40 -07:00
|
|
|
// Templatizing (generating the instance constructor) needs to wait
|
|
|
|
|
// until ready, since won't have its template content handed back to
|
|
|
|
|
// it until then
|
|
|
|
|
if (!this._ctor) {
|
2016-08-15 19:17:29 -07:00
|
|
|
var template = this.template = this.querySelector('template');
|
2016-08-17 02:14:50 -07:00
|
|
|
if (!template) {
|
2017-01-30 21:14:07 -08:00
|
|
|
// // Wait until childList changes and template should be there by then
|
|
|
|
|
let observer = new MutationObserver(() => {
|
|
|
|
|
if (this.querySelector('template')) {
|
|
|
|
|
observer.disconnect();
|
|
|
|
|
this._render();
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('dom-repeat requires a <template> child');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
observer.observe(this, {childList: true});
|
|
|
|
|
return false;
|
2016-08-17 02:14:50 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
// Template instance props that should be excluded from forwarding
|
2016-09-14 18:52:47 -07:00
|
|
|
var instanceProps = {};
|
2016-04-27 10:01:40 -07:00
|
|
|
instanceProps[this.as] = true;
|
|
|
|
|
instanceProps[this.indexAs] = true;
|
2016-09-23 15:02:24 -07:00
|
|
|
instanceProps[this.itemsIndexAs] = true;
|
2017-02-08 18:11:35 -08:00
|
|
|
this._ctor = Polymer.Templatize.templatize(template, this, {
|
2016-04-27 10:01:40 -07:00
|
|
|
instanceProps: instanceProps,
|
2017-02-08 18:11:35 -08:00
|
|
|
forwardHostProp: function(prop, value) {
|
|
|
|
|
var i$ = this._instances;
|
2016-04-27 10:01:40 -07:00
|
|
|
for (var i=0, inst; (i<i$.length) && (inst=i$[i]); i++) {
|
2017-02-08 18:11:35 -08:00
|
|
|
inst.forwardHostProp(prop, value);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
|
|
|
|
},
|
2017-02-08 18:08:06 -08:00
|
|
|
notifyInstanceProp: function(inst, prop, value) {
|
2017-02-08 18:11:35 -08:00
|
|
|
if (Polymer.Path.matches(this.as, prop)) {
|
|
|
|
|
let idx = inst[this.itemsIndexAs];
|
|
|
|
|
if (prop == this.as) {
|
|
|
|
|
this.items[idx] = value;
|
2016-09-19 15:19:22 -07:00
|
|
|
}
|
2017-02-08 18:11:35 -08:00
|
|
|
let path = Polymer.Path.translate(this.as, 'items.' + idx, prop);
|
|
|
|
|
this.notifyPath(path, value);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2017-01-30 21:14:07 -08:00
|
|
|
return true;
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2017-02-09 12:33:13 -08:00
|
|
|
_getMethodHost() {
|
|
|
|
|
// Technically this should be the owner of the outermost template.
|
|
|
|
|
// In shadow dom, this is always getRootNode().host, but we can
|
|
|
|
|
// approximate this via cooperation with our dataHost always setting
|
|
|
|
|
// `_methodHost` as long as there were bindings (or id's) on this
|
|
|
|
|
// instance causing it to get a dataHost.
|
|
|
|
|
return this.__dataHost._methodHost || this.__dataHost;
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-08-12 19:34:11 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_sortChanged(sort) {
|
2017-02-09 12:33:13 -08:00
|
|
|
var methodHost = this._getMethodHost();
|
2016-04-27 10:01:40 -07:00
|
|
|
this._sortFn = sort && (typeof sort == 'function' ? sort :
|
2017-02-09 12:33:13 -08:00
|
|
|
function() { return methodHost[sort].apply(methodHost, arguments); });
|
2016-04-27 10:01:40 -07:00
|
|
|
this._needFullRefresh = true;
|
|
|
|
|
if (this.items) {
|
2016-10-21 13:54:05 -07:00
|
|
|
this._debounceRender(this._render);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_filterChanged(filter) {
|
2017-02-09 12:33:13 -08:00
|
|
|
var methodHost = this._getMethodHost();
|
2016-04-27 10:01:40 -07:00
|
|
|
this._filterFn = filter && (typeof filter == 'function' ? filter :
|
2017-02-09 12:33:13 -08:00
|
|
|
function() { return methodHost[filter].apply(methodHost, arguments); });
|
2016-04-27 10:01:40 -07:00
|
|
|
this._needFullRefresh = true;
|
|
|
|
|
if (this.items) {
|
2016-10-21 13:54:05 -07:00
|
|
|
this._debounceRender(this._render);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_computeFrameTime(rate) {
|
2016-04-27 10:01:40 -07:00
|
|
|
return Math.ceil(1000/rate);
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_initializeChunking() {
|
2016-04-27 10:01:40 -07:00
|
|
|
if (this.initialCount) {
|
|
|
|
|
this._limit = this.initialCount;
|
|
|
|
|
this._chunkCount = this.initialCount;
|
|
|
|
|
this._lastChunkTime = performance.now();
|
|
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_tryRenderChunk() {
|
2016-04-27 10:01:40 -07:00
|
|
|
// Debounced so that multiple calls through `_render` between animation
|
|
|
|
|
// frames only queue one new rAF (e.g. array mutation & chunked render)
|
|
|
|
|
if (this.items && this._limit < this.items.length) {
|
2016-10-21 13:54:05 -07:00
|
|
|
this._debounceRender(this._requestRenderChunk);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_requestRenderChunk() {
|
|
|
|
|
requestAnimationFrame(()=>this._renderChunk());
|
|
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_renderChunk() {
|
2016-04-27 10:01:40 -07:00
|
|
|
// Simple auto chunkSize throttling algorithm based on feedback loop:
|
|
|
|
|
// measure actual time between frames and scale chunk count by ratio
|
|
|
|
|
// of target/actual frame time
|
|
|
|
|
var currChunkTime = performance.now();
|
|
|
|
|
var ratio = this._targetFrameTime / (currChunkTime - this._lastChunkTime);
|
|
|
|
|
this._chunkCount = Math.round(this._chunkCount * ratio) || 1;
|
|
|
|
|
this._limit += this._chunkCount;
|
|
|
|
|
this._lastChunkTime = currChunkTime;
|
2016-10-21 13:54:05 -07:00
|
|
|
this._debounceRender(this._render);
|
|
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_observeChanged() {
|
2016-09-19 15:19:22 -07:00
|
|
|
this._observePaths = this.observe &&
|
|
|
|
|
this.observe.replace('.*', '.').split(' ');
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-09-19 15:19:22 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_itemsChanged(change) {
|
2016-09-14 18:52:47 -07:00
|
|
|
if (this.items && !Array.isArray(this.items)) {
|
|
|
|
|
console.warn('dom-repeat expected array for `items`, found', this.items);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
2016-09-23 15:18:34 -07:00
|
|
|
// If path was to an item (e.g. 'items.3' or 'items.3.foo'), forward the
|
|
|
|
|
// path to that instance synchronously (retuns false for non-item paths)
|
2016-12-19 11:53:53 -08:00
|
|
|
if (!this._handleItemPath(change.path, change.value)) {
|
2016-09-23 15:18:34 -07:00
|
|
|
// Otherwise, the array was reset ('items') or spliced ('items.splices'),
|
|
|
|
|
// so queue a full refresh
|
2016-09-19 15:19:22 -07:00
|
|
|
this._needFullRefresh = true;
|
|
|
|
|
this._initializeChunking();
|
2016-10-21 13:54:05 -07:00
|
|
|
this._debounceRender(this._render);
|
2016-09-19 15:19:22 -07:00
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-09-19 15:19:22 -07:00
|
|
|
|
2016-12-19 11:53:53 -08:00
|
|
|
_handleObservedPaths(path) {
|
2016-09-19 15:19:22 -07:00
|
|
|
if (this._observePaths) {
|
2016-09-23 15:26:19 -07:00
|
|
|
path = path.substring(path.indexOf('.') + 1);
|
2016-09-19 15:19:22 -07:00
|
|
|
var paths = this._observePaths;
|
|
|
|
|
for (var i=0; i<paths.length; i++) {
|
|
|
|
|
if (path.indexOf(paths[i]) === 0) {
|
|
|
|
|
this._needFullRefresh = true;
|
2016-10-21 13:54:05 -07:00
|
|
|
this._debounceRender(this._render, this.delay);
|
2016-09-23 16:01:03 -07:00
|
|
|
return true;
|
2016-09-19 15:19:22 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2017-01-12 15:55:12 -08:00
|
|
|
/**
|
2017-01-12 17:51:56 -08:00
|
|
|
* @param {function()} fn
|
2017-01-18 14:24:27 -08:00
|
|
|
* @param {number=} delay
|
2017-01-12 15:55:12 -08:00
|
|
|
*/
|
2016-10-21 13:54:05 -07:00
|
|
|
_debounceRender(fn, delay) {
|
2016-12-19 15:37:15 -08:00
|
|
|
this._renderDebouncer = Polymer.Debouncer.debounce(
|
|
|
|
|
this._renderDebouncer
|
|
|
|
|
, delay > 0 ? Polymer.Async.timeOut.after(delay) : Polymer.Async.microTask
|
|
|
|
|
, fn.bind(this));
|
2017-02-09 17:54:20 -08:00
|
|
|
Polymer.enqueueDebouncer(this._renderDebouncer);
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-08-12 19:34:11 -07:00
|
|
|
|
2016-04-27 10:01:40 -07:00
|
|
|
/**
|
|
|
|
|
* Forces the element to render its content. Normally rendering is
|
|
|
|
|
* asynchronous to a provoking change. This is done for efficiency so
|
|
|
|
|
* that multiple changes trigger only a single render. The render method
|
|
|
|
|
* should be called if, for example, template rendering is required to
|
|
|
|
|
* validate application state.
|
|
|
|
|
*/
|
2016-10-21 13:54:05 -07:00
|
|
|
render() {
|
2016-04-27 10:01:40 -07:00
|
|
|
// Queue this repeater, then flush all in order
|
|
|
|
|
this._needFullRefresh = true;
|
2016-10-21 13:54:05 -07:00
|
|
|
this._debounceRender(this._render);
|
2017-02-09 17:54:20 -08:00
|
|
|
Polymer.flush();
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_render() {
|
2017-01-31 13:00:06 -08:00
|
|
|
if (!this._ensureTemplatized()) {
|
|
|
|
|
// No template found yet
|
2017-01-30 21:14:07 -08:00
|
|
|
return;
|
|
|
|
|
}
|
2016-09-14 18:52:47 -07:00
|
|
|
this._applyFullRefresh();
|
2016-04-27 10:01:40 -07:00
|
|
|
// Reset the pool
|
|
|
|
|
// TODO(kschaaf): Reuse pool across turns and nested templates
|
2017-02-08 18:11:35 -08:00
|
|
|
// Now that objects/arrays are re-evaluated when set, we can safely
|
|
|
|
|
// reuse pooled instances across turns, however we still need to decide
|
|
|
|
|
// semantics regarding how long to hold, how many to hold, etc.
|
2016-04-27 10:01:40 -07:00
|
|
|
this._pool.length = 0;
|
|
|
|
|
// Set rendered item count
|
|
|
|
|
this._setRenderedItemCount(this._instances.length);
|
|
|
|
|
// Notify users
|
2016-10-21 13:54:05 -07:00
|
|
|
this.dispatchEvent(new CustomEvent('dom-change', {bubbles: true}));
|
2016-04-27 10:01:40 -07:00
|
|
|
// Check to see if we need to render more items
|
|
|
|
|
this._tryRenderChunk();
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_applyFullRefresh() {
|
2016-09-14 18:52:47 -07:00
|
|
|
const items = this.items || [];
|
2016-09-23 16:01:03 -07:00
|
|
|
let isntIdxToItemsIdx = new Array(items.length);
|
2016-09-14 18:52:47 -07:00
|
|
|
for (let i=0; i<items.length; i++) {
|
2016-09-23 16:01:03 -07:00
|
|
|
isntIdxToItemsIdx[i] = i;
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
2016-09-14 18:52:47 -07:00
|
|
|
// Apply user filter
|
2016-04-27 10:01:40 -07:00
|
|
|
if (this._filterFn) {
|
2016-09-23 16:01:03 -07:00
|
|
|
isntIdxToItemsIdx = isntIdxToItemsIdx.filter((i, idx, array) =>
|
2016-09-23 09:25:11 -07:00
|
|
|
this._filterFn(items[i], idx, array));
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
2016-09-14 18:52:47 -07:00
|
|
|
// Apply user sort
|
2016-04-27 10:01:40 -07:00
|
|
|
if (this._sortFn) {
|
2016-09-23 16:01:03 -07:00
|
|
|
isntIdxToItemsIdx.sort((a, b) => this._sortFn(items[a], items[b]));
|
2016-09-14 18:52:47 -07:00
|
|
|
}
|
2016-09-23 15:18:34 -07:00
|
|
|
// items->inst map kept for item path forwarding
|
2016-09-23 16:01:03 -07:00
|
|
|
const itemsIdxToInstIdx = this._itemsIdxToInstIdx = {};
|
2016-09-14 18:52:47 -07:00
|
|
|
let instIdx = 0;
|
2016-09-23 15:18:34 -07:00
|
|
|
// Generate instances and assign items
|
2016-09-23 16:01:03 -07:00
|
|
|
const limit = Math.min(isntIdxToItemsIdx.length, this._limit);
|
2016-09-14 18:52:47 -07:00
|
|
|
for (; instIdx<limit; instIdx++) {
|
|
|
|
|
let inst = this._instances[instIdx];
|
2016-09-23 16:01:03 -07:00
|
|
|
let itemIdx = isntIdxToItemsIdx[instIdx];
|
2016-09-14 18:52:47 -07:00
|
|
|
let item = items[itemIdx];
|
2016-09-23 16:01:03 -07:00
|
|
|
itemsIdxToInstIdx[itemIdx] = instIdx;
|
2016-09-14 18:52:47 -07:00
|
|
|
if (inst && instIdx < this._limit) {
|
2017-02-08 18:11:35 -08:00
|
|
|
inst._setPendingProperty(this.as, item);
|
|
|
|
|
inst._setPendingProperty(this.indexAs, instIdx);
|
|
|
|
|
inst._setPendingProperty(this.itemsIndexAs, itemIdx);
|
|
|
|
|
inst._flushProperties(true);
|
2016-04-27 10:01:40 -07:00
|
|
|
} else {
|
2016-09-14 18:52:47 -07:00
|
|
|
this._insertInstance(item, instIdx, itemIdx);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Remove any extra instances from previous state
|
2016-09-14 18:52:47 -07:00
|
|
|
for (let i=this._instances.length-1; i>=instIdx; i--) {
|
|
|
|
|
this._detachAndRemoveInstance(i);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_detachInstance(idx) {
|
2016-04-27 10:01:40 -07:00
|
|
|
var inst = this._instances[idx];
|
2017-02-15 17:12:20 -08:00
|
|
|
for (var i=0; i<inst.children.length; i++) {
|
|
|
|
|
var el = inst.children[i];
|
|
|
|
|
inst.root.appendChild(el);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
2016-09-14 18:52:47 -07:00
|
|
|
return inst;
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_attachInstance(idx, parent) {
|
2016-04-27 10:01:40 -07:00
|
|
|
var inst = this._instances[idx];
|
2016-09-14 18:52:47 -07:00
|
|
|
parent.insertBefore(inst.root, this);
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_detachAndRemoveInstance(idx) {
|
2016-04-27 10:01:40 -07:00
|
|
|
var inst = this._detachInstance(idx);
|
|
|
|
|
if (inst) {
|
|
|
|
|
this._pool.push(inst);
|
|
|
|
|
}
|
|
|
|
|
this._instances.splice(idx, 1);
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_stampInstance(item, instIdx, itemIdx) {
|
2016-09-14 18:52:47 -07:00
|
|
|
var model = {};
|
|
|
|
|
model[this.as] = item;
|
|
|
|
|
model[this.indexAs] = instIdx;
|
2016-09-23 15:02:24 -07:00
|
|
|
model[this.itemsIndexAs] = itemIdx;
|
2017-02-08 18:11:35 -08:00
|
|
|
return new this._ctor(model);
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
_insertInstance(item, instIdx, itemIdx) {
|
2016-04-27 10:01:40 -07:00
|
|
|
var inst = this._pool.pop();
|
|
|
|
|
if (inst) {
|
|
|
|
|
// TODO(kschaaf): If the pool is shared across turns, hostProps
|
2016-09-14 18:52:47 -07:00
|
|
|
// need to be re-set to reused instances in addition to item
|
2017-02-08 18:11:35 -08:00
|
|
|
inst._setPendingProperty(this.as, item);
|
|
|
|
|
inst._setPendingProperty(this.indexAs, instIdx);
|
|
|
|
|
inst._setPendingProperty(this.itemsIndexAs, itemIdx);
|
|
|
|
|
inst._flushProperties(true);
|
2016-04-27 10:01:40 -07:00
|
|
|
} else {
|
2016-09-14 18:52:47 -07:00
|
|
|
inst = this._stampInstance(item, instIdx, itemIdx);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
2016-09-14 18:52:47 -07:00
|
|
|
var beforeRow = this._instances[instIdx + 1];
|
|
|
|
|
var beforeNode = beforeRow ? beforeRow.children[0] : this;
|
2016-08-03 15:52:31 -07:00
|
|
|
this.parentNode.insertBefore(inst.root, beforeNode);
|
2016-09-14 18:52:47 -07:00
|
|
|
this._instances[instIdx] = inst;
|
2016-04-27 10:01:40 -07:00
|
|
|
return inst;
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2017-01-26 11:28:31 -08:00
|
|
|
// Implements extension point from Templatize mixin
|
2016-10-21 13:54:05 -07:00
|
|
|
_showHideChildren(hidden) {
|
2016-04-27 10:01:40 -07:00
|
|
|
for (var i=0; i<this._instances.length; i++) {
|
|
|
|
|
this._instances[i]._showHideChildren(hidden);
|
|
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
2016-09-19 15:19:22 -07:00
|
|
|
// Called as a side effect of a host items.<key>.<path> path change,
|
|
|
|
|
// responsible for notifying item.<path> changes to inst for key
|
2016-12-19 11:53:53 -08:00
|
|
|
_handleItemPath(path, value) {
|
2016-12-06 15:39:15 -08:00
|
|
|
var itemsPath = path.slice(6); // 'items.'.length == 6
|
|
|
|
|
var dot = itemsPath.indexOf('.');
|
|
|
|
|
var itemsIdx = dot < 0 ? itemsPath : itemsPath.substring(0, dot);
|
2016-12-08 12:27:56 -08:00
|
|
|
// If path was index into array...
|
2017-01-12 15:55:12 -08:00
|
|
|
if (itemsIdx == parseInt(itemsIdx, 10)) {
|
2017-02-09 11:15:57 -08:00
|
|
|
var itemSubPath = dot < 0 ? '' : itemsPath.substring(dot+1);
|
2016-12-08 12:27:56 -08:00
|
|
|
// See if the item subpath should trigger a full refresh...
|
2017-02-09 11:15:57 -08:00
|
|
|
if (!this._handleObservedPaths(itemSubPath)) {
|
2016-12-08 12:27:56 -08:00
|
|
|
// If not, forward to the instance for that index
|
2016-12-06 15:39:15 -08:00
|
|
|
var instIdx = this._itemsIdxToInstIdx[itemsIdx];
|
|
|
|
|
var inst = this._instances[instIdx];
|
|
|
|
|
if (inst) {
|
2017-02-09 11:15:57 -08:00
|
|
|
let itemPath = this.as + (itemSubPath ? '.' + itemSubPath : '');
|
|
|
|
|
// This is effectively `notifyPath`, but avoids some of the overhead
|
|
|
|
|
// of the public API
|
|
|
|
|
inst._setPendingPropertyOrPath(itemPath, value, true);
|
2017-02-08 18:11:35 -08:00
|
|
|
inst._flushProperties(true);
|
2016-11-28 16:33:58 -08:00
|
|
|
}
|
2016-09-19 15:19:22 -07:00
|
|
|
}
|
2016-12-06 15:39:15 -08:00
|
|
|
return true;
|
2016-09-19 15:19:22 -07:00
|
|
|
}
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-09-19 15:19:22 -07:00
|
|
|
|
2016-04-27 10:01:40 -07:00
|
|
|
/**
|
|
|
|
|
* Returns the item associated with a given element stamped by
|
|
|
|
|
* this `dom-repeat`.
|
|
|
|
|
*
|
|
|
|
|
* Note, to modify sub-properties of the item,
|
|
|
|
|
* `modelForElement(el).set('item.<sub-prop>', value)`
|
|
|
|
|
* should be used.
|
|
|
|
|
*
|
|
|
|
|
* @method itemForElement
|
|
|
|
|
* @param {HTMLElement} el Element for which to return the item.
|
2017-01-12 15:55:12 -08:00
|
|
|
* @return {*} Item associated with the element.
|
2016-04-27 10:01:40 -07:00
|
|
|
*/
|
2016-10-21 13:54:05 -07:00
|
|
|
itemForElement(el) {
|
2016-04-27 10:01:40 -07:00
|
|
|
var instance = this.modelForElement(el);
|
|
|
|
|
return instance && instance[this.as];
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-04-27 10:01:40 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the inst index for a given element stamped by this `dom-repeat`.
|
|
|
|
|
* If `sort` is provided, the index will reflect the sorted order (rather
|
|
|
|
|
* than the original array order).
|
|
|
|
|
*
|
|
|
|
|
* @method indexForElement
|
|
|
|
|
* @param {HTMLElement} el Element for which to return the index.
|
2017-01-12 15:55:12 -08:00
|
|
|
* @return {*} Row index associated with the element (note this may
|
2016-04-27 10:01:40 -07:00
|
|
|
* not correspond to the array index if a user `sort` is applied).
|
|
|
|
|
*/
|
2016-10-21 13:54:05 -07:00
|
|
|
indexForElement(el) {
|
2016-04-27 10:01:40 -07:00
|
|
|
var instance = this.modelForElement(el);
|
|
|
|
|
return instance && instance[this.indexAs];
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
2016-08-15 19:17:29 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the template "model" associated with a given element, which
|
|
|
|
|
* serves as the binding scope for the template instance the element is
|
|
|
|
|
* contained in. A template model is an instance of `Polymer.Base`, and
|
|
|
|
|
* should be used to manipulate data associated with this template instance.
|
|
|
|
|
*
|
|
|
|
|
* Example:
|
|
|
|
|
*
|
|
|
|
|
* var model = modelForElement(el);
|
|
|
|
|
* if (model.index < 10) {
|
|
|
|
|
* model.set('item.checked', true);
|
|
|
|
|
* }
|
|
|
|
|
*
|
|
|
|
|
* @method modelForElement
|
|
|
|
|
* @param {HTMLElement} el Element for which to return a template model.
|
2017-02-15 17:12:20 -08:00
|
|
|
* @return {TemplateInstanceBase} Model representing the binding scope for
|
2016-08-15 19:17:29 -07:00
|
|
|
* the element.
|
|
|
|
|
*/
|
2016-10-21 13:54:05 -07:00
|
|
|
modelForElement(el) {
|
2017-01-26 11:28:31 -08:00
|
|
|
return Polymer.Templatize.modelForElement(this.template, el);
|
2016-04-27 10:01:40 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-21 13:54:05 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define('dom-repeat', DomRepeat);
|
|
|
|
|
|
|
|
|
|
Polymer.DomRepeat = DomRepeat;
|
2016-04-27 10:01:40 -07:00
|
|
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
</script>
|