Skip to content

Commit 62829de

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

File tree

10 files changed

+489
-144
lines changed

10 files changed

+489
-144
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 '@angular/cdk/collections/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+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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 '@angular/cdk/collections/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 templates that are not being used for re-use later.
44+
* Setting the cache size to `0` will disable caching. Defaults to 20 templates.
45+
*/
46+
templateCacheSize: number = 20;
47+
48+
/**
49+
* The template cache used to hold on ot template instancess that have been stamped out, but don't
50+
* currently need to be rendered. These instances will be reused in the future rather than
51+
* stamping out brand new ones.
52+
*/
53+
private _templateCache: 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 (let view of this._templateCache) {
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._templateCache.length < this.templateCacheSize) {
138+
this._templateCache.push(view);
139+
} else {
140+
const index = viewContainerRef.indexOf(view);
141+
142+
// It's very unlikely that the index will ever be -1, but just in case,
143+
// destroy the view on its own, otherwise destroy it through the
144+
// container to ensure that all the references are removed.
145+
if (index === -1) {
146+
view.destroy();
147+
} else {
148+
viewContainerRef.remove(index);
149+
}
150+
}
151+
}
152+
153+
/** Inserts a recycled view from the cache at the given index. */
154+
private _insertViewFromCache(index: number,
155+
viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> | null {
156+
const cachedView = this._templateCache.pop();
157+
if (cachedView) {
158+
viewContainerRef.insert(cachedView, index);
159+
}
160+
return cachedView || null;
161+
}
162+
163+
/** Detaches the embedded view at the given index. */
164+
private _detachView(index: number, viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> {
165+
return viewContainerRef.detach(index) as EmbeddedViewRef<C>;
166+
}
167+
}

src/cdk/collections/view-repeater.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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 R The type for the item in each IterableDiffer change record.
55+
*/
56+
export type ViewRepeaterItemValueResolver<T, R> =
57+
(record: IterableChangeRecord<R>) => T;
58+
59+
/** Indicates how a view was changed by a {@link ViewRepeater}. */
60+
export const enum ViewRepeaterOperation {
61+
/** The content of an existing view was replaced with another item. */
62+
REPLACED,
63+
/** A new view was created with `createEmbeddedView`. */
64+
INSERTED,
65+
/** The position of a view changed, but the content remains the same. */
66+
MOVED,
67+
/** A view was detached from the view container. */
68+
REMOVED,
69+
}
70+
71+
/**
72+
* Meta data describing the state of a view after it was updated by a
73+
* {@link ViewRepeater}.
74+
*/
75+
export interface ViewRepeaterItemChange<R, C> {
76+
/** The view's context after it was changed. */
77+
context?: C;
78+
/** Indicates how the view was changed. */
79+
operation: ViewRepeaterOperation;
80+
/** The view's corresponding change record. */
81+
record: IterableChangeRecord<R>;
82+
}
83+
84+
/** Type for a callback to be executed after a view has changed. */
85+
export type ViewRepeaterItemChanged<R, C> =
86+
(change: ViewRepeaterItemChange<R, C>) => void;
87+
88+
/**
89+
* Describes a strategy for rendering items in a {@link ViewContainerRef}.
90+
*
91+
* @template T The type for the embedded view's $implicit property.
92+
* @template R The type for the item in each IterableDiffer change record.
93+
* @template C The type for the context passed to each embedded view.
94+
*/
95+
export interface ViewRepeater<T, R, C extends ViewRepeaterItemContext<T>> {
96+
applyChanges(
97+
changes: IterableChanges<R>,
98+
viewContainerRef: ViewContainerRef,
99+
itemContextFactory: ViewRepeaterItemContextFactory<T, R, C>,
100+
itemValueResolver: ViewRepeaterItemValueResolver<T, R>,
101+
itemViewChanged?: ViewRepeaterItemChanged<R, C>): void;
102+
103+
detach(): void;
104+
}
105+
106+
/** Injection token for {@link ViewRepeater}. */
107+
export const VIEW_REPEATER_STRATEGY =
108+
new InjectionToken<
109+
ViewRepeater<unknown, unknown, ViewRepeaterItemContext<unknown>>>('ViewRepeater');

0 commit comments

Comments
 (0)