Skip to content

Commit 326f0ee

Browse files
committed
fix(virtual-scroll): move views that are already attached instead of inserting
1 parent 9473ce9 commit 326f0ee

File tree

2 files changed

+63
-36
lines changed

2 files changed

+63
-36
lines changed

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

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,10 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
287287
adjustedPreviousIndex: number | null,
288288
currentIndex: number | null) => {
289289
if (record.previousIndex == null) { // Item added.
290-
const view = this._getViewForNewItem();
291-
this._viewContainerRef.insert(view, currentIndex!);
290+
const view = this._insertViewForNewItem(currentIndex!);
292291
view.context.$implicit = record.item;
293292
} else if (currentIndex == null) { // Item removed.
294-
this._cacheView(this._viewContainerRef.detach(adjustedPreviousIndex!) as
295-
EmbeddedViewRef<CdkVirtualForOfContext<T>>);
293+
this._cacheView(this._detachView(adjustedPreviousIndex !));
296294
} else { // Item moved.
297295
const view = this._viewContainerRef.get(adjustedPreviousIndex!) as
298296
EmbeddedViewRef<CdkVirtualForOfContext<T>>;
@@ -337,18 +335,9 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
337335
}
338336
}
339337

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

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

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

Lines changed: 25 additions & 20 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 {ComponentFixture, fakeAsync, flush, inject, TestBed} from '@angular/core/testing';
@@ -486,49 +485,51 @@ describe('CdkVirtualScrollViewport', () => {
486485

487486
it('should trackBy value by default', fakeAsync(() => {
488487
testComponent.items = [];
489-
spyOn(testComponent.virtualForViewContainer, 'detach').and.callThrough();
488+
spyOn<any>(testComponent.virtualForOf, '_detachView').and.callThrough();
490489
finishInit(fixture);
491490

492491
testComponent.items = [0];
493492
fixture.detectChanges();
494493
flush();
495494

496-
expect(testComponent.virtualForViewContainer.detach).not.toHaveBeenCalled();
495+
expect((testComponent.virtualForOf as any)._detachView).not.toHaveBeenCalled();
497496

498497
testComponent.items = [1];
499498
fixture.detectChanges();
500499
flush();
501500

502-
expect(testComponent.virtualForViewContainer.detach).toHaveBeenCalled();
501+
expect((testComponent.virtualForOf as any)._detachView).toHaveBeenCalled();
503502
}));
504503

505504
it('should trackBy index when specified', fakeAsync(() => {
506505
testComponent.trackBy = i => i;
507506
testComponent.items = [];
508-
spyOn(testComponent.virtualForViewContainer, 'detach').and.callThrough();
507+
spyOn<any>(testComponent.virtualForOf, '_detachView').and.callThrough();
509508
finishInit(fixture);
510509

511510
testComponent.items = [0];
512511
fixture.detectChanges();
513512
flush();
514513

515-
expect(testComponent.virtualForViewContainer.detach).not.toHaveBeenCalled();
514+
expect((testComponent.virtualForOf as any)._detachView).not.toHaveBeenCalled();
516515

517516
testComponent.items = [1];
518517
fixture.detectChanges();
519518
flush();
520519

521-
expect(testComponent.virtualForViewContainer.detach).not.toHaveBeenCalled();
520+
expect((testComponent.virtualForOf as any)._detachView).not.toHaveBeenCalled();
522521
}));
523522

524523
it('should recycle views when template cache is large enough to accommodate', fakeAsync(() => {
525524
testComponent.trackBy = i => i;
526-
const spy =
527-
spyOn(testComponent.virtualForViewContainer, 'createEmbeddedView').and.callThrough();
525+
const spy = spyOn<any>(testComponent.virtualForOf, '_createEmbeddedViewAt')
526+
.and.callThrough();
527+
528528
finishInit(fixture);
529529

530530
// Should create views for the initial rendered items.
531-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(4);
531+
expect((testComponent.virtualForOf as any)._createEmbeddedViewAt)
532+
.toHaveBeenCalledTimes(4);
532533

533534
spy.calls.reset();
534535
triggerScroll(viewport, 10);
@@ -538,7 +539,8 @@ describe('CdkVirtualScrollViewport', () => {
538539
// As we first start to scroll we need to create one more item. This is because the first item
539540
// is still partially on screen and therefore can't be removed yet. At the same time a new
540541
// item is now partially on the screen at the bottom and so a new view is needed.
541-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(1);
542+
expect((testComponent.virtualForOf as any)._createEmbeddedViewAt)
543+
.toHaveBeenCalledTimes(1);
542544

543545
spy.calls.reset();
544546
const maxOffset =
@@ -551,18 +553,21 @@ describe('CdkVirtualScrollViewport', () => {
551553

552554
// As we scroll through the rest of the items, no new views should be created, our existing 5
553555
// can just be recycled as appropriate.
554-
expect(testComponent.virtualForViewContainer.createEmbeddedView).not.toHaveBeenCalled();
556+
expect((testComponent.virtualForOf as any)._createEmbeddedViewAt)
557+
.not.toHaveBeenCalled();
555558
}));
556559

557560
it('should not recycle views when template cache is full', fakeAsync(() => {
558561
testComponent.trackBy = i => i;
559562
testComponent.templateCacheSize = 0;
560-
const spy =
561-
spyOn(testComponent.virtualForViewContainer, 'createEmbeddedView').and.callThrough();
562-
finishInit(fixture);
563+
const spy = spyOn<any>(testComponent.virtualForOf, '_createEmbeddedViewAt')
564+
.and.callThrough();
565+
566+
finishInit(fixture);
563567

564568
// Should create views for the initial rendered items.
565-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(4);
569+
expect((testComponent.virtualForOf as any)._createEmbeddedViewAt)
570+
.toHaveBeenCalledTimes(4);
566571

567572
spy.calls.reset();
568573
triggerScroll(viewport, 10);
@@ -572,7 +577,8 @@ describe('CdkVirtualScrollViewport', () => {
572577
// As we first start to scroll we need to create one more item. This is because the first item
573578
// is still partially on screen and therefore can't be removed yet. At the same time a new
574579
// item is now partially on the screen at the bottom and so a new view is needed.
575-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(1);
580+
expect((testComponent.virtualForOf as any)._createEmbeddedViewAt)
581+
.toHaveBeenCalledTimes(1);
576582

577583
spy.calls.reset();
578584
const maxOffset =
@@ -585,7 +591,8 @@ describe('CdkVirtualScrollViewport', () => {
585591

586592
// Since our template cache size is 0, as we scroll through the rest of the items, we need to
587593
// create a new view for each one.
588-
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(5);
594+
expect((testComponent.virtualForOf as any)._createEmbeddedViewAt)
595+
.toHaveBeenCalledTimes(5);
589596
}));
590597

591598
it('should render up to maxBufferPx when buffer dips below minBufferPx', fakeAsync(() => {
@@ -829,7 +836,6 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) {
829836
class FixedSizeVirtualScroll {
830837
@ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;
831838
@ViewChild(CdkVirtualForOf) virtualForOf: CdkVirtualForOf<any>;
832-
@ViewChild(CdkVirtualForOf, {read: ViewContainerRef}) virtualForViewContainer: ViewContainerRef;
833839

834840
@Input() orientation = 'vertical';
835841
@Input() viewportSize = 200;
@@ -880,7 +886,6 @@ class FixedSizeVirtualScroll {
880886
})
881887
class FixedSizeVirtualScrollWithRtlDirection {
882888
@ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;
883-
@ViewChild(CdkVirtualForOf, {read: ViewContainerRef}) virtualForViewContainer: ViewContainerRef;
884889

885890
@Input() orientation = 'vertical';
886891
@Input() viewportSize = 200;

0 commit comments

Comments
 (0)