mirror of
https://github.com/Polymer/polymer.git
synced 2025-02-25 18:55:30 -06:00
Merge pull request #2659 from Polymer/2537-kschaaf-chunked-repeat
Adds incremental rendering to dom-repeat. Fixes #2537.
This commit is contained in:
commit
b137fe70dd
@ -188,7 +188,37 @@ Then the `observe` property should be configured as follows:
|
||||
* This is useful in rate-limiting shuffing of the view when
|
||||
* item changes may be frequent.
|
||||
*/
|
||||
delay: Number
|
||||
delay: Number,
|
||||
|
||||
/**
|
||||
* 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: {
|
||||
computed: '_computeFrameTime(targetFramerate)'
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
behaviors: [
|
||||
@ -201,18 +231,24 @@ Then the `observe` property should be configured as follows:
|
||||
|
||||
created: function() {
|
||||
this._instances = [];
|
||||
this._pool = [];
|
||||
this._limit = Infinity;
|
||||
var self = this;
|
||||
this._boundRenderChunk = function() {
|
||||
self._renderChunk();
|
||||
};
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
for (var i=0; i<this._instances.length; i++) {
|
||||
this._detachRow(i);
|
||||
this._detachInstance(i);
|
||||
}
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
var parentNode = Polymer.dom(this).parentNode;
|
||||
var parent = Polymer.dom(Polymer.dom(this).parentNode);
|
||||
for (var i=0; i<this._instances.length; i++) {
|
||||
Polymer.dom(parentNode).insertBefore(this._instances[i].root, this);
|
||||
this._attachInstance(i, parent);
|
||||
}
|
||||
},
|
||||
|
||||
@ -231,9 +267,8 @@ Then the `observe` property should be configured as follows:
|
||||
}
|
||||
},
|
||||
|
||||
_sortChanged: function() {
|
||||
_sortChanged: function(sort) {
|
||||
var dataHost = this._getRootDataHost();
|
||||
var sort = this.sort;
|
||||
this._sortFn = sort && (typeof sort == 'function' ? sort :
|
||||
function() { return dataHost[sort].apply(dataHost, arguments); });
|
||||
this._needFullRefresh = true;
|
||||
@ -242,9 +277,8 @@ Then the `observe` property should be configured as follows:
|
||||
}
|
||||
},
|
||||
|
||||
_filterChanged: function() {
|
||||
_filterChanged: function(filter) {
|
||||
var dataHost = this._getRootDataHost();
|
||||
var filter = this.filter;
|
||||
this._filterFn = filter && (typeof filter == 'function' ? filter :
|
||||
function() { return dataHost[filter].apply(dataHost, arguments); });
|
||||
this._needFullRefresh = true;
|
||||
@ -253,6 +287,42 @@ Then the `observe` property should be configured as follows:
|
||||
}
|
||||
},
|
||||
|
||||
_computeFrameTime: function(rate) {
|
||||
return Math.ceil(1000/rate);
|
||||
},
|
||||
|
||||
_initializeChunking: function() {
|
||||
if (this.initialCount) {
|
||||
this._limit = this.initialCount;
|
||||
this._chunkCount = this.initialCount;
|
||||
this._lastChunkTime = performance.now();
|
||||
}
|
||||
},
|
||||
|
||||
_tryRenderChunk: function() {
|
||||
// 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) {
|
||||
this.debounce('renderChunk', this._requestRenderChunk);
|
||||
}
|
||||
},
|
||||
|
||||
_requestRenderChunk: function() {
|
||||
requestAnimationFrame(this._boundRenderChunk);
|
||||
},
|
||||
|
||||
_renderChunk: function() {
|
||||
// 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;
|
||||
this._debounceTemplate(this._render);
|
||||
},
|
||||
|
||||
_observeChanged: function() {
|
||||
this._observePaths = this.observe &&
|
||||
this.observe.replace('.*', '.').split(' ');
|
||||
@ -271,6 +341,7 @@ Then the `observe` property should be configured as follows:
|
||||
this._keySplices = [];
|
||||
this._indexSplices = [];
|
||||
this._needFullRefresh = true;
|
||||
this._initializeChunking();
|
||||
this._debounceTemplate(this._render);
|
||||
} else if (change.path == 'items.splices') {
|
||||
this._keySplices = this._keySplices.concat(change.value.keySplices);
|
||||
@ -322,9 +393,11 @@ Then the `observe` property should be configured as follows:
|
||||
var c = this.collection;
|
||||
// Choose rendering path: full vs. incremental using splices
|
||||
if (this._needFullRefresh) {
|
||||
// Full refresh when items, sort, or filter change, or when render() called
|
||||
this._applyFullRefresh();
|
||||
this._needFullRefresh = false;
|
||||
} else {
|
||||
} else if (this._keySplices.length) {
|
||||
// Incremental refresh when splices were queued
|
||||
if (this._sortFn) {
|
||||
this._applySplicesUserSort(this._keySplices);
|
||||
} else {
|
||||
@ -335,17 +408,39 @@ Then the `observe` property should be configured as follows:
|
||||
this._applySplicesArrayOrder(this._indexSplices);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise only limit changed; no change to instances, just need to
|
||||
// upgrade more placeholders to instances
|
||||
}
|
||||
this._keySplices = [];
|
||||
this._indexSplices = [];
|
||||
// Update final _keyToInstIdx and instance indices
|
||||
// Update final _keyToInstIdx and instance indices, and
|
||||
// upgrade/downgrade placeholders
|
||||
var keyToIdx = this._keyToInstIdx = {};
|
||||
for (var i=0; i<this._instances.length; i++) {
|
||||
for (var i=this._instances.length-1; i>=0; i--) {
|
||||
var inst = this._instances[i];
|
||||
if (inst.isPlaceholder && i<this._limit) {
|
||||
inst = this._insertInstance(i, inst.__key__);
|
||||
} else if (!inst.isPlaceholder && i>=this._limit) {
|
||||
inst = this._downgradeInstance(i, inst.__key__);
|
||||
}
|
||||
keyToIdx[inst.__key__] = i;
|
||||
inst.__setProperty(this.indexAs, i, true);
|
||||
if (!inst.isPlaceholder) {
|
||||
inst.__setProperty(this.indexAs, i, true);
|
||||
}
|
||||
}
|
||||
// Reset the pool
|
||||
// TODO(kschaaf): Reuse pool across turns and nested templates
|
||||
// Requires updating parentProps and dealing with the fact that path
|
||||
// notifications won't reach instances sitting in the pool, which
|
||||
// could result in out-of-sync instances since simply re-setting
|
||||
// `item` may not be sufficient if the pooled instance happens to be
|
||||
// the same item.
|
||||
this._pool.length = 0;
|
||||
// Notify users
|
||||
this.fire('dom-change');
|
||||
// Check to see if we need to render more items
|
||||
this._tryRenderChunk();
|
||||
},
|
||||
|
||||
// Render method 1: full refesh
|
||||
@ -385,17 +480,20 @@ Then the `observe` property should be configured as follows:
|
||||
var key = keys[i];
|
||||
var inst = this._instances[i];
|
||||
if (inst) {
|
||||
inst.__setProperty('__key__', key, true);
|
||||
inst.__setProperty(this.as, c.getItem(key), true);
|
||||
inst.__key__ = key;
|
||||
if (!inst.isPlaceholder && i < this._limit) {
|
||||
inst.__setProperty(this.as, c.getItem(key), true);
|
||||
}
|
||||
} else if (i < this._limit) {
|
||||
this._insertInstance(i, key);
|
||||
} else {
|
||||
this._instances.push(this._insertRow(i, key));
|
||||
this._insertPlaceholder(i, key);
|
||||
}
|
||||
}
|
||||
// Remove any extra instances from previous state
|
||||
for (; i<this._instances.length; i++) {
|
||||
this._detachRow(i);
|
||||
for (var j=this._instances.length-1; j>=i; j--) {
|
||||
this._detachAndRemoveInstance(j);
|
||||
}
|
||||
this._instances.splice(keys.length, this._instances.length-keys.length);
|
||||
},
|
||||
|
||||
_keySort: function(a, b) {
|
||||
@ -414,7 +512,6 @@ Then the `observe` property should be configured as follows:
|
||||
var c = this.collection;
|
||||
var instances = this._instances;
|
||||
var keyMap = {};
|
||||
var pool = [];
|
||||
var sortFn = this._sortFn || this._keySort.bind(this);
|
||||
// Dedupe added and removed keys to a final added/removed map
|
||||
splices.forEach(function(s) {
|
||||
@ -448,8 +545,7 @@ Then the `observe` property should be configured as follows:
|
||||
var idx = removedIdxs[i];
|
||||
// Removed idx may be undefined if item was previously filtered out
|
||||
if (idx !== undefined) {
|
||||
pool.push(this._detachRow(idx));
|
||||
instances.splice(idx, 1);
|
||||
this._detachAndRemoveInstance(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -468,12 +564,12 @@ Then the `observe` property should be configured as follows:
|
||||
// Insertion-sort new instances into place (from pool or newly created)
|
||||
var start = 0;
|
||||
for (var i=0; i<addedKeys.length; i++) {
|
||||
start = this._insertRowUserSort(start, addedKeys[i], pool);
|
||||
start = this._insertRowUserSort(start, addedKeys[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_insertRowUserSort: function(start, key, pool) {
|
||||
_insertRowUserSort: function(start, key) {
|
||||
var c = this.collection;
|
||||
var item = c.getItem(key);
|
||||
var end = this._instances.length - 1;
|
||||
@ -497,7 +593,7 @@ Then the `observe` property should be configured as follows:
|
||||
idx = end + 1;
|
||||
}
|
||||
// Insert instance at insertion point
|
||||
this._instances.splice(idx, 0, this._insertRow(idx, key, pool));
|
||||
this._insertPlaceholder(idx, key);
|
||||
return idx;
|
||||
},
|
||||
|
||||
@ -507,70 +603,88 @@ Then the `observe` property should be configured as follows:
|
||||
// rows are as placeholders, and placeholders are updated to
|
||||
// actual rows at the end to take full advantage of removed rows
|
||||
_applySplicesArrayOrder: function(splices) {
|
||||
var pool = [];
|
||||
var c = this.collection;
|
||||
splices.forEach(function(s) {
|
||||
// Detach & pool removed instances
|
||||
for (var i=0; i<s.removed.length; i++) {
|
||||
var inst = this._detachRow(s.index + i);
|
||||
if (!inst.isPlaceholder) {
|
||||
pool.push(inst);
|
||||
}
|
||||
this._detachAndRemoveInstance(s.index);
|
||||
}
|
||||
this._instances.splice(s.index, s.removed.length);
|
||||
// Insert placeholders for new rows
|
||||
for (var i=0; i<s.addedKeys.length; i++) {
|
||||
var inst = {
|
||||
isPlaceholder: true,
|
||||
key: s.addedKeys[i]
|
||||
};
|
||||
this._instances.splice(s.index + i, 0, inst);
|
||||
this._insertPlaceholder(s.index+i, s.addedKeys[i]);
|
||||
}
|
||||
}, this);
|
||||
// Replace placeholders with actual instances (from pool or newly created)
|
||||
// Iterate backwards to ensure insertBefore refrence is never a placeholder
|
||||
for (var i=this._instances.length-1; i>=0; i--) {
|
||||
var inst = this._instances[i];
|
||||
if (inst.isPlaceholder) {
|
||||
this._instances[i] = this._insertRow(i, inst.key, pool, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_detachRow: function(idx) {
|
||||
_detachInstance: function(idx) {
|
||||
var inst = this._instances[idx];
|
||||
if (!inst.isPlaceholder) {
|
||||
var parentNode = Polymer.dom(this).parentNode;
|
||||
for (var i=0; i<inst._children.length; i++) {
|
||||
var el = inst._children[i];
|
||||
Polymer.dom(inst.root).appendChild(el);
|
||||
}
|
||||
return inst;
|
||||
}
|
||||
return inst;
|
||||
},
|
||||
|
||||
_insertRow: function(idx, key, pool, replace) {
|
||||
var inst;
|
||||
if (inst = pool && pool.pop()) {
|
||||
inst.__setProperty(this.as, this.collection.getItem(key), true);
|
||||
inst.__setProperty('__key__', key, true);
|
||||
} else {
|
||||
inst = this._generateRow(idx, key);
|
||||
_attachInstance: function(idx, parent) {
|
||||
var inst = this._instances[idx];
|
||||
if (!inst.isPlaceholder) {
|
||||
parent.insertBefore(inst.root, this);
|
||||
}
|
||||
var beforeRow = this._instances[replace ? idx + 1 : idx];
|
||||
var beforeNode = beforeRow ? beforeRow._children[0] : this;
|
||||
var parentNode = Polymer.dom(this).parentNode;
|
||||
Polymer.dom(parentNode).insertBefore(inst.root, beforeNode);
|
||||
return inst;
|
||||
},
|
||||
|
||||
_generateRow: function(idx, key) {
|
||||
_detachAndRemoveInstance: function(idx) {
|
||||
var inst = this._detachInstance(idx);
|
||||
if (inst) {
|
||||
this._pool.push(inst);
|
||||
}
|
||||
this._instances.splice(idx, 1);
|
||||
},
|
||||
|
||||
_insertPlaceholder: function(idx, key) {
|
||||
this._instances.splice(idx, 0, {
|
||||
isPlaceholder: true,
|
||||
__key__: key
|
||||
});
|
||||
},
|
||||
|
||||
_stampInstance: function(idx, key) {
|
||||
var model = {
|
||||
__key__: key
|
||||
};
|
||||
model[this.as] = this.collection.getItem(key);
|
||||
model[this.indexAs] = idx;
|
||||
var inst = this.stamp(model);
|
||||
return this.stamp(model);
|
||||
},
|
||||
|
||||
_insertInstance: function(idx, key) {
|
||||
var inst = this._pool.pop();
|
||||
if (inst) {
|
||||
// TODO(kschaaf): If the pool is shared across turns, parentProps
|
||||
// need to be re-set to reused instances in addition to item/key
|
||||
inst.__setProperty(this.as, this.collection.getItem(key), true);
|
||||
inst.__setProperty('__key__', key, true);
|
||||
} else {
|
||||
inst = this._stampInstance(idx, key);
|
||||
}
|
||||
var beforeRow = this._instances[idx + 1];
|
||||
var beforeNode = beforeRow && !beforeRow.isPlaceholder ? beforeRow._children[0] : this;
|
||||
var parentNode = Polymer.dom(this).parentNode;
|
||||
Polymer.dom(parentNode).insertBefore(inst.root, beforeNode);
|
||||
this._instances[idx] = inst;
|
||||
return inst;
|
||||
},
|
||||
|
||||
_downgradeInstance: function(idx, key) {
|
||||
var inst = this._detachInstance(idx);
|
||||
if (inst) {
|
||||
this._pool.push(inst);
|
||||
}
|
||||
inst = {
|
||||
isPlaceholder: true,
|
||||
__key__: key
|
||||
};
|
||||
this._instances[idx] = inst;
|
||||
return inst;
|
||||
},
|
||||
|
||||
@ -614,18 +728,24 @@ Then the `observe` property should be configured as follows:
|
||||
// Called as side-effect of a host property change, responsible for
|
||||
// notifying parent path change on each inst
|
||||
_forwardParentProp: function(prop, value) {
|
||||
this._instances.forEach(function(inst) {
|
||||
inst.__setProperty(prop, value, true);
|
||||
}, this);
|
||||
for (var i=0, i$=this._instances, il=i$.length; i<il; i++) {
|
||||
var inst = i$[i];
|
||||
if (!inst.isPlaceholder) {
|
||||
inst.__setProperty(prop, value, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Implements extension point from Templatizer
|
||||
// Called as side-effect of a host path change, responsible for
|
||||
// notifying parent path change on each inst
|
||||
_forwardParentPath: function(path, value) {
|
||||
this._instances.forEach(function(inst) {
|
||||
inst._notifyPath(path, value, true);
|
||||
}, this);
|
||||
for (var i=0, i$=this._instances, il=i$.length; i<il; i++) {
|
||||
var inst = i$[i];
|
||||
if (!inst.isPlaceholder) {
|
||||
inst._notifyPath(path, value, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Called as a side effect of a host items.<key>.<path> path change,
|
||||
@ -636,7 +756,7 @@ Then the `observe` property should be configured as follows:
|
||||
var key = path.substring(0, dot < 0 ? path.length : dot);
|
||||
var idx = this._keyToInstIdx[key];
|
||||
var inst = this._instances[idx];
|
||||
if (inst) {
|
||||
if (inst && !inst.isPlaceholder) {
|
||||
if (dot >= 0) {
|
||||
path = this.as + '.' + path.substring(dot+1);
|
||||
inst._notifyPath(path, value, true);
|
||||
|
@ -411,3 +411,64 @@ window.data = [
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
||||
|
||||
<dom-module id="x-repeat-limit">
|
||||
<template>
|
||||
<template id="repeater" is="dom-repeat" items="{{items}}">
|
||||
<div prop="{{outerProp.prop}}">{{item.prop}}</div>
|
||||
</template>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'x-repeat-limit',
|
||||
properties: {
|
||||
preppedItems: {
|
||||
value: function() {
|
||||
var ar = [];
|
||||
for (var i = 0; i < 20; i++) {
|
||||
ar.push({prop: i});
|
||||
}
|
||||
return ar;
|
||||
}
|
||||
},
|
||||
outerProp: {
|
||||
value: function() {
|
||||
return {prop: 'outer'};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
||||
|
||||
<dom-module id="x-repeat-chunked">
|
||||
<template>
|
||||
<template id="repeater" is="dom-repeat" items="{{items}}" initial-count="10">
|
||||
<x-wait>{{item.prop}}</x-wait>
|
||||
</template>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'x-repeat-chunked',
|
||||
properties: {
|
||||
preppedItems: {
|
||||
value: function() {
|
||||
var ar = [];
|
||||
for (var i = 0; i < 100; i++) {
|
||||
ar.push({prop: i});
|
||||
}
|
||||
return ar;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Polymer({
|
||||
is: 'x-wait',
|
||||
created: function() {
|
||||
var time = performance.now();
|
||||
time += 4;
|
||||
while (performance.now() < time) {}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
||||
|
@ -71,6 +71,12 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
||||
<h4>x-primitive-large</h4>
|
||||
<x-primitive-large id="primitiveLarge"></x-primitive-large>
|
||||
|
||||
<h4>x-repeat-limit</h4>
|
||||
<x-repeat-limit id="limited"></x-repeat-limit>
|
||||
|
||||
<h4>x-repeat-chunked</h4>
|
||||
<x-repeat-chunked id="chunked"></x-repeat-chunked>
|
||||
|
||||
<div id="inDocumentContainer">
|
||||
</div>
|
||||
|
||||
@ -93,6 +99,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
||||
stamped[38] .. 3-3-3
|
||||
*/
|
||||
|
||||
|
||||
suite('errors', function() {
|
||||
|
||||
test('items must be array', function() {
|
||||
@ -3348,6 +3355,487 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
|
||||
|
||||
});
|
||||
|
||||
|
||||
suite('limit', function() {
|
||||
|
||||
var checkItemOrder = function(stamped) {
|
||||
for (var i=0; i<stamped.length; i++) {
|
||||
assert.equal(parseInt(stamped[i].textContent), i);
|
||||
}
|
||||
};
|
||||
|
||||
test('initial limit', function() {
|
||||
limited.items = limited.preppedItems;
|
||||
limited.$.repeater._limit = 2;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 2);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('change item paths in & out of limit', function() {
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
limited.outerProp = {prop: 'changed'};
|
||||
assert.equal(stamped[0].prop, 'changed');
|
||||
limited.set('items.0.prop', '0-changed');
|
||||
limited.set('items.3.prop', '3-changed');
|
||||
assert.equal(stamped[0].textContent, '0-changed');
|
||||
limited.set('outerProp.prop', 'changed again');
|
||||
assert.equal(stamped[0].prop, 'changed again');
|
||||
});
|
||||
|
||||
test('increase limit', function() {
|
||||
// Increase limit
|
||||
limited.$.repeater._limit = 10;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 10);
|
||||
checkItemOrder(stamped);
|
||||
assert.equal(stamped[3].prop, 'changed again');
|
||||
assert.equal(stamped[3].textContent, '3-changed');
|
||||
limited.set('items.0.prop', 0);
|
||||
limited.set('items.3.prop', 3);
|
||||
// Increase limit
|
||||
limited.$.repeater._limit = 20;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 20);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('increase limit above items.length', function() {
|
||||
limited.$.repeater._limit = 30;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 20);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('decrease limit', function() {
|
||||
// Decrease limit
|
||||
limited.$.repeater._limit = 15;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 15);
|
||||
checkItemOrder(stamped);
|
||||
// Decrease limit
|
||||
limited.$.repeater._limit = 0;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 0);
|
||||
});
|
||||
|
||||
test('negative limit', function() {
|
||||
limited.$.repeater._limit = -10;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('limit with sort', function() {
|
||||
|
||||
var checkItemOrder = function(stamped) {
|
||||
for (var i=0; i<stamped.length; i++) {
|
||||
assert.equal(stamped[i].textContent, 19 - i);
|
||||
}
|
||||
};
|
||||
|
||||
test('initial limit', function() {
|
||||
limited.$.repeater._limit = 2;
|
||||
limited.$.repeater.sort = function(a, b) {
|
||||
return b.prop - a.prop;
|
||||
};
|
||||
limited.items = null;
|
||||
limited.$.repeater.render();
|
||||
limited.items = limited.preppedItems;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 2);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('increase limit', function() {
|
||||
// Increase limit
|
||||
limited.$.repeater._limit = 10;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 10);
|
||||
checkItemOrder(stamped);
|
||||
// Increase limit
|
||||
limited.$.repeater._limit = 20;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 20);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('increase limit above items.length', function() {
|
||||
limited.$.repeater._limit = 30;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 20);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('decrease limit', function() {
|
||||
// Decrease limit
|
||||
limited.$.repeater._limit = 15;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 15);
|
||||
checkItemOrder(stamped);
|
||||
// Decrease limit
|
||||
limited.$.repeater._limit = 0;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 0);
|
||||
});
|
||||
|
||||
test('negative limit', function() {
|
||||
limited.$.repeater._limit = -10;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('limit with filter', function() {
|
||||
|
||||
var checkItemOrder = function(stamped) {
|
||||
for (var i=0; i<stamped.length; i++) {
|
||||
assert.equal(stamped[i].textContent, i * 2);
|
||||
}
|
||||
};
|
||||
|
||||
test('initial limit', function() {
|
||||
var items = limited.items;
|
||||
limited.$.repeater._limit = 2;
|
||||
limited.$.repeater.sort = null;
|
||||
limited.$.repeater.filter = function(a) {
|
||||
return (a.prop % 2) === 0;
|
||||
};
|
||||
limited.items = null;
|
||||
limited.$.repeater.render();
|
||||
limited.items = items;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 2);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('increase limit', function() {
|
||||
// Increase limit
|
||||
limited.$.repeater._limit = 5;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 5);
|
||||
checkItemOrder(stamped);
|
||||
// Increase limit
|
||||
limited.$.repeater._limit = 10;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 10);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('increase limit above items.length', function() {
|
||||
limited.$.repeater._limit = 30;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 10);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('decrease limit', function() {
|
||||
// Decrease limit
|
||||
limited.$.repeater._limit = 5;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 5);
|
||||
checkItemOrder(stamped);
|
||||
// Decrease limit
|
||||
limited.$.repeater._limit = 0;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 0);
|
||||
});
|
||||
|
||||
test('negative limit', function() {
|
||||
limited.$.repeater._limit = -10;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('limit with sort & filter', function() {
|
||||
|
||||
var checkItemOrder = function(stamped) {
|
||||
for (var i=0; i<stamped.length; i++) {
|
||||
assert.equal(stamped[i].textContent, (9 - i) * 2);
|
||||
}
|
||||
};
|
||||
|
||||
test('initial limit', function() {
|
||||
var items = limited.items;
|
||||
limited.$.repeater._limit = 2;
|
||||
limited.$.repeater.sort = function(a, b) {
|
||||
return b.prop - a.prop;
|
||||
};
|
||||
limited.$.repeater.filter = function(a) {
|
||||
return (a.prop % 2) === 0;
|
||||
};
|
||||
limited.items = null;
|
||||
limited.$.repeater.render();
|
||||
limited.items = items;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 2);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('increase limit', function() {
|
||||
// Increase limit
|
||||
limited.$.repeater._limit = 5;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 5);
|
||||
checkItemOrder(stamped);
|
||||
// Increase limit
|
||||
limited.$.repeater._limit = 10;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 10);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('increase limit above items.length', function() {
|
||||
limited.$.repeater._limit = 30;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 10);
|
||||
checkItemOrder(stamped);
|
||||
});
|
||||
|
||||
test('decrease limit', function() {
|
||||
// Decrease limit
|
||||
limited.$.repeater._limit = 5;
|
||||
limited.$.repeater.render();
|
||||
var stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 5);
|
||||
checkItemOrder(stamped);
|
||||
// Decrease limit
|
||||
limited.$.repeater._limit = 0;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 0);
|
||||
});
|
||||
|
||||
test('negative limit', function() {
|
||||
limited.$.repeater._limit = -10;
|
||||
limited.$.repeater.render();
|
||||
stamped = Polymer.dom(limited.root).querySelectorAll('*:not(template)');
|
||||
assert.equal(stamped.length, 0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('chunked rendering', function() {
|
||||
|
||||
test('basic chunked rendering', function(done) {
|
||||
|
||||
var checkItemOrder = function(stamped) {
|
||||
for (var i=0; i<stamped.length; i++) {
|
||||
assert.equal(stamped[i].textContent, i);
|
||||
}
|
||||
};
|
||||
|
||||
var lastLength = 0;
|
||||
var checkCount = function() {
|
||||
var stamped = Polymer.dom(chunked.root).querySelectorAll('*:not(template)');
|
||||
checkItemOrder(stamped);
|
||||
if (stamped.length && lastLength === 0) {
|
||||
// Initial rendering of initial count
|
||||
assert.equal(stamped.length, 10);
|
||||
} else {
|
||||
// Remaining rendering incremenets
|
||||
assert.isTrue(stamped.length > lastLength);
|
||||
}
|
||||
if (stamped.length < 100) {
|
||||
lastLength = stamped.length;
|
||||
checkUntilComplete();
|
||||
} else {
|
||||
// Final rendering at exact item count
|
||||
assert.equal(stamped.length, 100);
|
||||
done();
|
||||
}
|
||||
};
|
||||
var checkUntilComplete = function() {
|
||||
// On polyfilled MO, need to wait one setTimeout before rAF
|
||||
if (MutationObserver._isPolyfilled) {
|
||||
setTimeout(function() {
|
||||
requestAnimationFrame(checkCount);
|
||||
});
|
||||
} else {
|
||||
requestAnimationFrame(checkCount);
|
||||
}
|
||||
};
|
||||
|
||||
chunked.items = chunked.preppedItems.slice();
|
||||
checkUntilComplete();
|
||||
|
||||
});
|
||||
|
||||
test('mutations during chunked rendering', function(done) {
|
||||
|
||||
var checkItemOrder = function(stamped) {
|
||||
var last = -1;
|
||||
for (var i=0; i<stamped.length; i++) {
|
||||
var curr = parseFloat(stamped[i].textContent);
|
||||
assert.isTrue(curr > last);
|
||||
last = curr;
|
||||
}
|
||||
};
|
||||
|
||||
var mutateArray = function(repeater, renderedCount) {
|
||||
// The goal here is to remove & add some, and do it over
|
||||
// the threshold of where we have currently rendered items, and
|
||||
// ensure that the prop values of the newly inserted items are in
|
||||
// ascending order so we can do a simple check in checkItemOrder
|
||||
var overlap = 2;
|
||||
var remove = 4;
|
||||
var add = 6;
|
||||
var start = renderedCount.length - overlap;
|
||||
if (start + add < repeater.items.length) {
|
||||
var end = start + remove;
|
||||
var args = ['items', start, remove];
|
||||
var startVal = repeater.items[start].prop;
|
||||
var endVal = repeater.items[end].prop;
|
||||
var delta = (endVal - startVal) / add;
|
||||
for (var i=0; i<add; i++) {
|
||||
args.push({prop: startVal + i*delta});
|
||||
}
|
||||
repeater.splice.apply(repeater, args);
|
||||
}
|
||||
};
|
||||
|
||||
var lastLength = 0;
|
||||
var mutateCount = 5;
|
||||
var checkCount = function() {
|
||||
var stamped = Polymer.dom(chunked.root).querySelectorAll('*:not(template)');
|
||||
checkItemOrder(stamped);
|
||||
if (stamped.length && lastLength === 0) {
|
||||
// Initial rendering of initial count
|
||||
assert.equal(stamped.length, 10);
|
||||
} else {
|
||||
// Remaining rendering incremenets
|
||||
assert.isTrue(stamped.length > lastLength);
|
||||
}
|
||||
if (stamped.length < chunked.items.length) {
|
||||
if (mutateCount-- > 0) {
|
||||
mutateArray(chunked, stamped);
|
||||
}
|
||||
lastLength = stamped.length;
|
||||
checkUntilComplete();
|
||||
} else {
|
||||
// Final rendering at exact item count
|
||||
assert.equal(stamped.length, chunked.items.length);
|
||||
done();
|
||||
}
|
||||
};
|
||||
var checkUntilComplete = function() {
|
||||
// On polyfilled MO, need to wait one setTimeout before rAF
|
||||
if (MutationObserver._isPolyfilled) {
|
||||
setTimeout(function() {
|
||||
requestAnimationFrame(checkCount);
|
||||
});
|
||||
} else {
|
||||
requestAnimationFrame(checkCount);
|
||||
}
|
||||
};
|
||||
|
||||
chunked.items = chunked.preppedItems.slice();
|
||||
checkUntilComplete();
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('mutations during chunked rendering, sort & filtered', function(done) {
|
||||
|
||||
var checkItemOrder = function(stamped) {
|
||||
var last = Infinity;
|
||||
for (var i=0; i<stamped.length; i++) {
|
||||
var curr = parseFloat(stamped[i].textContent);
|
||||
assert.isTrue(curr <= last);
|
||||
assert.strictEqual(curr % 2, 0);
|
||||
last = curr;
|
||||
}
|
||||
};
|
||||
|
||||
var mutateArray = function(repeater, stamped) {
|
||||
var start = parseInt(stamped[0].textContent);
|
||||
var end = parseInt(stamped[stamped.length-1].textContent);
|
||||
var mid = (end-start)/2;
|
||||
for (var i=0; i<5; i++) {
|
||||
chunked.push('items', {prop: mid + 1});
|
||||
}
|
||||
chunked.splice('items', Math.round(stamped.length/2), 3);
|
||||
};
|
||||
|
||||
var lastLength = 0;
|
||||
var mutateCount = 5;
|
||||
var checkCount = function() {
|
||||
var stamped = Polymer.dom(chunked.root).querySelectorAll('*:not(template)');
|
||||
checkItemOrder(stamped);
|
||||
var filteredLength = chunked.items.filter(chunked.$.repeater.filter).length;
|
||||
if (stamped.length && lastLength === 0) {
|
||||
// Initial rendering of initial count
|
||||
assert.equal(stamped.length, 10);
|
||||
} else {
|
||||
// Remaining rendering incremenets
|
||||
if (stamped.length < filteredLength) {
|
||||
assert.isTrue(stamped.length > lastLength);
|
||||
}
|
||||
}
|
||||
if (stamped.length < filteredLength) {
|
||||
if (mutateCount-- > 0) {
|
||||
mutateArray(chunked, stamped);
|
||||
}
|
||||
lastLength = stamped.length;
|
||||
checkUntilComplete();
|
||||
} else {
|
||||
assert.equal(stamped.length, filteredLength);
|
||||
done();
|
||||
}
|
||||
};
|
||||
var checkUntilComplete = function() {
|
||||
// On polyfilled MO, need to wait one setTimeout before rAF
|
||||
if (MutationObserver._isPolyfilled) {
|
||||
setTimeout(function() {
|
||||
requestAnimationFrame(checkCount);
|
||||
});
|
||||
} else {
|
||||
requestAnimationFrame(checkCount);
|
||||
}
|
||||
};
|
||||
|
||||
chunked.$.repeater.sort = function(a, b) {
|
||||
return b.prop - a.prop;
|
||||
};
|
||||
chunked.$.repeater.filter = function(a) {
|
||||
return (a.prop % 2) === 0;
|
||||
};
|
||||
chunked.items = chunked.preppedItems.slice();
|
||||
checkUntilComplete();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
Loading…
Reference in New Issue
Block a user