Skip to content

Commit e206d12

Browse files
committed
fix(scrolling): virtual list not updating when source array is mutated
When an array is passed to `cdkVirtualFor`, it falls back to creating an `ArrayDataSource` which won't emit if the array has changed. Since the intention of the `cdkVirtualFor` is to be (more or less) a drop-in alternative for `ngFor`, it is expected that it'll update if the data has changed. These changes add a new type of data source that will allow us to detect changes on the data and to update the view. Fixes #14635. Fixes #14501.
1 parent f1bd252 commit e206d12

File tree

6 files changed

+111
-6
lines changed

6 files changed

+111
-6
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Observable, of as observableOf} from 'rxjs';
1010
import {DataSource} from './data-source';
1111

1212

13-
/** DataSource wrapper for a native array. */
13+
/** DataSource wrapper for a static native array. */
1414
export class ArrayDataSource<T> extends DataSource<T> {
1515
constructor(private _data: T[] | ReadonlyArray<T> | Observable<T[] | ReadonlyArray<T>>) {
1616
super();
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {DataSource} from './data-source';
10+
import {IterableDiffer, IterableDiffers, TrackByFunction} from '@angular/core';
11+
import {Subject, Observable} from 'rxjs';
12+
13+
/** DataSource wrapper for an iterable whose value might change. Emits when changes are detected. */
14+
export class DifferDataSource<T> extends DataSource<T> {
15+
private _differ: IterableDiffer<T>;
16+
private _changes = new Subject<T[]>();
17+
18+
constructor(
19+
private _differs: IterableDiffers,
20+
private _iterable: T[],
21+
trackBy?: TrackByFunction<T>) {
22+
23+
super();
24+
this._differ = _differs.find(_iterable).create(trackBy);
25+
}
26+
27+
connect(): Observable<T[] | ReadonlyArray<T>> {
28+
return this._changes.asObservable();
29+
}
30+
31+
disconnect() {
32+
this._changes.complete();
33+
}
34+
35+
/** Checks the array for changes. */
36+
doCheck() {
37+
if (this._differ.diff(this._iterable)) {
38+
this._changes.next(this._iterable);
39+
}
40+
}
41+
42+
/** Switches the `trackBy` function of the data source. */
43+
switchTrackBy(trackBy?: TrackByFunction<T>) {
44+
this._differ = this._differs.find(this._iterable).create(trackBy);
45+
this.doCheck();
46+
}
47+
}

src/cdk/collections/public-api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
export * from './array-data-source';
10+
export * from './differ-data-source';
1011
export * from './collection-viewer';
1112
export * from './data-source';
1213
export * from './selection';

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

+32-5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
DataSource,
1313
ListRange,
1414
isDataSource,
15+
DifferDataSource,
1516
} from '@angular/cdk/collections';
1617
import {
1718
Directive,
@@ -81,18 +82,32 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
8182
/** Subject that emits when a new DataSource instance is given. */
8283
private _dataSourceChanges = new Subject<DataSource<T>>();
8384

85+
/**
86+
* Current differ data source. Needs to be kept in a separate
87+
* property so we can run change detection on it.
88+
*/
89+
private _differDataSource: DifferDataSource<T> | null = null;
90+
8491
/** The DataSource to display. */
8592
@Input()
8693
get cdkVirtualForOf(): DataSource<T> | Observable<T[]> | NgIterable<T> {
8794
return this._cdkVirtualForOf;
8895
}
8996
set cdkVirtualForOf(value: DataSource<T> | Observable<T[]> | NgIterable<T>) {
9097
this._cdkVirtualForOf = value;
91-
const ds = isDataSource(value) ? value :
92-
// Slice the value if its an NgIterable to ensure we're working with an array.
93-
new ArrayDataSource<T>(
94-
value instanceof Observable ? value : Array.prototype.slice.call(value || []));
95-
this._dataSourceChanges.next(ds);
98+
99+
let dataSource: DataSource<T>;
100+
101+
if (isDataSource(value)) {
102+
dataSource = value;
103+
} else if (Array.isArray(value)) {
104+
this._differDataSource = dataSource =
105+
new DifferDataSource(this._differs, value, this.cdkVirtualForTrackBy);
106+
} else {
107+
dataSource = new ArrayDataSource<T>(value as T[] | Observable<T[]>);
108+
}
109+
110+
this._dataSourceChanges.next(dataSource);
96111
}
97112
_cdkVirtualForOf: DataSource<T> | Observable<T[]> | NgIterable<T>;
98113

@@ -109,6 +124,10 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
109124
this._cdkVirtualForTrackBy = fn ?
110125
(index, item) => fn(index + (this._renderedRange ? this._renderedRange.start : 0), item) :
111126
undefined;
127+
128+
if (this._differDataSource) {
129+
this._differDataSource.switchTrackBy(this._cdkVirtualForTrackBy);
130+
}
112131
}
113132
private _cdkVirtualForTrackBy: TrackByFunction<T> | undefined;
114133

@@ -221,6 +240,10 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
221240
}
222241

223242
ngDoCheck() {
243+
if (this._differDataSource) {
244+
this._differDataSource.doCheck();
245+
}
246+
224247
if (this._differ && this._needsUpdate) {
225248
// TODO(mmalerba): We should differentiate needs update due to scrolling and a new portion of
226249
// this list being rendered (can use simpler algorithm) vs needs update due to data actually
@@ -268,6 +291,10 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
268291

269292
if (oldDs) {
270293
oldDs.disconnect(this);
294+
295+
if (oldDs === this._differDataSource) {
296+
this._differDataSource = null;
297+
}
271298
}
272299

273300
this._needsUpdate = true;

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

+22
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,28 @@ describe('CdkVirtualScrollViewport', () => {
502502
expect(dataSource.disconnect).toHaveBeenCalled();
503503
}));
504504

505+
it('should work if the iterable is mutated', fakeAsync(() => {
506+
testComponent.items = [];
507+
finishInit(fixture);
508+
509+
expect(viewport.getRenderedRange())
510+
.toEqual({start: 0, end: 0}, 'no items should be rendered');
511+
512+
testComponent.items.push(1, 2, 3);
513+
fixture.detectChanges();
514+
flush();
515+
516+
expect(viewport.getRenderedRange())
517+
.toEqual({start: 0, end: 3}, 'newly emitted items should be rendered');
518+
519+
testComponent.items.pop();
520+
fixture.detectChanges();
521+
flush();
522+
523+
expect(viewport.getRenderedRange())
524+
.toEqual({start: 0, end: 2}, 'last item to be removed');
525+
}));
526+
505527
it('should trackBy value by default', fakeAsync(() => {
506528
testComponent.items = [];
507529
spyOn(testComponent.virtualForOf, '_detachView').and.callThrough();

tools/public_api_guard/cdk/collections.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ export declare abstract class DataSource<T> {
1313
abstract disconnect(collectionViewer: CollectionViewer): void;
1414
}
1515

16+
export declare class DifferDataSource<T> extends DataSource<T> {
17+
constructor(_differs: IterableDiffers, _iterable: T[], trackBy?: TrackByFunction<T>);
18+
connect(): Observable<T[] | ReadonlyArray<T>>;
19+
disconnect(): void;
20+
doCheck(): void;
21+
switchTrackBy(trackBy?: TrackByFunction<T>): void;
22+
}
23+
1624
export declare function getMultipleValuesInSingleSelectionError(): Error;
1725

1826
export declare function isDataSource(value: any): value is DataSource<any>;

0 commit comments

Comments
 (0)