diff --git a/sample/sample-v-ui-app/src/app.html b/sample/sample-v-ui-app/src/app.html
index 11e4c48..106910d 100644
--- a/sample/sample-v-ui-app/src/app.html
+++ b/sample/sample-v-ui-app/src/app.html
@@ -6,7 +6,7 @@
diff --git a/sample/sample-v-ui-app/src/app.ts b/sample/sample-v-ui-app/src/app.ts
index 9a8edf3..fce0659 100644
--- a/sample/sample-v-ui-app/src/app.ts
+++ b/sample/sample-v-ui-app/src/app.ts
@@ -10,14 +10,26 @@ export class App {
{
route: ['', 'phone-list'],
moduleId: PLATFORM.moduleName('./phone-list'),
- nav: true,
+ nav: 1,
title: 'Contacts'
},
{
route: 'issue-138',
moduleId: PLATFORM.moduleName('./issue-138/sub-app'),
- nav: true,
+ nav: 2,
title: 'Issue 138'
+ },
+ {
+ route: 'issue-129',
+ moduleId: PLATFORM.moduleName('./issue-129/sub-app'),
+ nav: 3,
+ title: 'Issue 129'
+ },
+ {
+ route: 'issue-102',
+ moduleId: PLATFORM.moduleName('./issue-102/sub-app'),
+ nav: 4,
+ title: 'Issue 102'
}
]);
@@ -25,13 +37,5 @@ export class App {
window['app'] = this;
}
- bind() {
- this.check();
- }
-
- check() {
- setInterval(() => {
- this.virtualRepeat = window['virtualRepeat'];
- }, 1500);
- }
+ window = window;
}
diff --git a/sample/sample-v-ui-app/src/issue-102/sub-app.html b/sample/sample-v-ui-app/src/issue-102/sub-app.html
new file mode 100644
index 0000000..71b84f8
--- /dev/null
+++ b/sample/sample-v-ui-app/src/issue-102/sub-app.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ # |
+ First Name |
+ Last Name |
+ Action |
+
+
+
+
+ ${$index} |
+ ${person.fname} |
+ ${person.lname} |
+
+
+
+ |
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/sample-v-ui-app/src/issue-102/sub-app.ts b/sample/sample-v-ui-app/src/issue-102/sub-app.ts
new file mode 100644
index 0000000..e24c6d6
--- /dev/null
+++ b/sample/sample-v-ui-app/src/issue-102/sub-app.ts
@@ -0,0 +1,47 @@
+interface IScrollContext {
+ isAtTop: boolean;
+ isAtBottom: boolean;
+ topIndex: number;
+}
+
+export interface Person {
+ fname: string;
+ lname: string;
+}
+
+const fNames = [
+ // tslint:disable-next-line:max-line-length
+ 'Ford', 'Arthur', 'Trillian', 'Sneezy', 'Sleepy', 'Dopey', 'Doc', 'Happy', 'Bashful', 'Grumpy', 'Mufasa', 'Sarabi', 'Simba', 'Nala', 'Kiara', 'Kovu', 'Timon', 'Pumbaa', 'Rafiki', 'Shenzi'
+];
+const lNames = [
+ // tslint:disable-next-line:max-line-length
+ 'Prefect', 'Dent', 'Astra', 'Adams', 'Baker', 'Clark', 'Davis', 'Evans', 'Frank', 'Ghosh', 'Hills', 'Irwin', 'Jones', 'Klein', 'Lopez', 'Mason', 'Nalty', 'Ochoa', 'Patel', 'Quinn', 'Reily', 'Smith', 'Trott', 'Usman', 'Valdo', 'White', 'Xiang', 'Yakub', 'Zafar'
+];
+export class App {
+ private people: Person[];
+
+ constructor() {
+ this.people = [
+ { fname: fNames[0], lname: lNames[0] },
+ { fname: fNames[1], lname: lNames[1] },
+ { fname: fNames[2], lname: lNames[2] }
+ ];
+ this.push30(undefined, 0);
+ }
+
+ public push30(scrollContext?: IScrollContext, count = 30) {
+ console.log('Issue-102 getting more...');
+ // if (scrollContext) {
+ // console.log('Issue-129 getting more:', JSON.stringify(scrollContext, undefined, 2));
+ // }
+ if (!scrollContext || scrollContext.isAtBottom) {
+ for (let i = 0; i < count; i++) {
+ this.people.push({
+ fname: fNames[Math.floor(Math.random() * fNames.length)],
+ lname: lNames[Math.floor(Math.random() * lNames.length)]
+ });
+ }
+ }
+ console.log('Population size:', this.people.length);
+ }
+}
diff --git a/sample/sample-v-ui-app/src/issue-129/sub-app.html b/sample/sample-v-ui-app/src/issue-129/sub-app.html
new file mode 100644
index 0000000..71b84f8
--- /dev/null
+++ b/sample/sample-v-ui-app/src/issue-129/sub-app.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ # |
+ First Name |
+ Last Name |
+ Action |
+
+
+
+
+ ${$index} |
+ ${person.fname} |
+ ${person.lname} |
+
+
+
+ |
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/sample-v-ui-app/src/issue-129/sub-app.ts b/sample/sample-v-ui-app/src/issue-129/sub-app.ts
new file mode 100644
index 0000000..1d170c8
--- /dev/null
+++ b/sample/sample-v-ui-app/src/issue-129/sub-app.ts
@@ -0,0 +1,47 @@
+interface IScrollContext {
+ isAtTop: boolean;
+ isAtBottom: boolean;
+ topIndex: number;
+}
+
+export interface Person {
+ fname: string;
+ lname: string;
+}
+
+const fNames = [
+ // tslint:disable-next-line:max-line-length
+ 'Ford', 'Arthur', 'Trillian', 'Sneezy', 'Sleepy', 'Dopey', 'Doc', 'Happy', 'Bashful', 'Grumpy', 'Mufasa', 'Sarabi', 'Simba', 'Nala', 'Kiara', 'Kovu', 'Timon', 'Pumbaa', 'Rafiki', 'Shenzi'
+];
+const lNames = [
+ // tslint:disable-next-line:max-line-length
+ 'Prefect', 'Dent', 'Astra', 'Adams', 'Baker', 'Clark', 'Davis', 'Evans', 'Frank', 'Ghosh', 'Hills', 'Irwin', 'Jones', 'Klein', 'Lopez', 'Mason', 'Nalty', 'Ochoa', 'Patel', 'Quinn', 'Reily', 'Smith', 'Trott', 'Usman', 'Valdo', 'White', 'Xiang', 'Yakub', 'Zafar'
+];
+export class App {
+ private people: Person[];
+
+ constructor() {
+ this.people = [
+ { fname: fNames[0], lname: lNames[0] },
+ { fname: fNames[1], lname: lNames[1] },
+ { fname: fNames[2], lname: lNames[2] }
+ ];
+ this.push30(undefined, 5);
+ }
+
+ public push30(scrollContext?: IScrollContext, count = 30) {
+ console.log('Issue-129 getting more...');
+ // if (scrollContext) {
+ // console.log('Issue-129 getting more:', JSON.stringify(scrollContext, undefined, 2));
+ // }
+ if (!scrollContext || scrollContext.isAtBottom) {
+ for (let i = 0; i < count; i++) {
+ this.people.push({
+ fname: fNames[Math.floor(Math.random() * fNames.length)],
+ lname: lNames[Math.floor(Math.random() * lNames.length)]
+ });
+ }
+ }
+ console.log('Population size:', this.people.length);
+ }
+}
diff --git a/src/array-virtual-repeat-strategy.ts b/src/array-virtual-repeat-strategy.ts
index e21e867..3961754 100644
--- a/src/array-virtual-repeat-strategy.ts
+++ b/src/array-virtual-repeat-strategy.ts
@@ -1,6 +1,6 @@
import { ArrayRepeatStrategy, createFullOverrideContext } from 'aurelia-templating-resources';
import { updateVirtualOverrideContexts, rebindAndMoveView, getElementDistanceToBottomViewPort } from './utilities';
-import { IVirtualRepeat, IVirtualRepeatStrategy } from './interfaces';
+import { IVirtualRepeat, IVirtualRepeatStrategy, IView } from './interfaces';
import { ViewSlot } from 'aurelia-templating';
import { mergeSplice } from 'aurelia-binding';
@@ -14,6 +14,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I
repeat.addView(overrideContext.bindingContext, overrideContext);
}
/**
+ * @override
* Handle the repeat's collection instance changing.
* @param repeat The repeater instance.
* @param items The new array instance.
@@ -23,6 +24,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I
}
/**
+ * @override
* Handle the repeat's collection instance mutating.
* @param repeat The repeat instance.
* @param array The modified array.
@@ -170,14 +172,15 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I
return undefined;
}
- _removeViewAt(repeat: IVirtualRepeat, collectionIndex: number, returnToCache: boolean, j: number, removedLength: number): any {
- let viewOrPromise;
- let view;
+ /**@internal */
+ _removeViewAt(repeat: IVirtualRepeat, collectionIndex: number, returnToCache: boolean, removeIndex: number, removedLength: number): any {
+ let viewOrPromise: IView | Promise;
+ let view: IView;
let viewSlot = repeat.viewSlot;
let viewCount = repeat.viewCount();
- let viewAddIndex;
+ let viewAddIndex: number;
let removeMoreThanInDom = removedLength > viewCount;
- if (repeat._viewsLength <= j) {
+ if (repeat._viewsLength <= removeIndex) {
repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight);
repeat._adjustBufferHeights();
return;
@@ -196,7 +199,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I
let lastViewItem = repeat._getLastViewItem();
collectionAddIndex = repeat.items.indexOf(lastViewItem) + 1;
} else {
- collectionAddIndex = j;
+ collectionAddIndex = removeIndex;
}
repeat._bottomBufferHeight = repeat._bottomBufferHeight - (repeat.itemHeight);
} else if (repeat._topBufferHeight > 0) {
@@ -207,7 +210,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I
let data = repeat.items[collectionAddIndex];
if (data) {
let overrideContext = createFullOverrideContext(repeat, data, collectionAddIndex, repeat.items.length);
- view = repeat.viewFactory.create();
+ view = repeat.viewFactory.create() as IView;
view.bind(overrideContext.bindingContext, overrideContext);
}
}
@@ -233,16 +236,22 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I
repeat._adjustBufferHeights();
}
+ /**@internal */
_isIndexBeforeViewSlot(repeat: IVirtualRepeat, viewSlot: ViewSlot, index: number): boolean {
let viewIndex = this._getViewIndex(repeat, viewSlot, index);
return viewIndex < 0;
}
+ /**@internal */
_isIndexAfterViewSlot(repeat: IVirtualRepeat, viewSlot: ViewSlot, index: number): boolean {
let viewIndex = this._getViewIndex(repeat, viewSlot, index);
return viewIndex > repeat._viewsLength - 1;
}
+ /**
+ * @internal
+ * Calculate real index of a given index, based on existing buffer height and item height
+ */
_getViewIndex(repeat: IVirtualRepeat, viewSlot: ViewSlot, index: number): number {
if (repeat.viewCount() === 0) {
return -1;
@@ -252,6 +261,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I
return index - topBufferItems;
}
+ /**@internal */
_handleAddedSplices(repeat: IVirtualRepeat, array: Array, splices: any): void {
let arrayLength = array.length;
let viewSlot = repeat.viewSlot;
diff --git a/src/aurelia-ui-virtualization.ts b/src/aurelia-ui-virtualization.ts
index a5f3162..7201a02 100644
--- a/src/aurelia-ui-virtualization.ts
+++ b/src/aurelia-ui-virtualization.ts
@@ -12,3 +12,7 @@ export {
VirtualRepeat,
InfiniteScrollNext
};
+
+export {
+ IScrollNextScrollContext
+} from './interfaces';
diff --git a/src/infinite-scroll-next.ts b/src/infinite-scroll-next.ts
index 97c8219..a9b5079 100644
--- a/src/infinite-scroll-next.ts
+++ b/src/infinite-scroll-next.ts
@@ -1,5 +1,3 @@
-import { OverrideContext, Scope } from 'aurelia-binding';
-
// Placeholder attribute to prohibit use of this attribute name in other places
export class InfiniteScrollNext {
@@ -10,15 +8,4 @@ export class InfiniteScrollNext {
name: 'infinite-scroll-next'
};
}
-
- /**@internal */
- scope: Scope;
-
- bind(bindingContext: any, overrideContext: OverrideContext): void {
- this.scope = { bindingContext, overrideContext };
- }
-
- unbind() {
- this.scope = undefined;
- }
}
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 3e9ab0f..ef71bba 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -3,6 +3,12 @@ import { ViewSlot, View, ViewFactory, BoundViewFactory, Controller } from 'aurel
import { Scope, Binding, OverrideContext } from 'aurelia-binding';
import { TaskQueue } from 'aurelia-task-queue';
+export interface IScrollNextScrollContext {
+ topIndex: number;
+ isAtBottom: boolean;
+ isAtTop: boolean;
+}
+
/**@internal */
declare module 'aurelia-binding' {
interface ObserverLocator {
@@ -11,11 +17,7 @@ declare module 'aurelia-binding' {
interface OverrideContext {
$index: number;
- $scrollContext: {
- topIndex: number;
- isAtBottom: boolean;
- isAtTop: boolean;
- };
+ $scrollContext: IScrollNextScrollContext;
$first: boolean;
$last: boolean;
$middle: boolean;
@@ -55,13 +57,25 @@ export interface IVirtualRepeatStrategy extends RepeatStrategy {
export interface IVirtualRepeat extends Repeat {
- /**@internal */ _first: number;
+ /**
+ * @internal
+ * First view index, for proper follow up calculations
+ */
+ _first: number;
- /**@internal */ _previousFirst: number;
+ /**
+ * @internal
+ * Preview first view index, for proper determination of delta
+ */
+ _previousFirst: number;
/**@internal */ _viewsLength: number;
- /**@internal */ _lastRebind: number;
+ /**
+ * @internal
+ * Last rebound view index, for determining rendered range
+ */
+ _lastRebind: number;
/**@internal */ _topBufferHeight: number;
@@ -89,6 +103,7 @@ export interface IVirtualRepeat extends Repeat {
/**@internal */ viewSlot: ViewSlot & { children: IView[] };
+ items: any[];
itemHeight: number;
strategy: IVirtualRepeatStrategy;
@@ -141,3 +156,19 @@ export interface ITemplateStrategy {
* Override `bindingContext` and `overrideContext` on `View` interface
*/
export type IView = View & Scope;
+
+// export const enum IVirtualRepeatState {
+// isAtTop = 0b0_000000_000,
+// isLastIndex = 0b0_000000_000,
+// scrollingDown = 0b0_000000_000,
+// scrollingUp = 0b0_000000_000,
+// switchedDirection = 0b0_000000_000,
+// isAttached = 0b0_000000_000,
+// ticking = 0b0_000000_000,
+// fixedHeightContainer = 0b0_000000_000,
+// hasCalculatedSizes = 0b0_000000_000,
+// calledGetMore = 0b0_000000_000,
+// skipNextScrollHandle = 0b0_000000_000,
+// handlingMutations = 0b0_000000_000,
+// isScrolling = 0b0_000000_000
+// }
diff --git a/src/virtual-repeat.ts b/src/virtual-repeat.ts
index 1b7d435..01805ed 100644
--- a/src/virtual-repeat.ts
+++ b/src/virtual-repeat.ts
@@ -1,4 +1,4 @@
-import {ObserverLocator, Scope, Expression, Disposable, ICollectionObserverSplice, OverrideContext} from 'aurelia-binding';
+import {ObserverLocator, Scope, Expression, Disposable, ICollectionObserverSplice, OverrideContext, BindingExpression} from 'aurelia-binding';
import {
BoundViewFactory,
ViewSlot,
@@ -28,8 +28,10 @@ import {
IVirtualRepeat,
IVirtualRepeatStrategy,
ITemplateStrategy,
- IView
+ IView,
+ IScrollNextScrollContext
} from './interfaces';
+import { TaskQueue } from '../sample/sample-v-ui-app/node_modules/aurelia-framework/dist/aurelia-framework';
const enum VirtualRepeatCallContext {
handleCollectionMutated = 'handleCollectionMutated',
@@ -50,12 +52,23 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
type: 'attribute',
name: 'virtual-repeat',
templateController: true,
- bindables: ['items', 'local'] as any // Wrong typings in templating
+ // Wrong typings in templating
+ bindables: ['items', 'local'] as any
};
}
- /**@internal*/ _first = 0;
- /**@internal*/ _previousFirst = 0;
+ /**
+ * @internal
+ * First view index, for proper follow up calculations
+ */
+ _first = 0;
+
+ /**
+ * @internal
+ * Preview first view index, for proper determination of delta
+ */
+ _previousFirst = 0;
+
/**@internal*/ _viewsLength = 0;
/**@internal*/ _lastRebind = 0;
/**@internal*/ _topBufferHeight = 0;
@@ -135,7 +148,10 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
/**@internal */
private domHelper: DomHelper;
- /**@internal */
+ /**
+ * @internal
+ * Temporary snapshot of items list count. Updated regularly to determinate calculation need
+ */
private _itemsLength: number;
/**@internal */
@@ -145,13 +161,18 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
private scrollListener: () => any;
/**@internal */
- _sizeInterval: any;
+ private _sizeInterval: any;
+
+ /**@internal */
+ private _calcDistanceToTopInterval: any;
+
+ /**@internal */
+ private taskQueue: TaskQueue;
templateStrategy: ITemplateStrategy;
topBuffer: HTMLElement;
bottomBuffer: HTMLElement;
- calcDistanceToTopInterval: any;
itemHeight: number;
movedViewsCount: number;
@@ -190,9 +211,10 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
this.element = element;
this.viewFactory = viewFactory;
this.instruction = instruction;
- this.viewSlot = viewSlot as any;
+ this.viewSlot = viewSlot as ViewSlot & { children: IView[] };
this.lookupFunctions = viewResources['lookupFunctions'];
this.observerLocator = observerLocator;
+ this.taskQueue = observerLocator.taskQueue;
this.strategyLocator = strategyLocator;
this.templateStrategyLocator = templateStrategyLocator;
this.sourceExpression = getItemsSourceExpression(this.instruction, 'virtual-repeat.for');
@@ -200,44 +222,48 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
this.domHelper = domHelper;
}
+ /**@override */
+ bind(bindingContext: any, overrideContext: OverrideContext): void {
+ this.scope = { bindingContext, overrideContext };
+ }
+
/**@override */
attached(): void {
this._isAttached = true;
- let element = this.element;
this._itemsLength = this.items.length;
- this.templateStrategy = this.templateStrategyLocator.getStrategy(element);
- this.scrollContainer = this.templateStrategy.getScrollContainer(element);
- this.topBuffer = this.templateStrategy.createTopBufferElement(element);
- this.bottomBuffer = this.templateStrategy.createBottomBufferElement(element);
+
+ let element = this.element;
+ let templateStrategy = this.templateStrategy = this.templateStrategyLocator.getStrategy(element);
+
+ let scrollListener = this.scrollListener = () => this._onScroll();
+ let scrollContainer = this.scrollContainer = templateStrategy.getScrollContainer(element);
+ let topBuffer = this.topBuffer = templateStrategy.createTopBufferElement(element);
+
+ this.bottomBuffer = templateStrategy.createBottomBufferElement(element);
this.itemsChanged();
- this.scrollListener = () => this._onScroll();
- this.calcDistanceToTopInterval = setInterval(() => {
- let distanceToTop = this.distanceToTop;
- this.distanceToTop = this.domHelper.getElementDistanceToTopOfDocument(this.topBuffer);
- this.distanceToTop += this.topBufferDistance;
- if (distanceToTop !== this.distanceToTop) {
+ this._calcDistanceToTopInterval = PLATFORM.global.setInterval(() => {
+ let prevDistanceToTop = this.distanceToTop;
+ let currDistanceToTop = this.domHelper.getElementDistanceToTopOfDocument(topBuffer) + this.topBufferDistance;
+ this.distanceToTop = currDistanceToTop;
+ if (prevDistanceToTop !== currDistanceToTop) {
this._handleScroll();
}
}, 500);
- this.distanceToTop = this.domHelper.getElementDistanceToTopOfDocument(this.templateStrategy.getFirstElement(this.topBuffer));
// When dealing with tables, there can be gaps between elements, causing distances to be messed up. Might need to handle this case here.
- this.topBufferDistance = this.templateStrategy.getTopBufferDistance(this.topBuffer);
+ this.topBufferDistance = templateStrategy.getTopBufferDistance(topBuffer);
+ this.distanceToTop = this.domHelper
+ .getElementDistanceToTopOfDocument(templateStrategy.getFirstElement(topBuffer));
- if (this.domHelper.hasOverflowScroll(this.scrollContainer)) {
+ if (this.domHelper.hasOverflowScroll(scrollContainer)) {
this._fixedHeightContainer = true;
- this.scrollContainer.addEventListener('scroll', this.scrollListener);
+ scrollContainer.addEventListener('scroll', scrollListener);
} else {
- document.addEventListener('scroll', this.scrollListener);
+ document.addEventListener('scroll', scrollListener);
}
- }
-
- /**@override */
- bind(bindingContext: any, overrideContext: OverrideContext): void {
- this.scope = { bindingContext, overrideContext };
- if (this._isAttached) {
- this.itemsChanged();
+ if (this.items.length < this.elementsInView && this.isLastIndex === undefined) {
+ this._getMore(true);
}
}
@@ -253,22 +279,43 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
} else {
document.removeEventListener('scroll', this.scrollListener);
}
+ this.isLastIndex = undefined;
this._fixedHeightContainer = false;
this._resetCalculation();
this._isAttached = false;
+ this._itemsLength = 0;
this.templateStrategy.removeBufferElements(this.element, this.topBuffer, this.bottomBuffer);
- this.scrollContainer = null;
+ this.topBuffer = this.bottomBuffer = this.scrollContainer = this.scrollListener = null;
this.scrollContainerHeight = 0;
this.distanceToTop = 0;
this.removeAllViews(true, false);
this._unsubscribeCollection();
- clearInterval(this.calcDistanceToTopInterval);
+ clearInterval(this._calcDistanceToTopInterval);
if (this._sizeInterval) {
clearInterval(this._sizeInterval);
}
}
/**@override */
+ unbind(): void {
+ this.scope = null;
+ this.items = null;
+ this._itemsLength = 0;
+ }
+
+ /**
+ * @override
+ *
+ * If `items` is truthy, do the following calculation/work:
+ *
+ * - container fixed height flag
+ * - necessary initial heights
+ * - create new collection observer & observe for changes
+ * - invoke `instanceChanged` on repeat strategy to create views / move views
+ * - update indices
+ * - update scrollbar position in special scenarios
+ * - handle scroll as if scroll event happened
+ */
itemsChanged(): void {
this._unsubscribeCollection();
// still bound?
@@ -303,14 +350,19 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
this.strategy.instanceChanged(this, items, this._first);
if (shouldCalculateSize) {
- this._lastRebind = this._first; // Reset rebinding
+ // Reset rebinding
+ this._lastRebind = this._first;
if (reducingItems && previousLastViewIndex > this.items.length - 1) {
// Do we need to set scrolltop so that we appear at the bottom of the list to match scrolling as far as we could?
// We only want to execute this line if we're reducing such that it brings us to the bottom of the new list
// Make sure we handle the special case of tables
+ // -------
+ // Note: if branch is never the case anymore,
+ // keeping this code to keep the history of logic for future work
if (this.scrollContainer.tagName === 'TBODY') {
- let realScrollContainer = this.scrollContainer.parentNode.parentNode as Element; // tbody > table > container
+ // tbody > table > container
+ let realScrollContainer = this.scrollContainer.parentNode.parentNode as Element;
realScrollContainer.scrollTop = realScrollContainer.scrollTop + (this.viewCount() * this.itemHeight);
} else {
this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight);
@@ -319,7 +371,8 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
if (!reducingItems) {
// If we're expanding our items, then we need to reset our previous first for the next go around of scroll handling
this._previousFirst = this._first;
- this._scrollingDown = true; // Simulating the down scroll event to load up data appropriately
+ // Simulating the down scroll event to load up data appropriately
+ this._scrollingDown = true;
this._scrollingUp = false;
// 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)
@@ -332,15 +385,9 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
}
}
- /**@override */
- unbind(): void {
- this.scope = null;
- this.items = null;
- this._itemsLength = null;
- }
-
/**@override */
handleCollectionMutated(collection: any[], changes: ICollectionObserverSplice[]): void {
+ // guard against multiple mutation, or mutation combined with instance mutation
if (this.ignoreMutation) {
return;
}
@@ -358,7 +405,7 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
}
this.ignoreMutation = true;
let newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions);
- this.observerLocator.taskQueue.queueMicroTask(() => this.ignoreMutation = false);
+ this.taskQueue.queueMicroTask(() => this.ignoreMutation = false);
// call itemsChanged...
if (newItems === this.items) {
@@ -494,56 +541,66 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
/**@internal*/
_getMore(force?: boolean): void {
- if (this.isLastIndex || this._first === 0 || force) {
+ if (this.isLastIndex || this._first === 0 || force === true) {
if (!this._calledGetMore) {
let executeGetMore = () => {
this._calledGetMore = true;
let firstView = this._getFirstView();
- let func = (firstView
+ let scrollNextAttrName = 'infinite-scroll-next';
+ let func: string | (BindingExpression & { sourceExpression: Expression }) = (firstView
&& firstView.firstChild
&& firstView.firstChild.au
- && firstView.firstChild.au['infinite-scroll-next'])
- ? firstView.firstChild.au['infinite-scroll-next'].instruction.attributes['infinite-scroll-next']
+ && firstView.firstChild.au[scrollNextAttrName])
+ ? firstView.firstChild.au[scrollNextAttrName].instruction.attributes[scrollNextAttrName]
: undefined;
let topIndex = this._first;
let isAtBottom = this._bottomBufferHeight === 0;
let isAtTop = this._isAtTop;
- let scrollContext = {
+ let scrollContext: IScrollNextScrollContext = {
topIndex: topIndex,
isAtBottom: isAtBottom,
isAtTop: isAtTop
};
- this.scope.overrideContext.$scrollContext = scrollContext;
+ let overrideContext = this.scope.overrideContext;
+ overrideContext.$scrollContext = scrollContext;
if (func === undefined) {
+ // Still reset `_calledGetMore` flag as if it was invoked
+ // though this should not happen as presence of infinite-scroll-next attribute
+ // will make the value at least be an empty string
+ // keeping this logic here for future enhancement/evolution
+ this._calledGetMore = false;
return null;
} else if (typeof func === 'string') {
- let getMoreFuncName = (this.view(0).firstChild as Element).getAttribute('infinite-scroll-next');
- let funcCall = this.scope.overrideContext.bindingContext[getMoreFuncName];
+ let getMoreFuncName = (firstView.firstChild as Element).getAttribute(scrollNextAttrName);
+ let funcCall = overrideContext.bindingContext[getMoreFuncName];
if (typeof funcCall === 'function') {
- let result = funcCall.call(this.scope.overrideContext.bindingContext, topIndex, isAtBottom, isAtTop);
+ let result = funcCall.call(overrideContext.bindingContext, topIndex, isAtBottom, isAtTop);
if (!(result instanceof Promise)) {
- this._calledGetMore = false; // Reset for the next time
+ // Reset for the next time
+ this._calledGetMore = false;
} else {
return result.then(() => {
- this._calledGetMore = false; // Reset for the next time
+ // Reset for the next time
+ this._calledGetMore = false;
});
}
} else {
- throw new Error("'infinite-scroll-next' must be a function or evaluate to one");
+ throw new Error(`'${scrollNextAttrName}' must be a function or evaluate to one`);
}
} else if (func.sourceExpression) {
- this._calledGetMore = false; // Reset for the next time
+ // Reset for the next time
+ this._calledGetMore = false;
return func.sourceExpression.evaluate(this.scope);
} else {
- throw new Error("'infinite-scroll-next' must be a function or evaluate to one");
+ throw new Error(`'${scrollNextAttrName}' must be a function or evaluate to one`);
}
return null;
};
- this.observerLocator.taskQueue.queueMicroTask(executeGetMore);
+ this.taskQueue.queueMicroTask(executeGetMore);
}
}
}
@@ -592,20 +649,20 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
/**@internal*/
_unsubscribeCollection(): void {
- if (this.collectionObserver) {
- this.collectionObserver.unsubscribe(this.callContext, this);
- this.collectionObserver = null;
- this.callContext = null;
+ let collectionObserver = this.collectionObserver;
+ if (collectionObserver) {
+ collectionObserver.unsubscribe(this.callContext, this);
+ this.collectionObserver = this.callContext = null;
}
}
/**@internal */
- _getFirstView(): View | null {
+ _getFirstView(): IView | null {
return this.view(0);
}
/**@internal */
- _getLastView(): View | null {
+ _getLastView(): IView | null {
return this.view(this.viewCount() - 1);
}
@@ -642,19 +699,19 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
/**@internal*/
_getIndexOfLastView(): number {
- const lastView = this._getLastView() as IView;
+ const lastView = this._getLastView();
return lastView === null ? -1 : lastView.overrideContext.$index;
}
/**@internal*/
_getLastViewItem(): IView {
- let lastView = this._getLastView() as IView;
+ let lastView = this._getLastView();
return lastView === null ? undefined : lastView.bindingContext[this.local];
}
/**@internal*/
_getIndexOfFirstView(): number {
- let firstView = this._getFirstView() as IView;
+ let firstView = this._getFirstView();
return firstView === null ? -1 : firstView.overrideContext.$index;
}
@@ -720,7 +777,8 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
} else { // Use case when items are added (we are adding scrollable space to the bottom)
// We need to re-evaluate which is the true "first". If we've added items, then the previous "first" is actually too far down the list
this._first = this._getIndexOfFirstView();
- let adjustedTopBufferHeight = this._first * this.itemHeight; // appropriate buffer height for top, might be 1 too long...
+ // appropriate buffer height for top, might be 1 too long...
+ let adjustedTopBufferHeight = this._first * this.itemHeight;
this._topBufferHeight = adjustedTopBufferHeight;
// But what about when we've only scrolled slightly down the list? We need to readjust the top buffer height then
this._bottomBufferHeight = newBottomBufferHeight - adjustedTopBufferHeight;
@@ -767,10 +825,11 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
/**@internal*/
_observeCollection(): void {
- this.collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, this.items);
- if (this.collectionObserver) {
+ let collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, this.items);
+ if (collectionObserver) {
this.callContext = VirtualRepeatCallContext.handleCollectionMutated;
- this.collectionObserver.subscribe(this.callContext, this);
+ this.collectionObserver = collectionObserver;
+ collectionObserver.subscribe(this.callContext, this);
}
}
diff --git a/test/utilities.ts b/test/utilities.ts
index a874d0b..7710bd6 100644
--- a/test/utilities.ts
+++ b/test/utilities.ts
@@ -67,7 +67,10 @@ export function validateScrolledState(virtualRepeat: VirtualRepeat, viewModel: a
let topBufferHeight = virtualRepeat.topBuffer.getBoundingClientRect().height;
let bottomBufferHeight = virtualRepeat.bottomBuffer.getBoundingClientRect().height;
let renderedItemsHeight = views.length * itemHeight;
- expect(topBufferHeight + renderedItemsHeight + bottomBufferHeight).toBe(expectedHeight);
+ expect(topBufferHeight + renderedItemsHeight + bottomBufferHeight).toBe(
+ expectedHeight,
+ `Top buffer (${topBufferHeight}) + items height (${renderedItemsHeight}) + bottom buffer (${bottomBufferHeight}) should have been correct`
+ );
if (viewModel.items.length > views.length) {
expect(topBufferHeight + bottomBufferHeight).toBeGreaterThan(0);
@@ -91,3 +94,21 @@ export function validateScrolledState(virtualRepeat: VirtualRepeat, viewModel: a
expect(overrideContext.$even).toBe(even);
}
}
+
+/**
+ * Manually dispatch a scroll event and validate scrolled state of virtual repeat
+ *
+ * Programatically set `scrollTop` of element specified with `elementSelector` query string
+ * (or `#scrollContainer` by default) to be equal with its `scrollHeight`
+ */
+export function validateScroll(virtualRepeat: VirtualRepeat, viewModel: any, itemHeight: number, element: Element, done: Function): void {
+ let event = new Event('scroll');
+ element.scrollTop = element.scrollHeight;
+ element.dispatchEvent(event);
+ window.setTimeout(() => {
+ window.requestAnimationFrame(() => {
+ validateScrolledState(virtualRepeat, viewModel, itemHeight);
+ done();
+ });
+ });
+}
diff --git a/test/virtual-repeat-integration.spec.ts b/test/virtual-repeat-integration.spec.ts
index e7f3cf2..bbe6b1e 100644
--- a/test/virtual-repeat-integration.spec.ts
+++ b/test/virtual-repeat-integration.spec.ts
@@ -1,10 +1,10 @@
import './setup';
// import {Container} from 'aurelia-dependency-injection';
// import {TaskQueue} from 'aurelia-task-queue'
-import {StageComponent, ComponentTester} from './component-tester';
+import { StageComponent, ComponentTester } from './component-tester';
import { PLATFORM } from 'aurelia-pal';
// import {TableStrategy} from '../src/template-strategy';
-import { createAssertionQueue, validateState, validateScrolledState } from './utilities';
+import { createAssertionQueue, validateState, validateScrolledState, Queue as AsyncQueue } from './utilities';
import { VirtualRepeat } from '../src/virtual-repeat';
PLATFORM.moduleName('src/virtual-repeat');
@@ -14,40 +14,46 @@ PLATFORM.moduleName('src/infinite-scroll-next');
describe('VirtualRepeat Integration', () => {
// async queue
- let nq = createAssertionQueue();
+ let nq: AsyncQueue = createAssertionQueue();
let itemHeight = 100;
- function validateScroll(virtualRepeat: VirtualRepeat, viewModel: any, done: Function, element?: string) {
- let elem = document.getElementById(element || 'scrollContainer');
- let event = new Event('scroll');
- elem.scrollTop = elem.scrollHeight;
- elem.dispatchEvent(event);
- window.setTimeout(() => {
- window.requestAnimationFrame(() => {
- validateScrolledState(virtualRepeat, viewModel, itemHeight);
- done();
- });
+ /**
+ * Manually dispatch a scroll event and validate scrolled state of virtual repeat
+ *
+ * Programatically set `scrollTop` of element specified with `elementSelector` query string
+ * (or `#scrollContainer` by default) to be equal with its `scrollHeight`
+ */
+ function validateScroll(virtualRepeat: VirtualRepeat, viewModel: any, done: Function, elementSelector?: string): void {
+ let elem = document.getElementById(elementSelector || 'scrollContainer');
+ let event = new Event('scroll');
+ elem.scrollTop = elem.scrollHeight;
+ elem.dispatchEvent(event);
+ window.setTimeout(() => {
+ window.requestAnimationFrame(() => {
+ validateScrolledState(virtualRepeat, viewModel, itemHeight);
+ done();
});
+ });
}
- function validateScrollUp(virtualRepeat: VirtualRepeat, viewModel: any, done: Function, element?: string) {
- let elem = document.getElementById(element || 'scrollContainer');
- let event = new Event('scroll');
- elem.scrollTop = elem.scrollHeight / 2; // Scroll down but not far enough to reach bottom and call 'getNext'
- elem.dispatchEvent(event);
- window.setTimeout(() => {
+ function validateScrollUp(virtualRepeat: VirtualRepeat, viewModel: any, done: Function, element?: string): void {
+ let elem = document.getElementById(element || 'scrollContainer');
+ let event = new Event('scroll');
+ elem.scrollTop = elem.scrollHeight / 2; // Scroll down but not far enough to reach bottom and call 'getNext'
+ elem.dispatchEvent(event);
+ window.setTimeout(() => {
+ window.requestAnimationFrame(() => {
+ let eventUp = new Event('scroll');
+ elem.scrollTop = 0;
+ elem.dispatchEvent(eventUp);
+ window.setTimeout(() => {
window.requestAnimationFrame(() => {
- let eventUp = new Event('scroll');
- elem.scrollTop = 0;
- elem.dispatchEvent(eventUp);
- window.setTimeout(() => {
- window.requestAnimationFrame(() => {
- validateScrolledState(virtualRepeat, viewModel, itemHeight);
- done();
- });
- });
+ validateScrolledState(virtualRepeat, viewModel, itemHeight);
+ done();
});
+ });
});
+ });
}
function validatePush(virtualRepeat: VirtualRepeat, viewModel: any, done: Function) {
@@ -64,44 +70,44 @@ describe('VirtualRepeat Integration', () => {
function validatePop(virtualRepeat: VirtualRepeat, viewModel: any, done: Function) {
viewModel.items.pop();
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => viewModel.items.pop());
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => viewModel.items.pop());
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => done());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => viewModel.items.pop());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => viewModel.items.pop());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => done());
}
function validateUnshift(virtualRepeat, viewModel, done) {
viewModel.items.unshift('z');
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => viewModel.items.unshift('y', 'x'));
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => viewModel.items.unshift());
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => done());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => viewModel.items.unshift('y', 'x'));
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => viewModel.items.unshift());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => done());
}
function validateShift(virtualRepeat, viewModel, done) {
viewModel.items.shift();
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => viewModel.items.shift());
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => viewModel.items.shift());
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => done());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => viewModel.items.shift());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => viewModel.items.shift());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => done());
}
function validateReverse(virtualRepeat, viewModel, done) {
viewModel.items.reverse();
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => done());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => done());
}
function validateSplice(virtualRepeat: VirtualRepeat, viewModel: any, done: Function) {
viewModel.items.splice(2, 1, 'x', 'y');
- nq(() => validateState(virtualRepeat, viewModel, itemHeight));
- nq(() => done());
+ nq(() => validateState(virtualRepeat, viewModel, itemHeight));
+ nq(() => done());
}
function validateArrayChange(virtualRepeat, viewModel, done) {
@@ -292,12 +298,12 @@ describe('VirtualRepeat Integration', () => {
});
it('handles scrolling to bottom', done => {
- containerCreate.then(() => {
- validateScroll(containerVirtualRepeat, containerViewModel, () => {
- expect(containerVirtualRepeat._onScroll).toHaveBeenCalled();
- done();
- }, 'scrollContainer2');
- });
+ containerCreate.then(() => {
+ validateScroll(containerVirtualRepeat, containerViewModel, () => {
+ expect(containerVirtualRepeat._onScroll).toHaveBeenCalled();
+ done();
+ }, 'scrollContainer2');
+ });
});
it('handles array changes', done => {
@@ -397,39 +403,39 @@ describe('VirtualRepeat Integration', () => {
beforeEach(() => {
items = [];
vm = {
- items: items,
- getNextPage: function() {
- let itemLength = this.items.length;
- for (let i = 0; i < 100; ++i) {
- let itemNum = itemLength + i;
- this.items.push('item' + itemNum);
- }
+ items: items,
+ getNextPage: function () {
+ let itemLength = this.items.length;
+ for (let i = 0; i < 100; ++i) {
+ let itemNum = itemLength + i;
+ this.items.push('item' + itemNum);
}
+ }
};
nestedVm = {
items: items,
bar: [1],
- getNextPage: function(topIndex, isAtBottom, isAtTop) {
+ getNextPage: function (topIndex, isAtBottom, isAtTop) {
let itemLength = this.items.length;
for (let i = 0; i < 100; ++i) {
- let itemNum = itemLength + i;
- this.items.push('item' + itemNum);
+ let itemNum = itemLength + i;
+ this.items.push('item' + itemNum);
}
}
};
promisedVm = {
- items: items,
- test: '2',
- getNextPage: function() {
- return new Promise((resolve, reject) => {
- let itemLength = this.items.length;
- for (let i = 0; i < 100; ++i) {
- let itemNum = itemLength + i;
- this.items.push('item' + itemNum);
- }
- resolve(true);
- });
- }
+ items: items,
+ test: '2',
+ getNextPage: function () {
+ return new Promise((resolve, reject) => {
+ let itemLength = this.items.length;
+ for (let i = 0; i < 100; ++i) {
+ let itemNum = itemLength + i;
+ this.items.push('item' + itemNum);
+ }
+ resolve(true);
+ });
+ }
};
for (let i = 0; i < 1000; ++i) {
items.push('item' + i);
@@ -487,48 +493,49 @@ describe('VirtualRepeat Integration', () => {
});
it('handles scrolling', done => {
- create.then(() => {
- validateScroll(virtualRepeat, viewModel, () => {
- expect(virtualRepeat._onScroll).toHaveBeenCalled();
- done();
- });
+ create.then(() => {
+ validateScroll(virtualRepeat, viewModel, () => {
+ expect(virtualRepeat._onScroll).toHaveBeenCalled();
+ done();
});
+ });
});
+
it('handles getting next data set', done => {
- create.then(() => {
- validateScroll(virtualRepeat, viewModel, () => {
- expect(vm.getNextPage).toHaveBeenCalled();
- done();
- });
+ create.then(() => {
+ validateScroll(virtualRepeat, viewModel, () => {
+ expect(vm.getNextPage).toHaveBeenCalled();
+ done();
});
+ });
});
it('handles getting next data set from nested function', done => {
- nestedCreate.then(() => {
- validateScroll(nestedVirtualRepeat, nestedViewModel, () => {
- expect(nestedVm.getNextPage).toHaveBeenCalled();
- done();
- }, 'scrollContainerNested');
- });
+ nestedCreate.then(() => {
+ validateScroll(nestedVirtualRepeat, nestedViewModel, () => {
+ expect(nestedVm.getNextPage).toHaveBeenCalled();
+ done();
+ }, 'scrollContainerNested');
+ });
});
it('handles getting next data set scrolling up', done => {
create.then(() => {
- validateScrollUp(virtualRepeat, viewModel, () => {
- let args = vm.getNextPage.calls.argsFor(0);
- expect(args[0]).toEqual(0);
- expect(args[1]).toBe(false);
- expect(args[2]).toBe(true);
- done();
- });
+ validateScrollUp(virtualRepeat, viewModel, () => {
+ let args = vm.getNextPage.calls.argsFor(0);
+ expect(args[0]).toEqual(0);
+ expect(args[1]).toBe(false);
+ expect(args[2]).toBe(true);
+ done();
+ });
});
});
it('handles getting next data set with promises', done => {
- promisedCreate.then(() => {
- validateScroll(promisedVirtualRepeat, promisedViewModel, () => {
- // Jasmine spies seem to not be working with returned promises and getting the instance of them, causing regular checks on getNextPage to fail
- expect(promisedVm.items.length).toBe(1100);
- done();
- }, 'scrollContainerPromise');
- });
+ promisedCreate.then(() => {
+ validateScroll(promisedVirtualRepeat, promisedViewModel, () => {
+ // Jasmine spies seem to not be working with returned promises and getting the instance of them, causing regular checks on getNextPage to fail
+ expect(promisedVm.items.length).toBe(1100);
+ done();
+ }, 'scrollContainerPromise');
+ });
});
it('handles getting next data set with small page size', done => {
vm.items = [];
@@ -542,44 +549,49 @@ describe('VirtualRepeat Integration', () => {
});
});
});
- it('handles not scrolling if number of items less than elements in view', done => {
- vm.items = [];
- for (let i = 0; i < 5; ++i) {
- vm.items.push('item' + i);
- }
+ // The following test used to pass because there was no getMore() invoked during initialization
+ // so `validateScroll()` would not have been able to trigger all flow within _handleScroll of VirtualRepeat instance
+ // with the commit to fix issue 129, it starts to have more item and thus, scrollContainer has real scrollbar
+ // making synthesized scroll event in `validateScroll` work, resulting in failed test
+ // kept but commented out for history reason
+ // it('handles not scrolling if number of items less than elements in view', done => {
+ // vm.items = [];
+ // for (let i = 0; i < 5; ++i) {
+ // vm.items.push('item' + i);
+ // }
+ // create.then(() => {
+ // validateScroll(virtualRepeat, viewModel, () => {
+ // expect(vm.getNextPage).not.toHaveBeenCalled();
+ // done();
+ // });
+ // });
+ // });
+ it('passes the current index and location state', done => {
create.then(() => {
validateScroll(virtualRepeat, viewModel, () => {
- expect(vm.getNextPage).not.toHaveBeenCalled();
+ // Taking into account 1 index difference due to default styles on browsers causing small margins of error
+ let args = vm.getNextPage.calls.argsFor(0);
+ expect(args[0]).toBeGreaterThan(988);
+ expect(args[0]).toBeLessThan(995);
+ expect(args[1]).toBe(true);
+ expect(args[2]).toBe(false);
done();
});
});
});
- it('passes the current index and location state', done => {
- create.then(() => {
- validateScroll(virtualRepeat, viewModel, () => {
- // Taking into account 1 index difference due to default styles on browsers causing small margins of error
- let args = vm.getNextPage.calls.argsFor(0);
- expect(args[0]).toBeGreaterThan(988);
- expect(args[0]).toBeLessThan(995);
- expect(args[1]).toBe(true);
- expect(args[2]).toBe(false);
- done();
- });
- });
- });
it('passes context information when using call', done => {
- nestedCreate.then(() => {
- validateScroll(nestedVirtualRepeat, nestedViewModel, () => {
- // Taking into account 1 index difference due to default styles on browsers causing small margins of error
- expect(nestedVm.getNextPage).toHaveBeenCalled();
- let scrollContext = nestedVm.getNextPage.calls.argsFor(0)[0];
- expect(scrollContext.topIndex).toBeGreaterThan(988);
- expect(scrollContext.topIndex).toBeLessThan(995);
- expect(scrollContext.isAtBottom).toBe(true);
- expect(scrollContext.isAtTop).toBe(false);
- done();
- }, 'scrollContainerNested');
- });
+ nestedCreate.then(() => {
+ validateScroll(nestedVirtualRepeat, nestedViewModel, () => {
+ // Taking into account 1 index difference due to default styles on browsers causing small margins of error
+ expect(nestedVm.getNextPage).toHaveBeenCalled();
+ let scrollContext = nestedVm.getNextPage.calls.argsFor(0)[0];
+ expect(scrollContext.topIndex).toBeGreaterThan(988);
+ expect(scrollContext.topIndex).toBeLessThan(995);
+ expect(scrollContext.isAtBottom).toBe(true);
+ expect(scrollContext.isAtTop).toBe(false);
+ done();
+ }, 'scrollContainerNested');
+ });
});
});
});
diff --git a/test/virtual-repeat-integration.table.spec.ts b/test/virtual-repeat-integration.table.spec.ts
index c8cb99f..d01da10 100644
--- a/test/virtual-repeat-integration.table.spec.ts
+++ b/test/virtual-repeat-integration.table.spec.ts
@@ -2,8 +2,9 @@ import './setup';
import { StageComponent, ComponentTester } from 'aurelia-testing';
import { PLATFORM } from 'aurelia-pal';
import { bootstrap } from 'aurelia-bootstrapper';
-import { createAssertionQueue, validateState, Queue } from './utilities';
+import { createAssertionQueue, validateState, Queue, validateScroll } from './utilities';
import { VirtualRepeat } from '../src/virtual-repeat';
+import { IScrollNextScrollContext } from '../src/interfaces';
PLATFORM.moduleName('src/virtual-repeat');
PLATFORM.moduleName('test/noop-value-converter');
@@ -109,6 +110,95 @@ describe('VirtualRepeat Integration', () => {
queue(() => validateState(virtualRepeat, viewModel, itemHeight));
queue(() => validatePush(virtualRepeat, viewModel, done));
});
+
+ describe('with [infinite-scroll-next]', () => {
+
+ beforeEach(() => {
+ resources.push('src/infinite-scroll-next');
+ });
+
+ describe('invoke "_getMore()" when initial amount of items is small', () => {
+
+ it('works with string as value of scroll-next attribute', async () => {
+ view =
+ ``;
+
+ let called = false;
+ viewModel.items = createItems(5);
+ viewModel.getNextPage = jasmine.createSpy('getNextPage()').and.callFake(
+ (topIndex: number, isAtTop: boolean, isAtBottom: boolean) => {
+ expect(topIndex).toBe(0);
+ expect(isAtTop).toBe(true);
+ expect(isAtBottom).toBe(true);
+ called = true;
+ }
+ );
+
+ await bootstrapComponent();
+
+ expect(virtualRepeat['_fixedHeightContainer']).toBe(true);
+ expect(called).toBe(true);
+ expect(viewModel.getNextPage).toHaveBeenCalledTimes(1);
+ });
+
+ it('works with call binding expression', async () => {
+ view =
+ ``;
+
+ let scrollContext: IScrollNextScrollContext;
+ viewModel.items = createItems(5);
+ viewModel.getNextPage = jasmine.createSpy('getNextPage()').and.callFake(($scrollContext: IScrollNextScrollContext) => {
+ scrollContext = $scrollContext;
+ });
+
+ await bootstrapComponent();
+
+ expect(virtualRepeat['_fixedHeightContainer']).toBe(true);
+ expect(scrollContext).toBeDefined();
+ expect(scrollContext.isAtTop).toBe(true);
+ expect(scrollContext.isAtBottom).toBe(true, 'Expected is at bottom to be true, recevied:' + scrollContext.isAtBottom);
+ expect(scrollContext.topIndex).toBe(0);
+ expect(viewModel.getNextPage).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not work with normal binding expression', async () => {
+ view =
+ ``;
+
+ viewModel.items = createItems(5);
+ viewModel.getNextPage = jasmine.createSpy('getNextPage()');
+
+ await bootstrapComponent();
+
+ expect(virtualRepeat['_fixedHeightContainer']).toBe(true);
+ expect(viewModel.getNextPage).not.toHaveBeenCalled();
+ });
+ });
+ });
});
function validatePush(virtualRepeat: VirtualRepeat, viewModel: any, done: Function) {
@@ -124,15 +214,14 @@ describe('VirtualRepeat Integration', () => {
}
function validateArrayChange(virtualRepeat, viewModel, done) {
- const createItems = (name: string, amount: number) => new Array(amount).map((v, index) => name + index);
- viewModel.items = createItems('A', 4);
+ viewModel.items = createItems(4, 'A');
queue(() => validateState(virtualRepeat, viewModel, itemHeight));
- queue(() => viewModel.items = createItems('B', 0));
+ queue(() => viewModel.items = createItems(0, 'B'));
queue(() => validateState(virtualRepeat, viewModel, itemHeight));
- queue(() => viewModel.items = createItems('C', 101));
+ queue(() => viewModel.items = createItems(101, 'C'));
queue(() => validateState(virtualRepeat, viewModel, itemHeight));
- queue(() => viewModel.items = createItems('D', 0));
+ queue(() => viewModel.items = createItems(0, 'D'));
queue(() => validateState(virtualRepeat, viewModel, itemHeight));
queue(() => done());
}
@@ -146,4 +235,8 @@ describe('VirtualRepeat Integration', () => {
virtualRepeat = component.viewModel;
return { virtualRepeat, viewModel, component: component };
}
+
+ function createItems(amount: number, name: string = 'item') {
+ return Array.from({ length: amount }, (_, index) => name + index);
+ }
});