Skip to content

Commit 234414c

Browse files
authored
virtual-scroll: only move views that need to be moved (#10099)
* virtual-scroll: only move views that need to be moved * address comments
1 parent c9354d8 commit 234414c

File tree

2 files changed

+55
-76
lines changed

2 files changed

+55
-76
lines changed

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

Lines changed: 51 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,6 @@ export type CdkVirtualForOfContext<T> = {
4545
};
4646

4747

48-
type RecordViewTuple<T> = {
49-
record: IterableChangeRecord<T> | null,
50-
view?: EmbeddedViewRef<CdkVirtualForOfContext<T>>
51-
};
52-
53-
5448
/**
5549
* A directive similar to `ngForOf` to be used for rendering data inside a virtual scrolling
5650
* container.
@@ -101,6 +95,8 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
10195
}
10296
}
10397

98+
@Input() cdkVirtualForTemplateCacheSize: number = 20;
99+
104100
/** Emits whenever the data in the current DataSource changes. */
105101
dataStream: Observable<T[]> = this._dataSourceChanges
106102
.pipe(
@@ -256,80 +252,63 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
256252

257253
/** Apply changes to the DOM. */
258254
private _applyChanges(changes: IterableChanges<T>) {
259-
// TODO(mmalerba): Currently we remove every view and then re-insert it in the correct place.
260-
// It would be better to generate the minimal set of remove & inserts to get to the new list
261-
// instead.
255+
// Rearrange the views to put them in the right location.
256+
changes.forEachOperation(
257+
(record: IterableChangeRecord<T>, adjustedPreviousIndex: number, currentIndex: number) => {
258+
if (record.previousIndex == null) { // Item added.
259+
const view = this._getViewForNewItem();
260+
this._viewContainerRef.insert(view, currentIndex);
261+
view.context.$implicit = record.item;
262+
} else if (currentIndex == null) { // Item removed.
263+
this._cacheView(this._viewContainerRef.detach(adjustedPreviousIndex) as
264+
EmbeddedViewRef<CdkVirtualForOfContext<T>>);
265+
} else { // Item moved.
266+
const view = this._viewContainerRef.get(adjustedPreviousIndex) as
267+
EmbeddedViewRef<CdkVirtualForOfContext<T>>;
268+
this._viewContainerRef.move(view, currentIndex);
269+
view.context.$implicit = record.item;
270+
}
271+
});
272+
273+
// Update $implicit for any items that had an identity change.
274+
changes.forEachIdentityChange((record: IterableChangeRecord<T>) => {
275+
const view = this._viewContainerRef.get(record.currentIndex!) as
276+
EmbeddedViewRef<CdkVirtualForOfContext<T>>;
277+
view.context.$implicit = record.item;
278+
});
262279

263-
// Detach all of the views and add them into an array to preserve their original order.
264-
const previousViews: (EmbeddedViewRef<CdkVirtualForOfContext<T>> | null)[] = [];
280+
// Update the context variables on all items.
281+
const count = this._data.length;
265282
let i = this._viewContainerRef.length;
266283
while (i--) {
267-
previousViews.unshift(
268-
this._viewContainerRef.detach()! as EmbeddedViewRef<CdkVirtualForOfContext<T>>);
284+
const view = this._viewContainerRef.get(i) as EmbeddedViewRef<CdkVirtualForOfContext<T>>;
285+
view.context.index = this._renderedRange.start + i;
286+
view.context.count = count;
287+
this._updateComputedContextProperties(view.context);
269288
}
289+
}
270290

271-
// Mark the removed indices so we can recycle their views.
272-
changes.forEachRemovedItem(record => {
273-
this._templateCache.push(previousViews[record.previousIndex!]!);
274-
previousViews[record.previousIndex!] = null;
275-
});
276-
277-
// Queue up the newly added items to be inserted, recycling views from the cache if possible.
278-
const insertTuples: RecordViewTuple<T>[] = [];
279-
changes.forEachAddedItem(record => {
280-
insertTuples[record.currentIndex!] = {record, view: this._templateCache.pop()};
281-
});
282-
283-
// Queue up moved items to be re-inserted.
284-
changes.forEachMovedItem(record => {
285-
insertTuples[record.currentIndex!] = {record, view: previousViews[record.previousIndex!]!};
286-
previousViews[record.previousIndex!] = null;
287-
});
288-
289-
// We have nulled-out all of the views that were removed or moved from previousViews. What is
290-
// left is the unchanged items that we queue up to be re-inserted.
291-
i = previousViews.length;
292-
while (i--) {
293-
if (previousViews[i]) {
294-
insertTuples[i] = {record: null, view: previousViews[i]!};
295-
}
291+
/** Cache the given detached view. */
292+
private _cacheView(view: EmbeddedViewRef<CdkVirtualForOfContext<T>>) {
293+
if (this._templateCache.length < this.cdkVirtualForTemplateCacheSize) {
294+
this._templateCache.push(view);
295+
} else {
296+
view.destroy();
296297
}
297-
298-
// We now have a full list of everything to be inserted, so go ahead and insert them.
299-
this._insertViews(insertTuples);
300298
}
301299

302-
/** Insert the RecordViewTuples into the container element. */
303-
private _insertViews(insertTuples: RecordViewTuple<T>[]) {
304-
const count = this._data.length;
305-
let i = insertTuples.length;
306-
const lastIndex = i - 1;
307-
while (i--) {
308-
const index = lastIndex - i;
309-
let {view, record} = insertTuples[index];
310-
if (view) {
311-
this._viewContainerRef.insert(view);
312-
} else {
313-
view = this._viewContainerRef.createEmbeddedView(this._template, {
314-
$implicit: null!,
315-
cdkVirtualForOf: this._cdkVirtualForOf,
316-
index: -1,
317-
count: -1,
318-
first: false,
319-
last: false,
320-
odd: false,
321-
even: false
322-
});
323-
}
324-
325-
if (record) {
326-
view.context.$implicit = record.item as T;
327-
}
328-
view.context.index = this._renderedRange.start + index;
329-
view.context.count = count;
330-
this._updateComputedContextProperties(view.context);
331-
view.detectChanges();
332-
}
300+
/** Get a view for a new item, either from the cache or by creating a new one. */
301+
private _getViewForNewItem(): EmbeddedViewRef<CdkVirtualForOfContext<T>> {
302+
return this._templateCache.pop() || this._viewContainerRef.createEmbeddedView(this._template, {
303+
$implicit: null!,
304+
cdkVirtualForOf: this._cdkVirtualForOf,
305+
index: -1,
306+
count: -1,
307+
first: false,
308+
last: false,
309+
odd: false,
310+
even: false
311+
});
333312
}
334313

335314
/** Update the computed properties on the `CdkVirtualForOfContext`. */

src/demo-app/demo-app-module.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {HttpClientModule} from '@angular/common/http';
910
import {ApplicationRef, NgModule} from '@angular/core';
1011
import {BrowserModule} from '@angular/platform-browser';
11-
import {HttpClientModule} from '@angular/common/http';
12-
import {RouterModule} from '@angular/router';
1312
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
14-
import {ALL_ROUTES} from './demo-app/routes';
13+
import {RouterModule} from '@angular/router';
14+
import {AccessibilityDemoModule} from './a11y/a11y-module';
1515
import {EntryApp} from './demo-app/demo-app';
1616
import {DemoModule} from './demo-app/demo-module';
17-
import {AccessibilityDemoModule} from './a11y/a11y-module';
17+
import {ALL_ROUTES} from './demo-app/routes';
1818

1919

2020
@NgModule({

0 commit comments

Comments
 (0)