Skip to content

Commit b861aa6

Browse files
committed
fix(virtual-repeat): handle array changes correctly
Resolves: #106, #98, #89 Previous behavior did not handle array changes correctly (in particular changes with the size of the array). Behavior now allows for array changes with different sizes, and should keep appropriate scroll positions.
1 parent cc34dc6 commit b861aa6

File tree

3 files changed

+91
-16
lines changed

3 files changed

+91
-16
lines changed

src/array-virtual-repeat-strategy.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy {
1515
* @param repeat The repeater instance.
1616
* @param items The new array instance.
1717
*/
18-
instanceChanged(repeat: VirtualRepeat, items: Array<any>): void {
19-
this._inPlaceProcessItems(repeat, items);
18+
instanceChanged(repeat: VirtualRepeat, items: Array<any>, first: number): void {
19+
this._inPlaceProcessItems(repeat, items, first);
2020
}
2121

2222
_standardProcessInstanceChanged(repeat: VirtualRepeat, items: Array<any>): void {
@@ -26,10 +26,16 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy {
2626
}
2727
}
2828

29-
_inPlaceProcessItems(repeat: VirtualRepeat, items: Array<any>): void {
29+
_inPlaceProcessItems(repeat: VirtualRepeat, items: Array<any>, first: number): void {
3030
let itemsLength = items.length;
3131
let viewsLength = repeat.viewCount();
32-
let first = repeat._getIndexOfFirstView();
32+
/*
33+
Get index of first view is looking at the view which is from the ViewSlot
34+
The view slot has not yet been updated with the new list
35+
New first has to be the calculated "first" in our view slot, so the first one that's going to be rendered
36+
To figure out that one, we're going to have to know where we are in our scrolling so we can know how far down we've gone to show the first view
37+
That "first" is calculated and passed into here
38+
*/
3339
// remove unneeded views.
3440
while (viewsLength > itemsLength) {
3541
viewsLength--;
@@ -51,6 +57,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy {
5157
view.bindingContext[local] = items[i + first];
5258
view.overrideContext.$middle = middle;
5359
view.overrideContext.$last = last;
60+
view.overrideContext.$index = i + first;
5461
repeat.updateBindings(view);
5562
}
5663
// add new views

src/virtual-repeat.js

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,47 @@ export class VirtualRepeat extends AbstractRepeater {
154154
if (!this.scope) {
155155
return;
156156
}
157+
let reducingItems = false;
158+
let previousLastViewIndex = this._getIndexOfLastView();
159+
157160
let items = this.items;
158161
this.strategy = this.strategyLocator.getStrategy(items);
159162
if (items.length > 0 && this.viewCount() === 0) {
160163
this.strategy.createFirstItem(this);
161164
}
165+
// Skip scroll handling if we are decreasing item list
166+
// Otherwise if expanding list, call the handle scroll below
167+
if (this._itemsLength >= items.length) {
168+
//Scroll handle is redundant in this case since the instanceChanged will re-evaluate orderings
169+
// Also, when items are reduced, we're not having to move any bindings, just a straight rebind of the items in the list
170+
this._skipNextScrollHandle = true;
171+
reducingItems = true;
172+
}
173+
this._checkFixedHeightContainer();
162174
this._calcInitialHeights(items.length);
163175
if (!this.isOneTime && !this._observeInnerCollection()) {
164176
this._observeCollection();
165177
}
178+
this.strategy.instanceChanged(this, items, this._first);
179+
this._lastRebind = this._first; //Reset rebinding
180+
181+
if (reducingItems && previousLastViewIndex > this.items.length - 1) {
182+
//Do we need to set scrolltop so that we appear at the bottom of the list to match scrolling as far as we could?
183+
//We only want to execute this line if we're reducing such that it brings us to the bottom of the new list
184+
this.scrollContainer.scrollTop = this.scrollContainer.scrollTop + (this.viewCount() * this.itemHeight);
185+
}
186+
if (!reducingItems) {
187+
// If we're expanding our items, then we need to reset our previous first for the next go around of scroll handling
188+
this._previousFirst = this._first;
189+
this._scrollingDown = true; //Simulating the down scroll event to load up data appropriately
190+
this._scrollingUp = false;
191+
192+
//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)
193+
this.isLastIndex = this._getIndexOfLastView() >= this.items.length - 1;
194+
}
166195

167-
this.strategy.instanceChanged(this, items, this._viewsLength);
196+
//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
197+
this._handleScroll();
168198
}
169199

170200
unbind(): void {
@@ -215,6 +245,10 @@ export class VirtualRepeat extends AbstractRepeater {
215245
if (!this._isAttached) {
216246
return;
217247
}
248+
if (this._skipNextScrollHandle) {
249+
this._skipNextScrollHandle = false;
250+
return;
251+
}
218252
let itemHeight = this.itemHeight;
219253
let scrollTop = this._fixedHeightContainer ? this.scrollContainer.scrollTop : pageYOffset - this.distanceToTop;
220254
this._first = Math.floor(scrollTop / itemHeight);
@@ -346,6 +380,12 @@ export class VirtualRepeat extends AbstractRepeater {
346380
}
347381
}
348382

383+
_checkFixedHeightContainer(): void {
384+
if (this.domHelper.hasOverflowScroll(this.scrollContainer)) {
385+
this._fixedHeightContainer = true;
386+
}
387+
}
388+
349389
_adjustBufferHeights(): void {
350390
this.topBuffer.style.height = `${this._topBufferHeight}px`;
351391
this.bottomBuffer.style.height = `${this._bottomBufferHeight}px`;
@@ -422,20 +462,38 @@ export class VirtualRepeat extends AbstractRepeater {
422462
}, 500);
423463
return;
424464
}
465+
425466
this._itemsLength = itemsLength;
426467
this.scrollContainerHeight = this._fixedHeightContainer ? this._calcScrollHeight(this.scrollContainer) : document.documentElement.clientHeight;
427468
this.elementsInView = Math.ceil(this.scrollContainerHeight / this.itemHeight) + 1;
428469
this._viewsLength = (this.elementsInView * 2) + this._bufferSize;
429-
this._bottomBufferHeight = this.itemHeight * itemsLength - this.itemHeight * this._viewsLength;
430-
if (this._bottomBufferHeight < 0) {
470+
471+
//Look at top buffer height (how far we've scrolled down)
472+
//If top buffer height is greater than the new bottom buffer height (how far we *can* scroll down)
473+
// Then set top buffer height to max it can be (bottom buffer height - views in length?) and bottom buffer height to 0
474+
let newBottomBufferHeight = this.itemHeight * itemsLength - this.itemHeight * this._viewsLength; //How much buffer room to the bottom if you were at the top
475+
if (newBottomBufferHeight < 0) { // In case of small lists, ensure that we never set the buffer heights to impossible values
476+
newBottomBufferHeight = 0;
477+
}
478+
if (this._topBufferHeight >= newBottomBufferHeight) { //Use case when items are removed (we've scrolled past where we can)
479+
this._topBufferHeight = newBottomBufferHeight;
431480
this._bottomBufferHeight = 0;
481+
this._first = this._itemsLength - this._viewsLength;
482+
if (this._first < 0) { // In case of small lists, ensure that we never set first to less than possible
483+
this._first = 0;
484+
}
485+
} else { //Use case when items are added (we are adding scrollable space to the bottom)
486+
// 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
487+
this._first = this._getIndexOfFirstView();
488+
let adjustedTopBufferHeight = this._first * this.itemHeight; //appropriate buffer height for top, might be 1 too long...
489+
this._topBufferHeight = adjustedTopBufferHeight;
490+
//But what about when we've only scrolled slightly down the list? We need to readjust the top buffer height then
491+
this._bottomBufferHeight = newBottomBufferHeight - adjustedTopBufferHeight;
492+
if (this._bottomBufferHeight < 0) {
493+
this._bottomBufferHeight = 0;
494+
}
432495
}
433-
this.bottomBuffer.style.height = `${this._bottomBufferHeight}px`;
434-
this._topBufferHeight = 0;
435-
this.topBuffer.style.height = `${this._topBufferHeight}px`;
436-
// TODO This will cause scrolling back to top when swapping collection instances that have different lengths - instead should keep the scroll position
437-
this.scrollContainer.scrollTop = 0;
438-
this._first = 0;
496+
this._adjustBufferHeights();
439497
return;
440498
}
441499

@@ -480,6 +538,7 @@ export class VirtualRepeat extends AbstractRepeater {
480538
}
481539

482540
// @override AbstractRepeater
541+
// How will these behaviors need to change since we are in a virtual list instead?
483542
viewCount() { return this.viewSlot.children.length; }
484543
views() { return this.viewSlot.children; }
485544
view(index) { return this.viewSlot.children[index]; }

test/virtual-repeat-integration.spec.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ describe('VirtualRepeat Integration', () => {
176176
nq(() => done());
177177
}
178178

179+
function validateArrayChange(virtualRepeat, viewModel, done) {
180+
viewModel.items = ['Foo B: 1', 'Foo B: 2', 'Foo B: 3', 'Foo B: 4', 'Foo B: 5'];
181+
nq(() => validateState(virtualRepeat, viewModel));
182+
nq(() => done());
183+
}
184+
179185
describe('iterating div', () => {
180186
let component;
181187
let virtualRepeat;
@@ -307,7 +313,6 @@ describe('VirtualRepeat Integration', () => {
307313
nq(() => done());
308314
});
309315
});
310-
311316
});
312317

313318
it('handles push', done => {
@@ -354,6 +359,10 @@ describe('VirtualRepeat Integration', () => {
354359
}, 'scrollContainer2')
355360
});
356361
});
362+
363+
it('handles array changes', done => {
364+
create.then(() => validateArrayChange(virtualRepeat, viewModel, done));
365+
});
357366
});
358367

359368
describe('iterating table', () => {
@@ -609,7 +618,7 @@ describe('VirtualRepeat Integration', () => {
609618
//Taking into account 1 index difference due to default styles on browsers causing small margins of error
610619
var args = vm.getNextPage.calls.argsFor(0);
611620
expect(args[0]).toBeGreaterThan(988);
612-
expect(args[0]).toBeLessThan(991);
621+
expect(args[0]).toBeLessThan(995);
613622
expect(args[1]).toBe(true);
614623
expect(args[2]).toBe(false);
615624
done();
@@ -623,7 +632,7 @@ describe('VirtualRepeat Integration', () => {
623632
expect(nestedVm.getNextPage).toHaveBeenCalled();
624633
var scrollContext = nestedVm.getNextPage.calls.argsFor(0)[0];
625634
expect(scrollContext.topIndex).toBeGreaterThan(988);
626-
expect(scrollContext.topIndex).toBeLessThan(991);
635+
expect(scrollContext.topIndex).toBeLessThan(995);
627636
expect(scrollContext.isAtBottom).toBe(true);
628637
expect(scrollContext.isAtTop).toBe(false);
629638
done();

0 commit comments

Comments
 (0)