Skip to content

Commit 3410779

Browse files
feat(cdk/collections): extract view repeater strategies from virtualForOf and CDK table.
1 parent 90f4d0e commit 3410779

File tree

14 files changed

+562
-146
lines changed

14 files changed

+562
-146
lines changed
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: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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+
/**
41+
* The size of the cache used to store unused views.
42+
* Setting the cache size to `0` will disable caching. Defaults to 20 views.
43+
*/
44+
viewCacheSize: number = 20;
45+
46+
/**
47+
* View cache that stores embedded view instances that have been previously stamped out,
48+
* but don't are not currently rendered. The view repeater will reuse these views rather than
49+
* creating brand new ones.
50+
*
51+
* TODO(michaeljamesparsons) Investigate whether using a linked list would improve performance.
52+
*/
53+
private _viewCache: EmbeddedViewRef<C>[] = [];
54+
55+
/** Apply changes to the DOM. */
56+
applyChanges(changes: IterableChanges<R>,
57+
viewContainerRef: ViewContainerRef,
58+
itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>,
59+
itemValueResolver: _ViewRepeaterItemValueResolver<T, R>,
60+
itemViewChanged?: _ViewRepeaterItemChanged<R, C>) {
61+
// Rearrange the views to put them in the right location.
62+
changes.forEachOperation((record: IterableChangeRecord<R>,
63+
adjustedPreviousIndex: number | null,
64+
currentIndex: number | null) => {
65+
let view: EmbeddedViewRef<C> | undefined;
66+
let operation: _ViewRepeaterOperation;
67+
if (record.previousIndex == null) { // Item added.
68+
const viewArgsFactory = () => itemContextFactory(
69+
record, adjustedPreviousIndex, currentIndex);
70+
view = this._insertView(viewArgsFactory, currentIndex!, viewContainerRef,
71+
itemValueResolver(record));
72+
operation = view ? _ViewRepeaterOperation.INSERTED : _ViewRepeaterOperation.REPLACED;
73+
} else if (currentIndex == null) { // Item removed.
74+
this._detachAndCacheView(adjustedPreviousIndex!, viewContainerRef);
75+
operation = _ViewRepeaterOperation.REMOVED;
76+
} else { // Item moved.
77+
view = this._moveView(adjustedPreviousIndex!, currentIndex!, viewContainerRef,
78+
itemValueResolver(record));
79+
operation = _ViewRepeaterOperation.MOVED;
80+
}
81+
82+
if (itemViewChanged) {
83+
itemViewChanged({
84+
context: view?.context,
85+
operation,
86+
record,
87+
});
88+
}
89+
});
90+
}
91+
92+
detach() {
93+
for (const view of this._viewCache) {
94+
view.destroy();
95+
}
96+
}
97+
98+
/**
99+
* Inserts a view for a new item, either from the cache or by creating a new
100+
* one. Returns `undefined` if the item was inserted into a cached view.
101+
*/
102+
private _insertView(viewArgsFactory: () => _ViewRepeaterItemInsertArgs<C>, currentIndex: number,
103+
viewContainerRef: ViewContainerRef,
104+
value: T): EmbeddedViewRef<C> | undefined {
105+
let cachedView = this._insertViewFromCache(currentIndex!, viewContainerRef);
106+
if (cachedView) {
107+
cachedView.context.$implicit = value;
108+
return undefined;
109+
}
110+
111+
const viewArgs = viewArgsFactory();
112+
return viewContainerRef.createEmbeddedView(
113+
viewArgs.templateRef, viewArgs.context, viewArgs.index);
114+
}
115+
116+
/** Detaches the view at the given index and inserts into the view cache. */
117+
private _detachAndCacheView(index: number, viewContainerRef: ViewContainerRef) {
118+
const detachedView = this._detachView(index, viewContainerRef);
119+
this._maybeCacheView(detachedView, viewContainerRef);
120+
}
121+
122+
/** Moves view at the previous index to the current index. */
123+
private _moveView(adjustedPreviousIndex: number, currentIndex: number,
124+
viewContainerRef: ViewContainerRef, value: T): EmbeddedViewRef<C> {
125+
const view = viewContainerRef.get(adjustedPreviousIndex!) as
126+
EmbeddedViewRef<C>;
127+
viewContainerRef.move(view, currentIndex);
128+
view.context.$implicit = value;
129+
return view;
130+
}
131+
132+
/**
133+
* Cache the given detached view. If the cache is full, the view will be
134+
* destroyed.
135+
*/
136+
private _maybeCacheView(view: EmbeddedViewRef<C>, viewContainerRef: ViewContainerRef) {
137+
if (this._viewCache.length < this.viewCacheSize) {
138+
this._viewCache.push(view);
139+
} else {
140+
const index = viewContainerRef.indexOf(view);
141+
142+
// The host component could remove views from the container outside of
143+
// the view repeater. It's unlikely this will occur, but just in case,
144+
// destroy the view on its own, otherwise destroy it through the
145+
// container to ensure that all the references are removed.
146+
if (index === -1) {
147+
view.destroy();
148+
} else {
149+
viewContainerRef.remove(index);
150+
}
151+
}
152+
}
153+
154+
/** Inserts a recycled view from the cache at the given index. */
155+
private _insertViewFromCache(index: number,
156+
viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> | null {
157+
const cachedView = this._viewCache.pop();
158+
if (cachedView) {
159+
viewContainerRef.insert(cachedView, index);
160+
}
161+
return cachedView || null;
162+
}
163+
164+
/** Detaches the embedded view at the given index. */
165+
private _detachView(index: number, viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> {
166+
return viewContainerRef.detach(index) as EmbeddedViewRef<C>;
167+
}
168+
}

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)