Skip to content

Commit 8dc572d

Browse files
karajosephperrott
authored andcommitted
fix(virtual-scroll): move views that are already attached instead of inserting (#15348)
1 parent b2f2bd3 commit 8dc572d

File tree

2 files changed

+65
-37
lines changed

2 files changed

+65
-37
lines changed

src/cdk/scrolling/virtual-for-of.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -293,12 +293,10 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
293293
adjustedPreviousIndex: number | null,
294294
currentIndex: number | null) => {
295295
if (record.previousIndex == null) { // Item added.
296-
const view = this._getViewForNewItem();
297-
this._viewContainerRef.insert(view, currentIndex!);
296+
const view = this._insertViewForNewItem(currentIndex!);
298297
view.context.$implicit = record.item;
299298
} else if (currentIndex == null) { // Item removed.
300-
this._cacheView(this._viewContainerRef.detach(adjustedPreviousIndex!) as
301-
EmbeddedViewRef<CdkVirtualForOfContext<T>>);
299+
this._cacheView(this._detachView(adjustedPreviousIndex !));
302300
} else { // Item moved.
303301
const view = this._viewContainerRef.get(adjustedPreviousIndex!) as
304302
EmbeddedViewRef<CdkVirtualForOfContext<T>>;
@@ -343,18 +341,9 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
343341
}
344342
}
345343

346-
/** Get a view for a new item, either from the cache or by creating a new one. */
347-
private _getViewForNewItem(): EmbeddedViewRef<CdkVirtualForOfContext<T>> {
348-
return this._templateCache.pop() || this._viewContainerRef.createEmbeddedView(this._template, {
349-
$implicit: null!,
350-
cdkVirtualForOf: this._cdkVirtualForOf,
351-
index: -1,
352-
count: -1,
353-
first: false,
354-
last: false,
355-
odd: false,
356-
even: false
357-
});
344+
/** Inserts a view for a new item, either from the cache or by creating a new one. */
345+
private _insertViewForNewItem(index: number): EmbeddedViewRef<CdkVirtualForOfContext<T>> {
346+
return this._insertViewFromCache(index) || this._createEmbeddedViewAt(index);
358347
}
359348

360349
/** Update the computed properties on the `CdkVirtualForOfContext`. */
@@ -364,4 +353,37 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
364353
context.even = context.index % 2 === 0;
365354
context.odd = !context.even;
366355
}
356+
357+
/** Creates a new embedded view and moves it to the given index */
358+
private _createEmbeddedViewAt(index: number): EmbeddedViewRef<CdkVirtualForOfContext<T>> {
359+
const view = this._viewContainerRef.createEmbeddedView(this._template, {
360+
$implicit: null!,
361+
cdkVirtualForOf: this._cdkVirtualForOf,
362+
index: -1,
363+
count: -1,
364+
first: false,
365+
last: false,
366+
odd: false,
367+
even: false
368+
});
369+
if (index < this._viewContainerRef.length) {
370+
this._viewContainerRef.move(view, index);
371+
}
372+
return view;
373+
}
374+
375+
/** Inserts a recycled view from the cache at the given index. */
376+
private _insertViewFromCache(index: number): EmbeddedViewRef<CdkVirtualForOfContext<T>>|null {
377+
const cachedView = this._templateCache.pop();
378+
if (cachedView) {
379+
this._viewContainerRef.insert(cachedView, index);
380+
}
381+
return cachedView || null;
382+
}
383+
384+
/** Detaches the embedded view at the given index. */
385+
private _detachView(index: number): EmbeddedViewRef<CdkVirtualForOfContext<T>> {
386+
return this._viewContainerRef.detach(index) as
387+
EmbeddedViewRef<CdkVirtualForOfContext<T>>;
388+
}
367389
}

