Merge pull request #2659 from Polymer/2537-kschaaf-chunked-repeat

Adds incremental rendering to dom-repeat. Fixes #2537.
This commit is contained in:
Steve Orvell 2015-11-04 20:01:49 -08:00
commit b137fe70dd
3 changed files with 737 additions and 68 deletions

View File

@ -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);

View File

@ -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>

View File

@ -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>