Skip to content

Commit 29e13bf

Browse files
feat(cdk/collections): extract view repeater strategies from virtualForOf and CDK table.
1 parent 544e335 commit 29e13bf

File tree

15 files changed

+575
-149
lines changed

15 files changed

+575
-149
lines changed

goldens/ts-circular-deps.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
"src/cdk-experimental/dialog/dialog-config.ts",
44
"src/cdk-experimental/dialog/dialog-container.ts"
55
],
6+
[
7+
"src/cdk/collections/dispose-view-repeater-strategy.ts",
8+
"src/cdk/collections/index.ts",
9+
"src/cdk/collections/public-api.ts"
10+
],
11+
[
12+
"src/cdk/collections/index.ts",
13+
"src/cdk/collections/public-api.ts",
14+
"src/cdk/collections/recycle-view-repeater-strategy.ts"
15+
],
616
[
717
"src/cdk/drag-drop/directives/drag.ts",
818
"src/cdk/drag-drop/directives/drop-list.ts"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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 {
10+
EmbeddedViewRef,
11+
IterableChangeRecord,
12+
IterableChanges,
13+
ViewContainerRef
14+
} from '@angular/core';
15+
import {
16+
_viewRepeater,
17+
_ViewRepeaterItemChanged,
18+
_ViewRepeaterItemContext,
19+
_ViewRepeaterItemContextFactory,
20+
_ViewRepeaterItemValueResolver,
21+
_ViewRepeaterOperation
22+
} from './view-repeater';
23+
24+
/**
25+
* A repeater that destroys views when they are removed from a
26+
* {@link ViewContainerRef}. When new items are inserted into the container,
27+
* the repeater will always construct a new embedded view for each item.
28+
*
29+
* @template T The type for the embedded view's $implicit property.
30+
* @template R The type for the item in each IterableDiffer change record.
31+
* @template C The type for the context passed to each embedded view.
32+
*/
33+
export class _disposeViewRepeaterStrategy<T, R, C extends _ViewRepeaterItemContext<T>>
34+
implements _viewRepeater<T, R, C> {
35+
applyChanges(changes: IterableChanges<R>,
36+
viewContainerRef: ViewContainerRef,
37+
itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>,
38+
itemValueResolver: _ViewRepeaterItemValueResolver<T, R>,
39+
itemViewChanged?: _ViewRepeaterItemChanged<R, C>) {
40+
changes.forEachOperation(
41+
(record: IterableChangeRecord<R>,
42+
adjustedPreviousIndex: number | null,
43+
currentIndex: number | null) => {
44+
let view: EmbeddedViewRef<C> | undefined;
45+
let operation: _ViewRepeaterOperation;
46+
if (record.previousIndex == null) {
47+
const insertContext = itemContextFactory(record, adjustedPreviousIndex, currentIndex);
48+
view = viewContainerRef.createEmbeddedView(
49+
insertContext.templateRef, insertContext.context, insertContext.index);
50+
operation = _ViewRepeaterOperation.INSERTED;
51+
} else if (currentIndex == null) {
52+
viewContainerRef.remove(adjustedPreviousIndex!);
53+
operation = _ViewRepeaterOperation.REMOVED;
54+
} else {
55+
view = viewContainerRef.get(adjustedPreviousIndex!) as EmbeddedViewRef<C>;
56+
viewContainerRef.move(view!, currentIndex);
57+
operation = _ViewRepeaterOperation.MOVED;
58+
}
59+
60+
if (itemViewChanged) {
61+
itemViewChanged({
62+
context: view?.context,
63+
operation,
64+
record,
65+
});
66+
}
67+
});
68+
}
69+
70+
detach() {
71+
}
72+
}

src/cdk/collections/public-api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
export * from './array-data-source';
1010
export * from './collection-viewer';
1111
export * from './data-source';
12+
export * from './dispose-view-repeater-strategy';
13+
export * from './recycle-view-repeater-strategy';
1214
export * from './selection-model';
1315
export {
1416
UniqueSelectionDispatcher,
1517
UniqueSelectionDispatcherListener,
1618
} from './unique-selection-dispatcher';
1719
export * from './tree-adapter';
20+
export * from './view-repeater';
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 {
10+
EmbeddedViewRef,
11+
IterableChangeRecord,
12+
IterableChanges,
13+
IterableDiffer,
14+
ViewContainerRef
15+
} from '@angular/core';
16+
import {
17+
_viewRepeater,
18+
_ViewRepeaterItemChanged,
19+
_ViewRepeaterItemContext,
20+
_ViewRepeaterItemContextFactory,
21+
_ViewRepeaterItemInsertArgs,
22+
_ViewRepeaterItemValueResolver,
23+
_ViewRepeaterOperation
24+
} from './view-repeater';
25+
26+
27+
/**
28+
* A repeater that caches views when they are removed from a
29+
* {@link ViewContainerRef}. When new items are inserted into the container,
30+
* the repeater will reuse one of the cached views instead of creating a new
31+
* embedded view. Recycling cached views reduces the quantity of expensive DOM
32+
* inserts.
33+
*
34+
* @template T The type for the embedded view's $implicit property.
35+
* @template R The type for the item in each IterableDiffer change record.
36+
* @template C The type for the context passed to each embedded view.
37+
*/
38+
export class _recycleViewRepeaterStrategy<T, R, C extends _ViewRepeaterItemContext<T>>
39+
implements _viewRepeater<T, R, C> {
40+
differ: IterableDiffer<C>;
41+
42+
/**
43+
* The size of the cache used to store unused views.
44+
* Setting the cache size to `0` will disable caching. Defaults to 20 views.
45+
*/
46+
viewCacheSize: number = 20;
47+
48+
/**
49+
* View cache that stores embedded view instances that have been previously stamped out,
50+
* but don't are not currently rendered. The view repeater will reuse these views rather than
51+
* creating brand new ones.
52+
*
53+
* TODO(michaeljamesparsons) Investigate whether using a linked list would improve performance.
54+
*/
55+
private _viewCache: EmbeddedViewRef<C>[] = [];
56+
57+
/** Apply changes to the DOM. */
58+
applyChanges(changes: IterableChanges<R>,
59+
viewContainerRef: ViewContainerRef,
60+
itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>,
61+
itemValueResolver: _ViewRepeaterItemValueResolver<T, R>,
62+
itemViewChanged?: _ViewRepeaterItemChanged<R, C>) {
63+
// Rearrange the views to put them in the right location.
64+
changes.forEachOperation((record: IterableChangeRecord<R>,
65+
adjustedPreviousIndex: number | null,
66+
currentIndex: number | null) => {
67+
let view: EmbeddedViewRef<C> | undefined;
68+
let operation: _ViewRepeaterOperation;
69+
if (record.previousIndex == null) { // Item added.
70+
const viewArgsFactory = () => itemContextFactory(
71+
record, adjustedPreviousIndex, currentIndex);
72+
view = this._insertView(viewArgsFactory, currentIndex!, viewContainerRef,
73+
itemValueResolver(record));
74+
operation = view ? _ViewRepeaterOperation.INSERTED : _ViewRepeaterOperation.REPLACED;
75+
} else if (currentIndex == null) { // Item removed.
76+
this._detachAndCacheView(adjustedPreviousIndex!, viewContainerRef);
77+
operation = _ViewRepeaterOperation.REMOVED;
78+
} else { // Item moved.
79+
view = this._moveView(adjustedPreviousIndex!, currentIndex!, viewContainerRef,
80+
itemValueResolver(record));
81+
operation = _ViewRepeaterOperation.MOVED;
82+
}
83+
84+
if (itemViewChanged) {
85+
itemViewChanged({
86+
context: view?.context,
87+
operation,
88+
record,
89+
});
90+
}
91+
});
92+
}
93+
94+
detach() {
95+
for (const view of this._viewCache) {
96+
view.destroy();
97+
}
98+
}
99+
100+
/**
101+
* Inserts a view for a new item, either from the cache or by creating a new
102+
* one. Returns `undefined` if the item was inserted into a cached view.
103+
*/
104+
private _insertView(viewArgsFactory: () => _ViewRepeaterItemInsertArgs<C>, currentIndex: number,
105+
viewContainerRef: ViewContainerRef,
106+
value: T): EmbeddedViewRef<C> | undefined {
107+
let cachedView = this._insertViewFromCache(currentIndex!, viewContainerRef);
108+
if (cachedView) {
109+
cachedView.context.$implicit = value;
110+
return undefined;
111+
}
112+
113+
const viewArgs = viewArgsFactory();
114+
return viewContainerRef.createEmbeddedView(
115+
viewArgs.templateRef, viewArgs.context, viewArgs.index);
116+
}
117+
118+
/** Detaches the view at the given index and inserts into the view cache. */
119+
private _detachAndCacheView(index: number, viewContainerRef: ViewContainerRef) {
120+
const detachedView = this._detachView(index, viewContainerRef);
121+
this._maybeCacheView(detachedView, viewContainerRef);
122+
}
123+
124+
/** Moves view at the previous index to the current index. */
125+
private _moveView(adjustedPreviousIndex: number, currentIndex: number,
126+
viewContainerRef: ViewContainerRef, value: T): EmbeddedViewRef<C> {
127+
const view = viewContainerRef.get(adjustedPreviousIndex!) as
128+
EmbeddedViewRef<C>;
129+
viewContainerRef.move(view, currentIndex);
130+
view.context.$implicit = value;
131+
return view;
132+
}
133+
134+
/**
135+
* Cache the given detached view. If the cache is full, the view will be
136+
* destroyed.
137+
*/
138+
private _maybeCacheView(view: EmbeddedViewRef<C>, viewContainerRef: ViewContainerRef) {
139+
if (this._viewCache.length < this.viewCacheSize) {
140+
this._viewCache.push(view);
141+
} else {
142+
const index = viewContainerRef.indexOf(view);
143+
144+
// The host component could remove views from the container outside of
145+
// the view repeater. It's unlikely this will occur, but just in case,
146+
// destroy the view on its own, otherwise destroy it through the
147+
// container to ensure that all the references are removed.
148+
if (index === -1) {
149+
view.destroy();
150+
} else {
151+
viewContainerRef.remove(index);
152+
}
153+
}
154+
}
155+
156+
/** Inserts a recycled view from the cache at the given index. */
157+
private _insertViewFromCache(index: number,
158+
viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> | null {
159+
const cachedView = this._viewCache.pop();
160+
if (cachedView) {
161+
viewContainerRef.insert(cachedView, index);
162+
}
163+
return cachedView || null;
164+
}
165+
166+
/** Detaches the embedded view at the given index. */
167+
private _detachView(index: number, viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> {
168+
return viewContainerRef.detach(index) as EmbeddedViewRef<C>;
169+
}
170+
}

src/cdk/collections/view-repeater.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 {
10+
InjectionToken,
11+
IterableChangeRecord,
12+
IterableChanges,
13+
TemplateRef,
14+
ViewContainerRef
15+
} from '@angular/core';
16+
17+
/**
18+
* The context for an embedded view in the repeater's view container.
19+
*
20+
* @template T The type for the embedded view's $implicit property.
21+
*/
22+
export interface _ViewRepeaterItemContext<T> {
23+
$implicit?: T;
24+
}
25+
26+
/**
27+
* The arguments needed to construct an embedded view for an item in a view
28+
* container.
29+
*
30+
* @template C The type for the context passed to each embedded view.
31+
*/
32+
export interface _ViewRepeaterItemInsertArgs<C> {
33+
templateRef: TemplateRef<C>;
34+
context?: C;
35+
index?: number;
36+
}
37+
38+
/**
39+
* A factory that derives the embedded view context for an item in a view
40+
* container.
41+
*
42+
* @template T The type for the embedded view's $implicit property.
43+
* @template R The type for the item in each IterableDiffer change record.
44+
* @template C The type for the context passed to each embedded view.
45+
*/
46+
export type _ViewRepeaterItemContextFactory<T, R, C extends _ViewRepeaterItemContext<T>> =
47+
(record: IterableChangeRecord<R>,
48+
adjustedPreviousIndex: number | null,
49+
currentIndex: number | null) => _ViewRepeaterItemInsertArgs<C>;
50+
51+
/**
52+
* Extracts the value of an item from an {@link IterableChangeRecord}.
53+
*
54+
* @template T The type for the embedded view's $implicit property.
55+
* @template R The type for the item in each IterableDiffer change record.
56+
*/
57+
export type _ViewRepeaterItemValueResolver<T, R> =
58+
(record: IterableChangeRecord<R>) => T;
59+
60+
/** Indicates how a view was changed by a {@link _viewRepeater}. */
61+
export const enum _ViewRepeaterOperation {
62+
/** The content of an existing view was replaced with another item. */
63+
REPLACED,
64+
/** A new view was created with `createEmbeddedView`. */
65+
INSERTED,
66+
/** The position of a view changed, but the content remains the same. */
67+
MOVED,
68+
/** A view was detached from the view container. */
69+
REMOVED,
70+
}
71+
72+
/**
73+
* Meta data describing the state of a view after it was updated by a
74+
* {@link _viewRepeater}.
75+
*
76+
* @template R The type for the item in each IterableDiffer change record.
77+
* @template C The type for the context passed to each embedded view.
78+
*/
79+
export interface _ViewRepeaterItemChange<R, C> {
80+
/** The view's context after it was changed. */
81+
context?: C;
82+
/** Indicates how the view was changed. */
83+
operation: _ViewRepeaterOperation;
84+
/** The view's corresponding change record. */
85+
record: IterableChangeRecord<R>;
86+
}
87+
88+
/**
89+
* Type for a callback to be executed after a view has changed.
90+
*
91+
* @template R The type for the item in each IterableDiffer change record.
92+
* @template C The type for the context passed to each embedded view.
93+
*/
94+
export type _ViewRepeaterItemChanged<R, C> =
95+
(change: _ViewRepeaterItemChange<R, C>) => void;
96+
97+
/**
98+
* Describes a strategy for rendering items in a {@link ViewContainerRef}.
99+
*
100+
* @template T The type for the embedded view's $implicit property.
101+
* @template R The type for the item in each IterableDiffer change record.
102+
* @template C The type for the context passed to each embedded view.
103+
*/
104+
export interface _viewRepeater<T, R, C extends _ViewRepeaterItemContext<T>> {
105+
applyChanges(
106+
changes: IterableChanges<R>,
107+
viewContainerRef: ViewContainerRef,
108+
itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>,
109+
itemValueResolver: _ViewRepeaterItemValueResolver<T, R>,
110+
itemViewChanged?: _ViewRepeaterItemChanged<R, C>): void;
111+
112+
detach(): void;
113+
}
114+
115+
/**
116+
* Injection token for {@link _viewRepeater}.
117+
*
118+
* INTERNAL ONLY - not for public consumption.
119+
* @docs-private
120+
*/
121+
export const _VIEW_REPEATER_STRATEGY = new InjectionToken<
122+
_viewRepeater<unknown, unknown, _ViewRepeaterItemContext<unknown>>>('_ViewRepeater');

0 commit comments

Comments
 (0)