Skip to content

Commit c196eec

Browse files
authored
virtual-scroll: add tests for cdkVirtualFor logic (#11275)
* merge fixed size test components into one * add tests for cdkVirtualFor logic * allow undefined to be explicitly passed as trackBy * fix bazel build * address comments
1 parent 5ad55da commit c196eec

File tree

6 files changed

+175
-59
lines changed

6 files changed

+175
-59
lines changed

src/cdk-experimental/scrolling/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ ts_library(
3737
srcs = glob(["**/*.spec.ts"]),
3838
deps = [
3939
":scrolling",
40+
"//src/cdk/collections",
4041
"//src/cdk/testing",
4142
"@rxjs",
4243
],

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,16 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
8181
* the item and produces a value to be used as the item's identity when tracking changes.
8282
*/
8383
@Input()
84-
get cdkVirtualForTrackBy(): TrackByFunction<T> {
84+
get cdkVirtualForTrackBy(): TrackByFunction<T> | undefined {
8585
return this._cdkVirtualForTrackBy;
8686
}
87-
set cdkVirtualForTrackBy(fn: TrackByFunction<T>) {
87+
set cdkVirtualForTrackBy(fn: TrackByFunction<T> | undefined) {
8888
this._needsUpdate = true;
89-
this._cdkVirtualForTrackBy =
90-
(index, item) => fn(index + (this._renderedRange ? this._renderedRange.start : 0), item);
89+
this._cdkVirtualForTrackBy = fn ?
90+
(index, item) => fn(index + (this._renderedRange ? this._renderedRange.start : 0), item) :
91+
undefined;
9192
}
92-
private _cdkVirtualForTrackBy: TrackByFunction<T>;
93+
private _cdkVirtualForTrackBy: TrackByFunction<T> | undefined;
9394

9495
/** The template used to stamp out new elements. */
9596
@Input()

src/cdk-experimental/scrolling/virtual-scroll-viewport.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ cdk-virtual-scroll-viewport {
1414
will-change: contents, transform;
1515
}
1616

17-
.virtual-scroll-orientation-horizontal {
17+
.cdk-virtual-scroll-orientation-horizontal {
1818
bottom: 0;
1919
}
2020

21-
.virtual-scroll-orientation-vertical {
21+
.cdk-virtual-scroll-orientation-vertical {
2222
right: 0;
2323
}
2424

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

Lines changed: 160 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import {ArrayDataSource} from '@angular/cdk/collections';
12
import {dispatchFakeEvent} from '@angular/cdk/testing';
23
import {Component, Input, ViewChild, ViewContainerRef, ViewEncapsulation} from '@angular/core';
34
import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing';
4-
import {animationFrameScheduler} from 'rxjs';
5+
import {animationFrameScheduler, Subject} from 'rxjs';
56
import {ScrollingModule} from './scrolling-module';
67
import {CdkVirtualForOf} from './virtual-for-of';
78
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
@@ -272,25 +273,9 @@ describe('CdkVirtualScrollViewport', () => {
272273
expect(viewport.getOffsetToRenderedContentStart())
273274
.toBe(testComponent.itemSize, 'should be scrolled to bottom of 5 item list');
274275
}));
275-
});
276-
277-
describe('with FixedSizeVirtualScrollStrategy and horizontal orientation', () => {
278-
let fixture: ComponentFixture<FixedHorizontalVirtualScroll>;
279-
let testComponent: FixedHorizontalVirtualScroll;
280-
let viewport: CdkVirtualScrollViewport;
281276

282-
beforeEach(() => {
283-
TestBed.configureTestingModule({
284-
imports: [ScrollingModule],
285-
declarations: [FixedHorizontalVirtualScroll],
286-
}).compileComponents();
287-
288-
fixture = TestBed.createComponent(FixedHorizontalVirtualScroll);
289-
testComponent = fixture.componentInstance;
290-
viewport = testComponent.viewport;
291-
});
292-
293-
it('should update viewport as user scrolls right', fakeAsync(() => {
277+
it('should update viewport as user scrolls right in horizontal mode', fakeAsync(() => {
278+
testComponent.orientation = 'horizontal';
294279
finishInit(fixture);
295280

296281
const maxOffset =
@@ -315,7 +300,8 @@ describe('CdkVirtualScrollViewport', () => {
315300
}
316301
}));
317302

318-
it('should update viewport as user scrolls left', fakeAsync(() => {
303+
it('should update viewport as user scrolls left in horizontal mode', fakeAsync(() => {
304+
testComponent.orientation = 'horizontal';
319305
finishInit(fixture);
320306

321307
const maxOffset =
@@ -339,6 +325,134 @@ describe('CdkVirtualScrollViewport', () => {
339325
`rendered content size should match expected value at offset ${offset}`);
340326
}
341327
}));
328+
329+
it('should work with an Observable', fakeAsync(() => {
330+
const data = new Subject<number[]>();
331+
testComponent.items = data as any;
332+
finishInit(fixture);
333+
334+
expect(viewport.getRenderedRange())
335+
.toEqual({start: 0, end: 0}, 'no items should be rendered');
336+
337+
data.next([1, 2, 3]);
338+
fixture.detectChanges();
339+
340+
expect(viewport.getRenderedRange())
341+
.toEqual({start: 0, end: 3}, 'newly emitted items should be rendered');
342+
}));
343+
344+
it('should work with a DataSource', fakeAsync(() => {
345+
const data = new Subject<number[]>();
346+
testComponent.items = new ArrayDataSource(data) as any;
347+
finishInit(fixture);
348+
flush();
349+
350+
expect(viewport.getRenderedRange())
351+
.toEqual({start: 0, end: 0}, 'no items should be rendered');
352+
353+
data.next([1, 2, 3]);
354+
fixture.detectChanges();
355+
flush();
356+
357+
expect(viewport.getRenderedRange())
358+
.toEqual({start: 0, end: 3}, 'newly emitted items should be rendered');
359+
}));
360+
361+
it('should trackBy value by default', fakeAsync(() => {
362+
testComponent.items = [];
363+
spyOn(testComponent.virtualForViewContainer, 'detach').and.callThrough();
364+
finishInit(fixture);
365+
366+
testComponent.items = [0];
367+
fixture.detectChanges();
368+
369+
expect(testComponent.virtualForViewContainer.detach).not.toHaveBeenCalled();
370+
371+
testComponent.items = [1];
372+
fixture.detectChanges();
373+
374+
expect(testComponent.virtualForViewContainer.detach).toHaveBeenCalled();
375+
}));
376+
377+
it('should trackBy index when specified', fakeAsync(() => {
378+
testComponent.trackBy = i => i;
379+
testComponent.items = [];
380+
spyOn(testComponent.virtualForViewContainer, 'detach').and.callThrough();
381+
finishInit(fixture);
382+
383+
testComponent.items = [0];
384+
fixture.detectChanges();
385+
386+
expect(testComponent.virtualForViewContainer.detach).not.toHaveBeenCalled();
387+
388+
testComponent.items = [1];
389+
fixture.detectChanges();
390+
391+
expect(testComponent.virtualForViewContainer.detach).not.toHaveBeenCalled();
392+
}));
393+
394+
it('should recycle views when template cache is large enough to accommodate', fakeAsync(() => {
395+
testComponent.trackBy = i => i;
396+
const spy =
397+
spyOn(testComponent.virtualForViewContainer, 'createEmbeddedView').and.callThrough();
398+
finishInit(fixture);
399+
400+
// Should create views for the initial rendered items.
401+
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(4);
402+
403+
spy.calls.reset();
404+
triggerScroll(viewport, 10);
405+
fixture.detectChanges();
406+
407+
// As we first start to scroll we need to create one more item. This is because the first item
408+
// is still partially on screen and therefore can't be removed yet. At the same time a new
409+
// item is now partially on the screen at the bottom and so a new view is needed.
410+
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(1);
411+
412+
spy.calls.reset();
413+
const maxOffset =
414+
testComponent.itemSize * testComponent.items.length - testComponent.viewportSize;
415+
for (let offset = 10; offset <= maxOffset; offset += 10) {
416+
triggerScroll(viewport, offset);
417+
fixture.detectChanges();
418+
}
419+
420+
// As we scroll through the rest of the items, no new views should be created, our existing 5
421+
// can just be recycled as appropriate.
422+
expect(testComponent.virtualForViewContainer.createEmbeddedView).not.toHaveBeenCalled();
423+
}));
424+
425+
it('should not recycle views when template cache is full', fakeAsync(() => {
426+
testComponent.trackBy = i => i;
427+
testComponent.templateCacheSize = 0;
428+
const spy =
429+
spyOn(testComponent.virtualForViewContainer, 'createEmbeddedView').and.callThrough();
430+
finishInit(fixture);
431+
432+
// Should create views for the initial rendered items.
433+
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(4);
434+
435+
spy.calls.reset();
436+
triggerScroll(viewport, 10);
437+
fixture.detectChanges();
438+
439+
// As we first start to scroll we need to create one more item. This is because the first item
440+
// is still partially on screen and therefore can't be removed yet. At the same time a new
441+
// item is now partially on the screen at the bottom and so a new view is needed.
442+
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(1);
443+
444+
spy.calls.reset();
445+
const maxOffset =
446+
testComponent.itemSize * testComponent.items.length - testComponent.viewportSize;
447+
for (let offset = 10; offset <= maxOffset; offset += 10) {
448+
triggerScroll(viewport, offset);
449+
fixture.detectChanges();
450+
}
451+
452+
// Since our template cache size is 0, as we scroll through the rest of the items, we need to
453+
// create a new view for each one.
454+
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(5);
455+
}));
342456
});
343457
});
344458

@@ -370,48 +484,46 @@ function triggerScroll(viewport: CdkVirtualScrollViewport, offset?: number) {
370484
@Component({
371485
template: `
372486
<cdk-virtual-scroll-viewport
373-
class="viewport" [itemSize]="itemSize" [bufferSize]="bufferSize"
374-
[style.height.px]="viewportSize" [style.width.px]="viewportCrossSize">
375-
<div class="item" *cdkVirtualFor="let item of items; let i = index"
376-
[style.height.px]="itemSize">
487+
[itemSize]="itemSize" [bufferSize]="bufferSize" [orientation]="orientation"
488+
[style.height.px]="viewportHeight" [style.width.px]="viewportWidth">
489+
<div class="item"
490+
*cdkVirtualFor="let item of items; let i = index; trackBy: trackBy; \
491+
templateCacheSize: templateCacheSize"
492+
[style.height.px]="itemSize" [style.width.px]="itemSize">
377493
{{i}} - {{item}}
378494
</div>
379495
</cdk-virtual-scroll-viewport>
380496
`,
381-
styles: [`.cdk-virtual-scroll-content-wrapper { display: flex; flex-direction: column; }`],
497+
styles: [`
498+
.cdk-virtual-scroll-content-wrapper {
499+
display: flex;
500+
flex-direction: column;
501+
}
502+
503+
.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {
504+
flex-direction: row;
505+
}
506+
`],
382507
encapsulation: ViewEncapsulation.None,
383508
})
384509
class FixedVirtualScroll {
385510
@ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;
386-
@ViewChild(CdkVirtualForOf, {read: ViewContainerRef}) cdkForOfViewContainer: ViewContainerRef;
511+
@ViewChild(CdkVirtualForOf, {read: ViewContainerRef}) virtualForViewContainer: ViewContainerRef;
387512

513+
@Input() orientation = 'vertical';
388514
@Input() viewportSize = 200;
389515
@Input() viewportCrossSize = 100;
390516
@Input() itemSize = 50;
391517
@Input() bufferSize = 0;
392518
@Input() items = Array(10).fill(0).map((_, i) => i);
393-
}
519+
@Input() trackBy;
520+
@Input() templateCacheSize = 20;
394521

395-
@Component({
396-
template: `
397-
<cdk-virtual-scroll-viewport
398-
class="viewport" [itemSize]="itemSize" [bufferSize]="bufferSize" orientation="horizontal"
399-
[style.width.px]="viewportSize" [style.height.px]="viewportCrossSize">
400-
<div class="item" *cdkVirtualFor="let item of items; let i = index"
401-
[style.width.px]="itemSize">
402-
{{i}} - {{item}}
403-
</div>
404-
</cdk-virtual-scroll-viewport>
405-
`,
406-
styles: [`.cdk-virtual-scroll-content-wrapper { display: flex; flex-direction: row; }`],
407-
encapsulation: ViewEncapsulation.None,
408-
})
409-
class FixedHorizontalVirtualScroll {
410-
@ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;
522+
get viewportWidth() {
523+
return this.orientation == 'horizontal' ? this.viewportSize : this.viewportCrossSize;
524+
}
411525

412-
@Input() viewportSize = 200;
413-
@Input() viewportCrossSize = 100;
414-
@Input() itemSize = 50;
415-
@Input() bufferSize = 0;
416-
@Input() items = Array(10).fill(0).map((_, i) => i);
526+
get viewportHeight() {
527+
return this.orientation == 'horizontal' ? this.viewportCrossSize : this.viewportSize;
528+
}
417529
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ function rangesEqual(r1: ListRange, r2: ListRange): boolean {
4242
styleUrls: ['virtual-scroll-viewport.css'],
4343
host: {
4444
'class': 'cdk-virtual-scroll-viewport',
45-
'[class.virtual-scroll-orientation-horizontal]': 'orientation === "horizontal"',
46-
'[class.virtual-scroll-orientation-vertical]': 'orientation === "vertical"',
45+
'[class.cdk-virtual-scroll-orientation-horizontal]': 'orientation === "horizontal"',
46+
'[class.cdk-virtual-scroll-orientation-vertical]': 'orientation === "vertical"',
4747
},
4848
encapsulation: ViewEncapsulation.None,
4949
changeDetection: ChangeDetectionStrategy.OnPush,

src/cdk/collections/array-data-source.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {DataSource} from './data-source';
1111

1212

1313
/** DataSource wrapper for a native array. */
14-
export class ArrayDataSource<T> implements DataSource<T> {
15-
constructor(private _data: T[] | Observable<T[]>) {}
14+
export class ArrayDataSource<T> extends DataSource<T> {
15+
constructor(private _data: T[] | Observable<T[]>) {
16+
super();
17+
}
1618

1719
connect(): Observable<T[]> {
1820
return this._data instanceof Observable ? this._data : observableOf(this._data);

0 commit comments

Comments
 (0)