Skip to content

Commit 6f92c31

Browse files
committed
feat(VirtualRepeat): handle null / undefined, empty collection
1 parent c1632a6 commit 6f92c31

File tree

6 files changed

+109
-60
lines changed

6 files changed

+109
-60
lines changed

src/array-virtual-repeat-strategy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy {
6161
repeat.updateBindings(view);
6262
}
6363
// add new views
64-
let minLength = Math.min(repeat._viewsLength, items.length);
64+
let minLength = Math.min(repeat._viewsLength, itemsLength);
6565
for (let i = viewsLength; i < minLength; i++) {
6666
let overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength);
6767
repeat.addView(overrideContext.bindingContext, overrideContext);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { NullRepeatStrategy } from "aurelia-templating-resources";
2+
3+
export class NullVirtualRepeatStrategy extends NullRepeatStrategy {
4+
instanceChanged(repeat) {
5+
super.instanceChanged(repeat);
6+
repeat._resetCalculation();
7+
}
8+
}

src/template-strategy.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class TableStrategy {
6868
}
6969

7070
createTopBufferElement(element: Element): Element {
71-
const elementName = element.parentNode.localName === 'ul' ? 'li' : 'div';
71+
const elementName = /^[uo]l$/.test(element.parentNode.localName) ? 'li' : 'div';
7272
const buffer = DOM.createElement(elementName);
7373
const tableElement = element.parentNode.parentNode;
7474
tableElement.parentNode.insertBefore(buffer, tableElement);
@@ -77,7 +77,7 @@ export class TableStrategy {
7777
}
7878

7979
createBottomBufferElement(element: Element): Element {
80-
const elementName = element.parentNode.localName === 'ul' ? 'li' : 'div';
80+
const elementName = /^[uo]l$/.test(element.parentNode.localName) ? 'li' : 'div';
8181
const buffer = DOM.createElement(elementName);
8282
const tableElement = element.parentNode.parentNode;
8383
tableElement.parentNode.insertBefore(buffer, tableElement.nextSibling);
@@ -135,14 +135,14 @@ export class DefaultTemplateStrategy {
135135
}
136136

137137
createTopBufferElement(element: Element): Element {
138-
const elementName = element.parentNode.localName === 'ul' ? 'li' : 'div';
138+
const elementName = /^[uo]l$/.test(element.parentNode.localName) ? 'li' : 'div';
139139
const buffer = DOM.createElement(elementName);
140140
element.parentNode.insertBefore(buffer, element);
141141
return buffer;
142142
}
143143

144144
createBottomBufferElement(element: Element): Element {
145-
const elementName = element.parentNode.localName === 'ul' ? 'li' : 'div';
145+
const elementName = /^[uo]l$/.test(element.parentNode.localName) ? 'li' : 'div';
146146
const buffer = DOM.createElement(elementName);
147147
element.parentNode.insertBefore(buffer, element.nextSibling);
148148
return buffer;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import {RepeatStrategyLocator} from 'aurelia-templating-resources';
22
import {ArrayVirtualRepeatStrategy} from './array-virtual-repeat-strategy';
3+
import {NullVirtualRepeatStrategy} from './null-virtual-repeat-strategy'
34

45
export class VirtualRepeatStrategyLocator extends RepeatStrategyLocator {
56
constructor() {
67
super();
78
this.matchers = [];
89
this.strategies = [];
910

11+
this.addStrategy(items => items === null || items === undefined, new NullVirtualRepeatStrategy());
1012
this.addStrategy(items => items instanceof Array, new ArrayVirtualRepeatStrategy());
1113
}
1214
}

src/virtual-repeat.js

Lines changed: 70 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -124,20 +124,9 @@ export class VirtualRepeat extends AbstractRepeater {
124124

125125
detached(): void {
126126
this.scrollContainer.removeEventListener('scroll', this.scrollListener);
127-
this._first = 0;
128-
this._previousFirst = 0;
129-
this._viewsLength = 0;
130-
this._lastRebind = 0;
131-
this._topBufferHeight = 0;
132-
this._bottomBufferHeight = 0;
133-
this._scrollingDown = false;
134-
this._scrollingUp = false;
135-
this._switchedDirection = false;
127+
this._resetCalculation();
136128
this._isAttached = false;
137-
this._ticking = false;
138-
this._hasCalculatedSizes = false;
139129
this.templateStrategy.removeBufferElements(this.element, this.topBuffer, this.bottomBuffer);
140-
this.isLastIndex = false;
141130
this.scrollContainer = null;
142131
this.scrollContainerHeight = null;
143132
this.distanceToTop = null;
@@ -162,49 +151,56 @@ export class VirtualRepeat extends AbstractRepeater {
162151
let previousLastViewIndex = this._getIndexOfLastView();
163152

164153
let items = this.items;
154+
let shouldCalculateSize = !!items;
165155
this.strategy = this.strategyLocator.getStrategy(items);
166-
if (items.length > 0 && this.viewCount() === 0) {
167-
this.strategy.createFirstItem(this);
168-
}
169-
// Skip scroll handling if we are decreasing item list
170-
// Otherwise if expanding list, call the handle scroll below
171-
if (this._itemsLength >= items.length) {
172-
//Scroll handle is redundant in this case since the instanceChanged will re-evaluate orderings
173-
// Also, when items are reduced, we're not having to move any bindings, just a straight rebind of the items in the list
174-
this._skipNextScrollHandle = true;
175-
reducingItems = true;
176-
}
177-
this._checkFixedHeightContainer();
178-
this._calcInitialHeights(items.length);
156+
157+
if (shouldCalculateSize) {
158+
if (items.length > 0 && this.viewCount() === 0) {
159+
this.strategy.createFirstItem(this);
160+
}
161+
// Skip scroll handling if we are decreasing item list
162+
// Otherwise if expanding list, call the handle scroll below
163+
if (this._itemsLength >= items.length) {
164+
//Scroll handle is redundant in this case since the instanceChanged will re-evaluate orderings
165+
// Also, when items are reduced, we're not having to move any bindings, just a straight rebind of the items in the list
166+
this._skipNextScrollHandle = true;
167+
reducingItems = true;
168+
}
169+
this._checkFixedHeightContainer();
170+
this._calcInitialHeights(items.length);
171+
}
179172
if (!this.isOneTime && !this._observeInnerCollection()) {
180173
this._observeCollection();
181174
}
182175
this.strategy.instanceChanged(this, items, this._first);
183-
this._lastRebind = this._first; //Reset rebinding
184-
185-
if (reducingItems && previousLastViewIndex > this.items.length - 1) {
186-
//Do we need to set scrolltop so that we appear at the bottom of the list to match scrolling as far as we could?
187-
//We only want to execute this line if we're reducing such that it brings us to the bottom of the new list
188-
//Make sure we handle the special case of tables
189-
if (this.scrollContainer.tagName === 'TBODY') {
190-
let realScrollContainer = this.scrollContainer.parentNode.parentNode; //tbody > table > container
191-
realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight);
192-
} else {
193-
this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight);
176+
177+
if (shouldCalculateSize) {
178+
this._lastRebind = this._first; //Reset rebinding
179+
180+
if (reducingItems && previousLastViewIndex > this.items.length - 1) {
181+
//Do we need to set scrolltop so that we appear at the bottom of the list to match scrolling as far as we could?
182+
//We only want to execute this line if we're reducing such that it brings us to the bottom of the new list
183+
//Make sure we handle the special case of tables
184+
if (this.scrollContainer.tagName === 'TBODY') {
185+
let realScrollContainer = this.scrollContainer.parentNode.parentNode; //tbody > table > container
186+
realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight);
187+
} else {
188+
this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight);
189+
}
194190
}
195-
}
196-
if (!reducingItems) {
197-
// If we're expanding our items, then we need to reset our previous first for the next go around of scroll handling
198-
this._previousFirst = this._first;
199-
this._scrollingDown = true; //Simulating the down scroll event to load up data appropriately
200-
this._scrollingUp = false;
191+
if (!reducingItems) {
192+
// If we're expanding our items, then we need to reset our previous first for the next go around of scroll handling
193+
this._previousFirst = this._first;
194+
this._scrollingDown = true; //Simulating the down scroll event to load up data appropriately
195+
this._scrollingUp = false;
201196

202-
//Make sure we fix any state (we could have been at the last index before, but this doesn't get set until too late for scrolling)
203-
this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1;
204-
}
197+
//Make sure we fix any state (we could have been at the last index before, but this doesn't get set until too late for scrolling)
198+
this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1;
199+
}
205200

206-
//Need to readjust the scroll position to "move" us back to the appropriate position, since moving the views will shift our view port's percieved location
207-
this._handleScroll();
201+
//Need to readjust the scroll position to "move" us back to the appropriate position, since moving the views will shift our view port's percieved location
202+
this._handleScroll();
203+
}
208204
}
209205

210206
unbind(): void {
@@ -240,6 +236,24 @@ export class VirtualRepeat extends AbstractRepeater {
240236
}
241237
}
242238

239+
_resetCalculation(): void {
240+
this._first = 0;
241+
this._previousFirst = 0;
242+
this._viewsLength = 0;
243+
this._lastRebind = 0;
244+
this._topBufferHeight = 0;
245+
this._bottomBufferHeight = 0;
246+
this._scrollingDown = false;
247+
this._scrollingUp = false;
248+
this._switchedDirection = false;
249+
this._ticking = false;
250+
this._hasCalculatedSizes = false;
251+
this._isAtTop = true;
252+
this.isLastIndex = false;
253+
this.elementsInView = 0;
254+
this._adjustBufferHeights();
255+
}
256+
243257
_onScroll(): void {
244258
if (!this._ticking && !this._handlingMutations) {
245259
requestAnimationFrame(() => this._handleScroll());
@@ -259,6 +273,9 @@ export class VirtualRepeat extends AbstractRepeater {
259273
this._skipNextScrollHandle = false;
260274
return;
261275
}
276+
if (!this.items) {
277+
return;
278+
}
262279
let itemHeight = this.itemHeight;
263280
let scrollTop = this._fixedHeightContainer ? this.scrollContainer.scrollTop : pageYOffset - this.distanceToTop;
264281
this._first = Math.floor(scrollTop / itemHeight);
@@ -457,7 +474,12 @@ export class VirtualRepeat extends AbstractRepeater {
457474
}
458475

459476
_calcInitialHeights(itemsLength: number): void {
460-
if (this._viewsLength > 0 && this._itemsLength === itemsLength || this._viewsLength > 0 && itemsLength < 0) {
477+
const isSameLength = this._viewsLength > 0 && this._itemsLength === itemsLength;
478+
if (isSameLength) {
479+
return;
480+
}
481+
if (itemsLength < 1) {
482+
this._resetCalculation();
461483
return;
462484
}
463485
this._hasCalculatedSizes = true;

test/virtual-repeat-integration.spec.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {Container} from 'aurelia-dependency-injection';
2+
import {TaskQueue} from 'aurelia-task-queue'
13
import {StageComponent} from './component-tester';
24
import {TableStrategy} from '../src/template-strategy';
35

@@ -371,6 +373,21 @@ describe('VirtualRepeat Integration', () => {
371373
it('handles array changes', done => {
372374
create.then(() => validateArrayChange(virtualRepeat, viewModel, done));
373375
});
376+
377+
it('handles array changes with null / undefined', done => {
378+
create.then(() => {
379+
viewModel.items = null;
380+
381+
setTimeout(() => {
382+
let topBufferHeight = virtualRepeat.topBuffer.getBoundingClientRect().height;
383+
let bottomBufferHeight = virtualRepeat.bottomBuffer.getBoundingClientRect().height;
384+
385+
expect(topBufferHeight + bottomBufferHeight).toBe(0);
386+
387+
validateArrayChange(virtualRepeat, viewModel, done);
388+
}, 1000)
389+
});
390+
});
374391
});
375392

376393
describe('iterating table', () => {
@@ -487,9 +504,9 @@ describe('VirtualRepeat Integration', () => {
487504
items = [];
488505
vm = {
489506
items: items,
490-
getNextPage: function(){
507+
getNextPage: function() {
491508
let itemLength = this.items.length;
492-
for(let i = 0; i < 100; ++i) {
509+
for (let i = 0; i < 100; ++i) {
493510
let itemNum = itemLength + i;
494511
this.items.push('item' + itemNum);
495512
}
@@ -498,9 +515,9 @@ describe('VirtualRepeat Integration', () => {
498515
nestedVm = {
499516
items: items,
500517
bar: [1],
501-
getNextPage: function(topIndex, isAtBottom, isAtTop){
518+
getNextPage: function(topIndex, isAtBottom, isAtTop) {
502519
let itemLength = this.items.length;
503-
for(let i = 0; i < 100; ++i) {
520+
for (let i = 0; i < 100; ++i) {
504521
let itemNum = itemLength + i;
505522
this.items.push('item' + itemNum);
506523
}
@@ -509,18 +526,18 @@ describe('VirtualRepeat Integration', () => {
509526
promisedVm = {
510527
items: items,
511528
test: '2',
512-
getNextPage: function(){
529+
getNextPage: function() {
513530
return new Promise((resolve, reject) => {
514531
let itemLength = this.items.length;
515-
for(let i = 0; i < 100; ++i) {
532+
for (let i = 0; i < 100; ++i) {
516533
let itemNum = itemLength + i;
517534
this.items.push('item' + itemNum);
518535
}
519536
resolve(true);
520537
});
521538
}
522539
};
523-
for(let i = 0; i < 1000; ++i) {
540+
for (let i = 0; i < 1000; ++i) {
524541
items.push('item' + i);
525542
}
526543

0 commit comments

Comments
 (0)