-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(cdk/collections): extract view repeater strategies #19964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import { | ||
EmbeddedViewRef, | ||
IterableChangeRecord, | ||
IterableChanges, | ||
ViewContainerRef | ||
} from '@angular/core'; | ||
import { | ||
_ViewRepeater, | ||
_ViewRepeaterItemChanged, | ||
_ViewRepeaterItemContext, | ||
_ViewRepeaterItemContextFactory, | ||
_ViewRepeaterItemValueResolver, | ||
_ViewRepeaterOperation | ||
} from './view-repeater'; | ||
|
||
/** | ||
* A repeater that destroys views when they are removed from a | ||
* {@link ViewContainerRef}. When new items are inserted into the container, | ||
* the repeater will always construct a new embedded view for each item. | ||
* | ||
* @template T The type for the embedded view's $implicit property. | ||
* @template R The type for the item in each IterableDiffer change record. | ||
* @template C The type for the context passed to each embedded view. | ||
*/ | ||
export class _DisposeViewRepeaterStrategy<T, R, C extends _ViewRepeaterItemContext<T>> | ||
implements _ViewRepeater<T, R, C> { | ||
applyChanges(changes: IterableChanges<R>, | ||
viewContainerRef: ViewContainerRef, | ||
itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>, | ||
itemValueResolver: _ViewRepeaterItemValueResolver<T, R>, | ||
itemViewChanged?: _ViewRepeaterItemChanged<R, C>) { | ||
changes.forEachOperation( | ||
(record: IterableChangeRecord<R>, | ||
adjustedPreviousIndex: number | null, | ||
currentIndex: number | null) => { | ||
let view: EmbeddedViewRef<C> | undefined; | ||
let operation: _ViewRepeaterOperation; | ||
if (record.previousIndex == null) { | ||
const insertContext = itemContextFactory(record, adjustedPreviousIndex, currentIndex); | ||
view = viewContainerRef.createEmbeddedView( | ||
insertContext.templateRef, insertContext.context, insertContext.index); | ||
operation = _ViewRepeaterOperation.INSERTED; | ||
} else if (currentIndex == null) { | ||
viewContainerRef.remove(adjustedPreviousIndex!); | ||
operation = _ViewRepeaterOperation.REMOVED; | ||
} else { | ||
view = viewContainerRef.get(adjustedPreviousIndex!) as EmbeddedViewRef<C>; | ||
viewContainerRef.move(view!, currentIndex); | ||
operation = _ViewRepeaterOperation.MOVED; | ||
} | ||
|
||
if (itemViewChanged) { | ||
itemViewChanged({ | ||
context: view?.context, | ||
operation, | ||
record, | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
detach() { | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import { | ||
EmbeddedViewRef, | ||
IterableChangeRecord, | ||
IterableChanges, | ||
ViewContainerRef | ||
} from '@angular/core'; | ||
import { | ||
_ViewRepeater, | ||
_ViewRepeaterItemChanged, | ||
_ViewRepeaterItemContext, | ||
_ViewRepeaterItemContextFactory, | ||
_ViewRepeaterItemInsertArgs, | ||
_ViewRepeaterItemValueResolver, | ||
_ViewRepeaterOperation | ||
} from './view-repeater'; | ||
|
||
|
||
/** | ||
* A repeater that caches views when they are removed from a | ||
* {@link ViewContainerRef}. When new items are inserted into the container, | ||
* the repeater will reuse one of the cached views instead of creating a new | ||
* embedded view. Recycling cached views reduces the quantity of expensive DOM | ||
* inserts. | ||
* | ||
* @template T The type for the embedded view's $implicit property. | ||
* @template R The type for the item in each IterableDiffer change record. | ||
* @template C The type for the context passed to each embedded view. | ||
*/ | ||
export class _RecycleViewRepeaterStrategy<T, R, C extends _ViewRepeaterItemContext<T>> | ||
implements _ViewRepeater<T, R, C> { | ||
/** | ||
* The size of the cache used to store unused views. | ||
* Setting the cache size to `0` will disable caching. Defaults to 20 views. | ||
*/ | ||
viewCacheSize: number = 20; | ||
|
||
/** | ||
* View cache that stores embedded view instances that have been previously stamped out, | ||
* but don't are not currently rendered. The view repeater will reuse these views rather than | ||
* creating brand new ones. | ||
* | ||
* TODO(michaeljamesparsons) Investigate whether using a linked list would improve performance. | ||
*/ | ||
private _viewCache: EmbeddedViewRef<C>[] = []; | ||
|
||
/** Apply changes to the DOM. */ | ||
applyChanges(changes: IterableChanges<R>, | ||
viewContainerRef: ViewContainerRef, | ||
itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>, | ||
itemValueResolver: _ViewRepeaterItemValueResolver<T, R>, | ||
itemViewChanged?: _ViewRepeaterItemChanged<R, C>) { | ||
// Rearrange the views to put them in the right location. | ||
changes.forEachOperation((record: IterableChangeRecord<R>, | ||
adjustedPreviousIndex: number | null, | ||
currentIndex: number | null) => { | ||
let view: EmbeddedViewRef<C> | undefined; | ||
let operation: _ViewRepeaterOperation; | ||
if (record.previousIndex == null) { // Item added. | ||
const viewArgsFactory = () => itemContextFactory( | ||
record, adjustedPreviousIndex, currentIndex); | ||
view = this._insertView(viewArgsFactory, currentIndex!, viewContainerRef, | ||
itemValueResolver(record)); | ||
operation = view ? _ViewRepeaterOperation.INSERTED : _ViewRepeaterOperation.REPLACED; | ||
} else if (currentIndex == null) { // Item removed. | ||
this._detachAndCacheView(adjustedPreviousIndex!, viewContainerRef); | ||
operation = _ViewRepeaterOperation.REMOVED; | ||
} else { // Item moved. | ||
view = this._moveView(adjustedPreviousIndex!, currentIndex!, viewContainerRef, | ||
itemValueResolver(record)); | ||
operation = _ViewRepeaterOperation.MOVED; | ||
} | ||
|
||
if (itemViewChanged) { | ||
itemViewChanged({ | ||
context: view?.context, | ||
operation, | ||
record, | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
detach() { | ||
for (const view of this._viewCache) { | ||
view.destroy(); | ||
} | ||
} | ||
|
||
/** | ||
* Inserts a view for a new item, either from the cache or by creating a new | ||
* one. Returns `undefined` if the item was inserted into a cached view. | ||
*/ | ||
private _insertView(viewArgsFactory: () => _ViewRepeaterItemInsertArgs<C>, currentIndex: number, | ||
viewContainerRef: ViewContainerRef, | ||
value: T): EmbeddedViewRef<C> | undefined { | ||
let cachedView = this._insertViewFromCache(currentIndex!, viewContainerRef); | ||
if (cachedView) { | ||
cachedView.context.$implicit = value; | ||
return undefined; | ||
} | ||
|
||
const viewArgs = viewArgsFactory(); | ||
return viewContainerRef.createEmbeddedView( | ||
viewArgs.templateRef, viewArgs.context, viewArgs.index); | ||
} | ||
|
||
/** Detaches the view at the given index and inserts into the view cache. */ | ||
private _detachAndCacheView(index: number, viewContainerRef: ViewContainerRef) { | ||
const detachedView = this._detachView(index, viewContainerRef); | ||
this._maybeCacheView(detachedView, viewContainerRef); | ||
} | ||
|
||
/** Moves view at the previous index to the current index. */ | ||
private _moveView(adjustedPreviousIndex: number, currentIndex: number, | ||
viewContainerRef: ViewContainerRef, value: T): EmbeddedViewRef<C> { | ||
const view = viewContainerRef.get(adjustedPreviousIndex!) as | ||
EmbeddedViewRef<C>; | ||
viewContainerRef.move(view, currentIndex); | ||
view.context.$implicit = value; | ||
return view; | ||
} | ||
|
||
/** | ||
* Cache the given detached view. If the cache is full, the view will be | ||
* destroyed. | ||
*/ | ||
private _maybeCacheView(view: EmbeddedViewRef<C>, viewContainerRef: ViewContainerRef) { | ||
if (this._viewCache.length < this.viewCacheSize) { | ||
this._viewCache.push(view); | ||
} else { | ||
const index = viewContainerRef.indexOf(view); | ||
|
||
// The host component could remove views from the container outside of | ||
// the view repeater. It's unlikely this will occur, but just in case, | ||
// destroy the view on its own, otherwise destroy it through the | ||
// container to ensure that all the references are removed. | ||
if (index === -1) { | ||
view.destroy(); | ||
} else { | ||
viewContainerRef.remove(index); | ||
} | ||
} | ||
} | ||
|
||
/** Inserts a recycled view from the cache at the given index. */ | ||
private _insertViewFromCache(index: number, | ||
viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> | null { | ||
const cachedView = this._viewCache.pop(); | ||
if (cachedView) { | ||
viewContainerRef.insert(cachedView, index); | ||
} | ||
return cachedView || null; | ||
} | ||
|
||
/** Detaches the embedded view at the given index. */ | ||
private _detachView(index: number, viewContainerRef: ViewContainerRef): EmbeddedViewRef<C> { | ||
return viewContainerRef.detach(index) as EmbeddedViewRef<C>; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import { | ||
InjectionToken, | ||
IterableChangeRecord, | ||
IterableChanges, | ||
TemplateRef, | ||
ViewContainerRef | ||
} from '@angular/core'; | ||
|
||
/** | ||
* The context for an embedded view in the repeater's view container. | ||
* | ||
* @template T The type for the embedded view's $implicit property. | ||
*/ | ||
export interface _ViewRepeaterItemContext<T> { | ||
$implicit?: T; | ||
} | ||
|
||
/** | ||
* The arguments needed to construct an embedded view for an item in a view | ||
* container. | ||
* | ||
* @template C The type for the context passed to each embedded view. | ||
*/ | ||
export interface _ViewRepeaterItemInsertArgs<C> { | ||
templateRef: TemplateRef<C>; | ||
context?: C; | ||
index?: number; | ||
} | ||
|
||
/** | ||
* A factory that derives the embedded view context for an item in a view | ||
* container. | ||
* | ||
* @template T The type for the embedded view's $implicit property. | ||
* @template R The type for the item in each IterableDiffer change record. | ||
* @template C The type for the context passed to each embedded view. | ||
*/ | ||
export type _ViewRepeaterItemContextFactory<T, R, C extends _ViewRepeaterItemContext<T>> = | ||
(record: IterableChangeRecord<R>, | ||
adjustedPreviousIndex: number | null, | ||
currentIndex: number | null) => _ViewRepeaterItemInsertArgs<C>; | ||
|
||
/** | ||
* Extracts the value of an item from an {@link IterableChangeRecord}. | ||
* | ||
* @template T The type for the embedded view's $implicit property. | ||
* @template R The type for the item in each IterableDiffer change record. | ||
*/ | ||
export type _ViewRepeaterItemValueResolver<T, R> = | ||
(record: IterableChangeRecord<R>) => T; | ||
|
||
/** Indicates how a view was changed by a {@link _ViewRepeater}. */ | ||
export const enum _ViewRepeaterOperation { | ||
/** The content of an existing view was replaced with another item. */ | ||
REPLACED, | ||
/** A new view was created with `createEmbeddedView`. */ | ||
INSERTED, | ||
/** The position of a view changed, but the content remains the same. */ | ||
MOVED, | ||
/** A view was detached from the view container. */ | ||
REMOVED, | ||
} | ||
|
||
/** | ||
* Meta data describing the state of a view after it was updated by a | ||
* {@link _ViewRepeater}. | ||
* | ||
* @template R The type for the item in each IterableDiffer change record. | ||
* @template C The type for the context passed to each embedded view. | ||
*/ | ||
export interface _ViewRepeaterItemChange<R, C> { | ||
/** The view's context after it was changed. */ | ||
context?: C; | ||
/** Indicates how the view was changed. */ | ||
operation: _ViewRepeaterOperation; | ||
/** The view's corresponding change record. */ | ||
record: IterableChangeRecord<R>; | ||
} | ||
|
||
/** | ||
* Type for a callback to be executed after a view has changed. | ||
* | ||
* @template R The type for the item in each IterableDiffer change record. | ||
* @template C The type for the context passed to each embedded view. | ||
*/ | ||
export type _ViewRepeaterItemChanged<R, C> = | ||
(change: _ViewRepeaterItemChange<R, C>) => void; | ||
|
||
/** | ||
* Describes a strategy for rendering items in a {@link ViewContainerRef}. | ||
* | ||
* @template T The type for the embedded view's $implicit property. | ||
* @template R The type for the item in each IterableDiffer change record. | ||
* @template C The type for the context passed to each embedded view. | ||
*/ | ||
export interface _ViewRepeater<T, R, C extends _ViewRepeaterItemContext<T>> { | ||
applyChanges( | ||
changes: IterableChanges<R>, | ||
viewContainerRef: ViewContainerRef, | ||
itemContextFactory: _ViewRepeaterItemContextFactory<T, R, C>, | ||
itemValueResolver: _ViewRepeaterItemValueResolver<T, R>, | ||
itemViewChanged?: _ViewRepeaterItemChanged<R, C>): void; | ||
|
||
detach(): void; | ||
} | ||
|
||
/** | ||
* Injection token for {@link _ViewRepeater}. | ||
* | ||
* INTERNAL ONLY - not for public consumption. | ||
MichaelJamesParsons marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @docs-private | ||
*/ | ||
export const _VIEW_REPEATER_STRATEGY = new InjectionToken< | ||
_ViewRepeater<unknown, unknown, _ViewRepeaterItemContext<unknown>>>('_ViewRepeater'); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.