Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 772d621

Browse files
committedDec 26, 2018
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 d22f48c commit 772d621

File tree

6 files changed

+117
-7
lines changed

6 files changed

+117
-7
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

+38-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ArrayDataSource, CollectionViewer, DataSource, ListRange} from '@angular/cdk/collections';
9+
import {
10+
ArrayDataSource,
11+
CollectionViewer,
12+
DataSource,
13+
ListRange,
14+
DifferDataSource,
15+
} from '@angular/cdk/collections';
1016
import {
1117
Directive,
1218
DoCheck,
@@ -75,18 +81,32 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
7581
/** Subject that emits when a new DataSource instance is given. */
7682
private _dataSourceChanges = new Subject<DataSource<T>>();
7783

84+
/**
85+
* Current differ data source. Needs to be kept in a separate
86+
* property so we can run change detection on it.
87+
*/
88+
private _differDataSource: DifferDataSource<T> | null = null;
89+
7890
/** The DataSource to display. */
7991
@Input()
8092
get cdkVirtualForOf(): DataSource<T> | Observable<T[]> | NgIterable<T> {
8193
return this._cdkVirtualForOf;
8294
}
8395
set cdkVirtualForOf(value: DataSource<T> | Observable<T[]> | NgIterable<T>) {
8496
this._cdkVirtualForOf = value;
85-
const ds = value instanceof DataSource ? value :
86-
// Slice the value if its an NgIterable to ensure we're working with an array.
87-
new ArrayDataSource<T>(
88-
value instanceof Observable ? value : Array.prototype.slice.call(value || []));
89-
this._dataSourceChanges.next(ds);
97+
98+
let dataSource: DataSource<T>;
99+
100+
if (value instanceof DataSource) {
101+
dataSource = value;
102+
} else if (Array.isArray(value)) {
103+
this._differDataSource = dataSource =
104+
new DifferDataSource(this._differs, value, this.cdkVirtualForTrackBy);
105+
} else {
106+
dataSource = new ArrayDataSource<T>(value as T[] | Observable<T[]>);
107+
}
108+
109+
this._dataSourceChanges.next(dataSource);
90110
}
91111
_cdkVirtualForOf: DataSource<T> | Observable<T[]> | NgIterable<T>;
92112

@@ -103,6 +123,10 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
103123
this._cdkVirtualForTrackBy = fn ?
104124
(index, item) => fn(index + (this._renderedRange ? this._renderedRange.start : 0), item) :
105125
undefined;
126+
127+
if (this._differDataSource) {
128+
this._differDataSource.switchTrackBy(this._cdkVirtualForTrackBy);
129+
}
106130
}
107131
private _cdkVirtualForTrackBy: TrackByFunction<T> | undefined;
108132

@@ -215,6 +239,10 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
215239
}
216240

217241
ngDoCheck() {
242+
if (this._differDataSource) {
243+
this._differDataSource.doCheck();
244+
}
245+
218246
if (this._differ && this._needsUpdate) {
219247
// TODO(mmalerba): We should differentiate needs update due to scrolling and a new portion of
220248
// this list being rendered (can use simpler algorithm) vs needs update due to data actually
@@ -261,6 +289,10 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
261289

262290
if (oldDs) {
263291
oldDs.disconnect(this);
292+
293+
if (oldDs === this._differDataSource) {
294+
this._differDataSource = null;
295+
}
264296
}
265297

266298
this._needsUpdate = true;

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

+22
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,28 @@ describe('CdkVirtualScrollViewport', () => {
484484
.toEqual({start: 0, end: 3}, 'newly emitted items should be rendered');
485485
}));
486486

487+
it('should work if the iterable is mutated', fakeAsync(() => {
488+
testComponent.items = [];
489+
finishInit(fixture);
490+
491+
expect(viewport.getRenderedRange())
492+
.toEqual({start: 0, end: 0}, 'no items should be rendered');
493+
494+
testComponent.items.push(1, 2, 3);
495+
fixture.detectChanges();
496+
flush();
497+
498+
expect(viewport.getRenderedRange())
499+
.toEqual({start: 0, end: 3}, 'newly emitted items should be rendered');
500+
501+
testComponent.items.pop();
502+
fixture.detectChanges();
503+
flush();
504+
505+
expect(viewport.getRenderedRange())
506+
.toEqual({start: 0, end: 2}, 'last item to be removed');
507+
}));
508+
487509
it('should trackBy value by default', fakeAsync(() => {
488510
testComponent.items = [];
489511
spyOn(testComponent.virtualForViewContainer, 'detach').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 type ListRange = {

0 commit comments

Comments
 (0)
Please sign in to comment.