Skip to content

Commit 43081d9

Browse files
authored
fix(cdk/scrolling): virtual scroll not picking up trackBy function when items come in after init (#21335)
The virtual scroll only creates its `IterableDiffer` once, which means that if the `trackBy` function comes in at a later point (e.g. the input changed or the initialization order is different), it won't be picked up. These changes make it so the differ always has a `trackBy` which then delegates to the user-provided one or falls back to the default. Fixes #21281.
1 parent 98f6fd9 commit 43081d9

File tree

3 files changed

+94
-23
lines changed

3 files changed

+94
-23
lines changed

src/cdk/scrolling/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ ng_test_library(
4545
"//src/cdk/bidi",
4646
"//src/cdk/collections",
4747
"//src/cdk/testing/private",
48+
"@npm//@angular/common",
4849
"@npm//rxjs",
4950
],
5051
)

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,11 @@ export class CdkVirtualForOf<T> implements
288288
}
289289
this._renderedItems = this._data.slice(this._renderedRange.start, this._renderedRange.end);
290290
if (!this._differ) {
291-
this._differ = this._differs.find(this._renderedItems).create(this.cdkVirtualForTrackBy);
291+
// Use a wrapper function for the `trackBy` so any new values are
292+
// picked up automatically without having to recreate the differ.
293+
this._differ = this._differs.find(this._renderedItems).create((index, item) => {
294+
return this.cdkVirtualForTrackBy ? this.cdkVirtualForTrackBy(index, item) : item;
295+
});
292296
}
293297
this._needsUpdate = true;
294298
}
@@ -310,7 +314,7 @@ export class CdkVirtualForOf<T> implements
310314
const count = this._data.length;
311315
let i = this._viewContainerRef.length;
312316
while (i--) {
313-
let view = this._viewContainerRef.get(i) as EmbeddedViewRef<CdkVirtualForOfContext<T>>;
317+
const view = this._viewContainerRef.get(i) as EmbeddedViewRef<CdkVirtualForOfContext<T>>;
314318
view.context.index = this._renderedRange.start + i;
315319
view.context.count = count;
316320
this._updateComputedContextProperties(view.context);
@@ -324,7 +328,7 @@ export class CdkVirtualForOf<T> implements
324328
changes,
325329
this._viewContainerRef,
326330
(record: IterableChangeRecord<T>,
327-
adjustedPreviousIndex: number | null,
331+
_adjustedPreviousIndex: number | null,
328332
currentIndex: number | null) => this._getEmbeddedViewArgs(record, currentIndex!),
329333
(record) => record.item);
330334

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

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import {
55
ScrollDispatcher,
66
ScrollingModule
77
} from '@angular/cdk/scrolling';
8+
import {CommonModule} from '@angular/common';
89
import {dispatchFakeEvent} from '@angular/cdk/testing/private';
910
import {
1011
Component,
11-
Input,
1212
NgZone,
1313
TrackByFunction,
1414
ViewChild,
@@ -29,7 +29,7 @@ import {animationFrameScheduler, Subject} from 'rxjs';
2929

3030

3131
describe('CdkVirtualScrollViewport', () => {
32-
describe ('with FixedSizeVirtualScrollStrategy', () => {
32+
describe('with FixedSizeVirtualScrollStrategy', () => {
3333
let fixture: ComponentFixture<FixedSizeVirtualScroll>;
3434
let testComponent: FixedSizeVirtualScroll;
3535
let viewport: CdkVirtualScrollViewport;
@@ -899,6 +899,35 @@ describe('CdkVirtualScrollViewport', () => {
899899
.toEqual(['0', '1', '2', '3', '4', '5', '6', '7']);
900900
}));
901901
});
902+
903+
describe('with delayed initialization', () => {
904+
let fixture: ComponentFixture<DelayedInitializationVirtualScroll>;
905+
let testComponent: DelayedInitializationVirtualScroll;
906+
let viewport: CdkVirtualScrollViewport;
907+
908+
beforeEach(waitForAsync(() => {
909+
TestBed.configureTestingModule({
910+
imports: [ScrollingModule, CommonModule],
911+
declarations: [DelayedInitializationVirtualScroll],
912+
}).compileComponents();
913+
fixture = TestBed.createComponent(DelayedInitializationVirtualScroll);
914+
testComponent = fixture.componentInstance;
915+
viewport = testComponent.viewport;
916+
}));
917+
918+
it('should call custom trackBy when virtual for is added after init', fakeAsync(() => {
919+
finishInit(fixture);
920+
expect(testComponent.trackBy).not.toHaveBeenCalled();
921+
922+
testComponent.renderVirtualFor = true;
923+
fixture.detectChanges();
924+
triggerScroll(viewport, testComponent.itemSize * 5);
925+
fixture.detectChanges();
926+
flush();
927+
928+
expect(testComponent.trackBy).toHaveBeenCalled();
929+
}));
930+
});
902931
});
903932

904933

@@ -973,15 +1002,15 @@ class FixedSizeVirtualScroll {
9731002
// Casting virtualForOf as any so we can spy on private methods
9741003
@ViewChild(CdkVirtualForOf, {static: true}) virtualForOf: any;
9751004

976-
@Input() orientation = 'vertical';
977-
@Input() viewportSize = 200;
978-
@Input() viewportCrossSize = 100;
979-
@Input() itemSize = 50;
980-
@Input() minBufferPx = 0;
981-
@Input() maxBufferPx = 0;
982-
@Input() items = Array(10).fill(0).map((_, i) => i);
983-
@Input() trackBy: TrackByFunction<number>;
984-
@Input() templateCacheSize = 20;
1005+
orientation = 'vertical';
1006+
viewportSize = 200;
1007+
viewportCrossSize = 100;
1008+
itemSize = 50;
1009+
minBufferPx = 0;
1010+
maxBufferPx = 0;
1011+
items = Array(10).fill(0).map((_, i) => i);
1012+
trackBy: TrackByFunction<number>;
1013+
templateCacheSize = 20;
9851014

9861015
scrolledToIndex = 0;
9871016
hasMargin = false;
@@ -1033,15 +1062,15 @@ class FixedSizeVirtualScroll {
10331062
class FixedSizeVirtualScrollWithRtlDirection {
10341063
@ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport;
10351064

1036-
@Input() orientation = 'vertical';
1037-
@Input() viewportSize = 200;
1038-
@Input() viewportCrossSize = 100;
1039-
@Input() itemSize = 50;
1040-
@Input() minBufferPx = 0;
1041-
@Input() maxBufferPx = 0;
1042-
@Input() items = Array(10).fill(0).map((_, i) => i);
1043-
@Input() trackBy: TrackByFunction<number>;
1044-
@Input() templateCacheSize = 20;
1065+
orientation = 'vertical';
1066+
viewportSize = 200;
1067+
viewportCrossSize = 100;
1068+
itemSize = 50;
1069+
minBufferPx = 0;
1070+
maxBufferPx = 0;
1071+
items = Array(10).fill(0).map((_, i) => i);
1072+
trackBy: TrackByFunction<number>;
1073+
templateCacheSize = 20;
10451074

10461075
scrolledToIndex = 0;
10471076

@@ -1116,3 +1145,40 @@ class VirtualScrollWithItemInjectingViewContainer {
11161145
items = Array(20000).fill(0).map((_, i) => i);
11171146
}
11181147

1148+
1149+
@Component({
1150+
template: `
1151+
<cdk-virtual-scroll-viewport [itemSize]="itemSize">
1152+
<ng-container *ngIf="renderVirtualFor">
1153+
<div class="item" *cdkVirtualFor="let item of items; trackBy: trackBy">{{item}}</div>
1154+
</ng-container>
1155+
</cdk-virtual-scroll-viewport>
1156+
`,
1157+
styles: [`
1158+
.cdk-virtual-scroll-content-wrapper {
1159+
display: flex;
1160+
flex-direction: column;
1161+
}
1162+
1163+
.cdk-virtual-scroll-viewport {
1164+
width: 200px;
1165+
height: 200px;
1166+
background-color: #f5f5f5;
1167+
}
1168+
1169+
.item {
1170+
width: 100%;
1171+
height: 50px;
1172+
box-sizing: border-box;
1173+
border: 1px dashed #ccc;
1174+
}
1175+
`],
1176+
encapsulation: ViewEncapsulation.None,
1177+
})
1178+
class DelayedInitializationVirtualScroll {
1179+
@ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport;
1180+
itemSize = 50;
1181+
items = Array(20000).fill(0).map((_, i) => i);
1182+
trackBy = jasmine.createSpy('trackBy').and.callFake((item: unknown) => item);
1183+
renderVirtualFor = false;
1184+
}

0 commit comments

Comments
 (0)