src/cdk/scrolling/virtual-scroll-viewport.spec.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
NgZone,
1313
TrackByFunction,
1414
ViewChild,
15-
ViewContainerRef,
1615
ViewEncapsulation
1716
} from '@angular/core';
1817
import {async, ComponentFixture, fakeAsync, flush, inject, TestBed} from '@angular/core/testing';
@@ -488,49 +487,51 @@ describe('CdkVirtualScrollViewport', () => {
488487

489488
it('should trackBy value by default', fakeAsync(() => {
490489
testComponent.items = [];
491-
spyOn(testComponent.virtualForViewContainer, 'detach').and.callThrough();
490+
spyOn(testComponent.virtualForOf, '_detachView').and.callThrough();
492491
finishInit(fixture);
493492

494493
testComponent.items = [0];
495494
fixture.detectChanges();
496495
flush();
497496

498-
expect(testComponent.virtualForViewContainer.detach).not.toHaveBeenCalled();
497+
expect(testComponent.virtualForOf._detachView).not.toHaveBeenCalled();
499498

500499
testComponent.items = [1];
501500
fixture.detectChanges();
502501
flush();
503502

504-
expect(testComponent.virtualForViewContainer.detach).toHaveBeenCalled();
503+
expect(testComponent.virtualForOf._detachView).toHaveBeenCalled();
505504
}));
506505

507506
it('should trackBy index when specified', fakeAsync(() => {
508507
testComponent.trackBy = i => i;
509508
testComponent.items = [];
510-
spyOn(testComponent.virtualForViewContainer, 'detach').and.callThrough();
509+
spyOn(testComponent.virtualForOf, '_detachView').and.callThrough();
511510
finishInit(fixture);
512511

513512
testComponent.items = [0];
514513
fixture.detectChanges();
515514
flush();
516515

517-
expect(testComponent.virtualForViewContainer.detach).not.toHaveBeenCalled();
516+
expect(testComponent.virtualForOf._detachView).not.toHaveBeenCalled();
518517

519518
testComponent.items = [1];
520519
fixture.detectChanges();
521520
flush();
522521

523-
expect(testComponent.virtualForViewContainer.detach).not.toHaveBeenCalled();
522+
expect(testComponent.virtualForOf._detachView).not.toHaveBeenCalled();
524523
}));
525524

526525
it('should recycle views when template cache is large enough to accommodate', fakeAsync(() => {
527526
testComponent.trackBy = i => i;
528-
const spy =
529-
spyOn(testComponent.virtualForViewContainer, 'createEmbeddedView').and.callThrough();
527+
const spy = spyOn(testComponent.virtualForOf, '_createEmbeddedViewAt')
528+
.and.callThrough();
529+
530530
finishInit(fixture);
531531

532532
// Should create views for the initial rendered items.
533-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(4);
533+
expect(testComponent.virtualForOf._createEmbeddedViewAt)
534+
.toHaveBeenCalledTimes(4);
534535

535536
spy.calls.reset();
536537
triggerScroll(viewport, 10);
@@ -540,7 +541,8 @@ describe('CdkVirtualScrollViewport', () => {
540541
// As we first start to scroll we need to create one more item. This is because the first item
541542
// is still partially on screen and therefore can't be removed yet. At the same time a new
542543
// item is now partially on the screen at the bottom and so a new view is needed.
543-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(1);
544+
expect(testComponent.virtualForOf._createEmbeddedViewAt)
545+
.toHaveBeenCalledTimes(1);
544546

545547
spy.calls.reset();
546548
const maxOffset =
@@ -553,18 +555,21 @@ describe('CdkVirtualScrollViewport', () => {
553555

554556
// As we scroll through the rest of the items, no new views should be created, our existing 5
555557
// can just be recycled as appropriate.
556-
expect(testComponent.virtualForViewContainer.createEmbeddedView).not.toHaveBeenCalled();
558+
expect(testComponent.virtualForOf._createEmbeddedViewAt)
559+
.not.toHaveBeenCalled();
557560
}));
558561

559562
it('should not recycle views when template cache is full', fakeAsync(() => {
560563
testComponent.trackBy = i => i;
561564
testComponent.templateCacheSize = 0;
562-
const spy =
563-
spyOn(testComponent.virtualForViewContainer, 'createEmbeddedView').and.callThrough();
564-
finishInit(fixture);
565+
const spy = spyOn(testComponent.virtualForOf, '_createEmbeddedViewAt')
566+
.and.callThrough();
567+
568+
finishInit(fixture);
565569

566570
// Should create views for the initial rendered items.
567-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(4);
571+
expect(testComponent.virtualForOf._createEmbeddedViewAt)
572+
.toHaveBeenCalledTimes(4);
568573

569574
spy.calls.reset();
570575
triggerScroll(viewport, 10);
@@ -574,7 +579,8 @@ describe('CdkVirtualScrollViewport', () => {
574579
// As we first start to scroll we need to create one more item. This is because the first item
575580
// is still partially on screen and therefore can't be removed yet. At the same time a new
576581
// item is now partially on the screen at the bottom and so a new view is needed.
577-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(1);
582+
expect(testComponent.virtualForOf._createEmbeddedViewAt)
583+
.toHaveBeenCalledTimes(1);
578584

579585
spy.calls.reset();
580586
const maxOffset =
@@ -587,7 +593,8 @@ describe('CdkVirtualScrollViewport', () => {
587593

588594
// Since our template cache size is 0, as we scroll through the rest of the items, we need to
589595
// create a new view for each one.
590-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(5);
596+
expect(testComponent.virtualForOf._createEmbeddedViewAt)
597+
.toHaveBeenCalledTimes(5);
591598
}));
592599

593600
it('should render up to maxBufferPx when buffer dips below minBufferPx', fakeAsync(() => {
@@ -830,8 +837,8 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) {
830837
})
831838
class FixedSizeVirtualScroll {
832839
@ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;
833-
@ViewChild(CdkVirtualForOf) virtualForOf: CdkVirtualForOf<any>;
834-
@ViewChild(CdkVirtualForOf, {read: ViewContainerRef}) virtualForViewContainer: ViewContainerRef;
840+
// Casting virtualForOf as any so we can spy on private methods
841+
@ViewChild(CdkVirtualForOf) virtualForOf: any;
835842

836843
@Input() orientation = 'vertical';
837844
@Input() viewportSize = 200;
@@ -882,7 +889,6 @@ class FixedSizeVirtualScroll {
882889
})
883890
class FixedSizeVirtualScrollWithRtlDirection {
884891
@ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;
885-
@ViewChild(CdkVirtualForOf, {read: ViewContainerRef}) virtualForViewContainer: ViewContainerRef;
886892

887893
@Input() orientation = 'vertical';
888894
@Input() viewportSize = 200;

0 commit comments

Comments
 (0)