diff --git a/src/cdk/scrolling/BUILD.bazel b/src/cdk/scrolling/BUILD.bazel index 7817d332def2..3585e2a20c78 100644 --- a/src/cdk/scrolling/BUILD.bazel +++ b/src/cdk/scrolling/BUILD.bazel @@ -45,6 +45,7 @@ ng_test_library( "//src/cdk/bidi", "//src/cdk/collections", "//src/cdk/testing/private", + "@npm//@angular/common", "@npm//rxjs", ], ) diff --git a/src/cdk/scrolling/virtual-for-of.ts b/src/cdk/scrolling/virtual-for-of.ts index fc10bf729fca..6f6db6eee3e4 100644 --- a/src/cdk/scrolling/virtual-for-of.ts +++ b/src/cdk/scrolling/virtual-for-of.ts @@ -288,7 +288,11 @@ export class CdkVirtualForOf implements } this._renderedItems = this._data.slice(this._renderedRange.start, this._renderedRange.end); if (!this._differ) { - this._differ = this._differs.find(this._renderedItems).create(this.cdkVirtualForTrackBy); + // Use a wrapper function for the `trackBy` so any new values are + // picked up automatically without having to recreate the differ. + this._differ = this._differs.find(this._renderedItems).create((index, item) => { + return this.cdkVirtualForTrackBy ? this.cdkVirtualForTrackBy(index, item) : item; + }); } this._needsUpdate = true; } @@ -310,7 +314,7 @@ export class CdkVirtualForOf implements const count = this._data.length; let i = this._viewContainerRef.length; while (i--) { - let view = this._viewContainerRef.get(i) as EmbeddedViewRef>; + const view = this._viewContainerRef.get(i) as EmbeddedViewRef>; view.context.index = this._renderedRange.start + i; view.context.count = count; this._updateComputedContextProperties(view.context); @@ -324,7 +328,7 @@ export class CdkVirtualForOf implements changes, this._viewContainerRef, (record: IterableChangeRecord, - adjustedPreviousIndex: number | null, + _adjustedPreviousIndex: number | null, currentIndex: number | null) => this._getEmbeddedViewArgs(record, currentIndex!), (record) => record.item); diff --git a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts index 1629e06d3609..2ac1602f2a8b 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.spec.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.spec.ts @@ -5,10 +5,10 @@ import { ScrollDispatcher, ScrollingModule } from '@angular/cdk/scrolling'; +import {CommonModule} from '@angular/common'; import {dispatchFakeEvent} from '@angular/cdk/testing/private'; import { Component, - Input, NgZone, TrackByFunction, ViewChild, @@ -29,7 +29,7 @@ import {animationFrameScheduler, Subject} from 'rxjs'; describe('CdkVirtualScrollViewport', () => { - describe ('with FixedSizeVirtualScrollStrategy', () => { + describe('with FixedSizeVirtualScrollStrategy', () => { let fixture: ComponentFixture; let testComponent: FixedSizeVirtualScroll; let viewport: CdkVirtualScrollViewport; @@ -899,6 +899,35 @@ describe('CdkVirtualScrollViewport', () => { .toEqual(['0', '1', '2', '3', '4', '5', '6', '7']); })); }); + + describe('with delayed initialization', () => { + let fixture: ComponentFixture; + let testComponent: DelayedInitializationVirtualScroll; + let viewport: CdkVirtualScrollViewport; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ScrollingModule, CommonModule], + declarations: [DelayedInitializationVirtualScroll], + }).compileComponents(); + fixture = TestBed.createComponent(DelayedInitializationVirtualScroll); + testComponent = fixture.componentInstance; + viewport = testComponent.viewport; + })); + + it('should call custom trackBy when virtual for is added after init', fakeAsync(() => { + finishInit(fixture); + expect(testComponent.trackBy).not.toHaveBeenCalled(); + + testComponent.renderVirtualFor = true; + fixture.detectChanges(); + triggerScroll(viewport, testComponent.itemSize * 5); + fixture.detectChanges(); + flush(); + + expect(testComponent.trackBy).toHaveBeenCalled(); + })); + }); }); @@ -973,15 +1002,15 @@ class FixedSizeVirtualScroll { // Casting virtualForOf as any so we can spy on private methods @ViewChild(CdkVirtualForOf, {static: true}) virtualForOf: any; - @Input() orientation = 'vertical'; - @Input() viewportSize = 200; - @Input() viewportCrossSize = 100; - @Input() itemSize = 50; - @Input() minBufferPx = 0; - @Input() maxBufferPx = 0; - @Input() items = Array(10).fill(0).map((_, i) => i); - @Input() trackBy: TrackByFunction; - @Input() templateCacheSize = 20; + orientation = 'vertical'; + viewportSize = 200; + viewportCrossSize = 100; + itemSize = 50; + minBufferPx = 0; + maxBufferPx = 0; + items = Array(10).fill(0).map((_, i) => i); + trackBy: TrackByFunction; + templateCacheSize = 20; scrolledToIndex = 0; hasMargin = false; @@ -1033,15 +1062,15 @@ class FixedSizeVirtualScroll { class FixedSizeVirtualScrollWithRtlDirection { @ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport; - @Input() orientation = 'vertical'; - @Input() viewportSize = 200; - @Input() viewportCrossSize = 100; - @Input() itemSize = 50; - @Input() minBufferPx = 0; - @Input() maxBufferPx = 0; - @Input() items = Array(10).fill(0).map((_, i) => i); - @Input() trackBy: TrackByFunction; - @Input() templateCacheSize = 20; + orientation = 'vertical'; + viewportSize = 200; + viewportCrossSize = 100; + itemSize = 50; + minBufferPx = 0; + maxBufferPx = 0; + items = Array(10).fill(0).map((_, i) => i); + trackBy: TrackByFunction; + templateCacheSize = 20; scrolledToIndex = 0; @@ -1116,3 +1145,40 @@ class VirtualScrollWithItemInjectingViewContainer { items = Array(20000).fill(0).map((_, i) => i); } + +@Component({ + template: ` + + +
{{item}}
+
+
+ `, + styles: [` + .cdk-virtual-scroll-content-wrapper { + display: flex; + flex-direction: column; + } + + .cdk-virtual-scroll-viewport { + width: 200px; + height: 200px; + background-color: #f5f5f5; + } + + .item { + width: 100%; + height: 50px; + box-sizing: border-box; + border: 1px dashed #ccc; + } + `], + encapsulation: ViewEncapsulation.None, +}) +class DelayedInitializationVirtualScroll { + @ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport; + itemSize = 50; + items = Array(20000).fill(0).map((_, i) => i); + trackBy = jasmine.createSpy('trackBy').and.callFake((item: unknown) => item); + renderVirtualFor = false; +}