mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): Fixes animations in conjunction with content projection (#63776)
Content Projected nodes are not destroyed and recreated, like every other situation. Enter and Leave animations were ephemeral and are expected to run once, and then be cleared. This means that for content projection cases, the animations would only ever work the first time they were shown / hid. In order to resolve this, we move to an animation queue that re-runs the animation functions stored in the LView. In most cases, this animation will run once on creation. For content projection, the enter and leave animations will fire more than once. Animations are stored on the LView, but indexed and scheduled by whichever RNode needs to be animated. So we only run animations for an affected RNode, rather than potentially all in the LView. This also moves the queue to afterRender, which is safer than right after template execution in refreshView. fixes: #63418 fixes: #64065 fixes: #63901 PR Close #63776
This commit is contained in:
committed by
Miles Malerba
parent
cb56fbfddb
commit
dd7c4cd9d1
@@ -18,6 +18,27 @@ export const ANIMATIONS_DISABLED = new InjectionToken<boolean>(
|
||||
},
|
||||
);
|
||||
|
||||
export interface AnimationQueue {
|
||||
queue: Set<Function>;
|
||||
isScheduled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A [DI token](api/core/InjectionToken) for the queue of all animations.
|
||||
*/
|
||||
export const ANIMATION_QUEUE = new InjectionToken<AnimationQueue>(
|
||||
typeof ngDevMode !== 'undefined' && ngDevMode ? 'AnimationQueue' : '',
|
||||
{
|
||||
providedIn: 'root',
|
||||
factory: () => {
|
||||
return {
|
||||
queue: new Set(),
|
||||
isScheduled: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The event type for when `animate.enter` and `animate.leave` are used with function
|
||||
* callbacks.
|
||||
@@ -78,10 +99,10 @@ export interface LongestAnimation {
|
||||
|
||||
export interface AnimationLViewData {
|
||||
// Enter animations that apply to nodes in this view
|
||||
enter?: Function[];
|
||||
enter?: Map<number, Function[]>;
|
||||
|
||||
// Leave animations that apply to nodes in this view
|
||||
leave?: (() => Promise<void>)[];
|
||||
leave?: Map<number, Function[]>;
|
||||
|
||||
// Leave animations that apply to nodes in this view
|
||||
// We chose to use unknown instead of PromiseSettledResult<void> to avoid requiring the type
|
||||
|
||||
@@ -162,17 +162,17 @@ export function trackLeavingNodes(tNode: TNode, el: HTMLElement): void {
|
||||
/**
|
||||
* Retrieves the list of specified enter animations from the lView
|
||||
*/
|
||||
export function getLViewEnterAnimations(lView: LView): Function[] {
|
||||
export function getLViewEnterAnimations(lView: LView): Map<number, Function[]> {
|
||||
const animationData = (lView[ANIMATIONS] ??= {});
|
||||
return (animationData.enter ??= []);
|
||||
return (animationData.enter ??= new Map<number, Function[]>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of specified leave animations from the lView
|
||||
*/
|
||||
export function getLViewLeaveAnimations(lView: LView): Function[] {
|
||||
export function getLViewLeaveAnimations(lView: LView): Map<number, Function[]> {
|
||||
const animationData = (lView[ANIMATIONS] ??= {});
|
||||
return (animationData.leave ??= []);
|
||||
return (animationData.leave ??= new Map<number, Function[]>());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,8 +244,18 @@ export function isLongestAnimation(
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given tNode is a content projection root node.
|
||||
* Stores a given animation function in the LView's animation map for later execution
|
||||
*
|
||||
* @param animations Either the enter or leave animation map from the LView
|
||||
* @param tNode The TNode the animation is associated with
|
||||
* @param fn The animation function to be called later
|
||||
*/
|
||||
export function isTNodeContentProjectionRoot(tNode: TNode): boolean {
|
||||
return Array.isArray(tNode.projection);
|
||||
export function addAnimationToLView(
|
||||
animations: Map<number, Function[]>,
|
||||
tNode: TNode,
|
||||
fn: Function,
|
||||
) {
|
||||
const animationFns = animations.get(tNode.index) ?? [];
|
||||
animationFns.push(fn);
|
||||
animations.set(tNode.index, animationFns);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
ANIMATION_QUEUE,
|
||||
AnimationCallbackEvent,
|
||||
AnimationFunction,
|
||||
MAX_ANIMATION_TIMEOUT,
|
||||
} from '../../animation/interfaces';
|
||||
import {getLView, getCurrentTNode} from '../state';
|
||||
import {RENDERER, INJECTOR, CONTEXT, LView} from '../interfaces/view';
|
||||
import {RENDERER, INJECTOR, CONTEXT, LView, ANIMATIONS} from '../interfaces/view';
|
||||
import {getNativeByTNode} from '../util/view_utils';
|
||||
import {performanceMarkFeature} from '../../util/performance';
|
||||
import {Renderer} from '../interfaces/renderer';
|
||||
@@ -20,8 +21,11 @@ import {NgZone} from '../../zone';
|
||||
import {determineLongestAnimation, allLeavingAnimations} from '../../animation/longest_animation';
|
||||
import {TNode} from '../interfaces/node';
|
||||
import {promiseWithResolvers} from '../../util/promise_with_resolvers';
|
||||
import {Injector} from '../../di';
|
||||
import {afterEveryRender} from '../after_render/hooks';
|
||||
|
||||
import {
|
||||
addAnimationToLView,
|
||||
areAnimationsDisabled,
|
||||
areAnimationSupported,
|
||||
assertAnimationTypes,
|
||||
@@ -66,12 +70,16 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
|
||||
|
||||
cancelLeavingNodes(tNode, lView);
|
||||
|
||||
getLViewEnterAnimations(lView).push(() => runEnterAnimation(lView, tNode, value));
|
||||
addAnimationToLView(getLViewEnterAnimations(lView), tNode, () =>
|
||||
runEnterAnimation(lView, tNode, value),
|
||||
);
|
||||
|
||||
queueEnterAnimations(lView);
|
||||
|
||||
return ɵɵanimateEnter; // For chaining
|
||||
}
|
||||
|
||||
export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Function): void {
|
||||
export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Function) {
|
||||
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
|
||||
|
||||
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
|
||||
@@ -94,7 +102,7 @@ export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Fu
|
||||
|
||||
const eventName = event instanceof AnimationEvent ? 'animationend' : 'transitionend';
|
||||
ngZone.runOutsideAngular(() => {
|
||||
cleanupFns.push(renderer.listen(nativeElement, eventName, handleEnterAnimationEnd));
|
||||
renderer.listen(nativeElement, eventName, handleEnterAnimationEnd);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -118,6 +126,7 @@ export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Fu
|
||||
for (const klass of activeClasses) {
|
||||
renderer.addClass(nativeElement, klass);
|
||||
}
|
||||
|
||||
// In the case that the classes added have no animations, we need to remove
|
||||
// the classes right away. This could happen because someone is intentionally
|
||||
// preventing an animation via selector specificity.
|
||||
@@ -182,7 +191,11 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
|
||||
|
||||
cancelLeavingNodes(tNode, lView);
|
||||
|
||||
getLViewEnterAnimations(lView).push(() => runEnterAnimationFunction(lView, tNode, value));
|
||||
addAnimationToLView(getLViewEnterAnimations(lView), tNode, () =>
|
||||
runEnterAnimationFunction(lView, tNode, value),
|
||||
);
|
||||
|
||||
queueEnterAnimations(lView);
|
||||
|
||||
return ɵɵanimateEnterListener;
|
||||
}
|
||||
@@ -190,7 +203,7 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
|
||||
/**
|
||||
* runs enter animations when a custom function is provided
|
||||
*/
|
||||
function runEnterAnimationFunction(lView: LView, tNode: TNode, value: AnimationFunction) {
|
||||
function runEnterAnimationFunction(lView: LView, tNode: TNode, value: AnimationFunction): void {
|
||||
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
|
||||
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
|
||||
|
||||
@@ -224,19 +237,16 @@ export function ɵɵanimateLeave(value: string | Function): typeof ɵɵanimateLe
|
||||
|
||||
const tNode = getCurrentTNode()!;
|
||||
|
||||
getLViewLeaveAnimations(lView).push(() =>
|
||||
runLeaveAnimations(lView, tNode, value, animationsDisabled),
|
||||
addAnimationToLView(getLViewLeaveAnimations(lView), tNode, () =>
|
||||
runLeaveAnimations(lView, tNode, value),
|
||||
);
|
||||
|
||||
enableAnimationQueueScheduler(lView[INJECTOR]);
|
||||
|
||||
return ɵɵanimateLeave; // For chaining
|
||||
}
|
||||
|
||||
function runLeaveAnimations(
|
||||
lView: LView,
|
||||
tNode: TNode,
|
||||
value: string | Function,
|
||||
animationsDisabled: boolean,
|
||||
): Promise<void> {
|
||||
function runLeaveAnimations(lView: LView, tNode: TNode, value: string | Function): Promise<void> {
|
||||
const {promise, resolve} = promiseWithResolvers<void>();
|
||||
const nativeElement = getNativeByTNode(tNode, lView) as Element;
|
||||
|
||||
@@ -253,7 +263,6 @@ function runLeaveAnimations(
|
||||
tNode,
|
||||
activeClasses,
|
||||
renderer,
|
||||
animationsDisabled,
|
||||
ngZone,
|
||||
resolve,
|
||||
);
|
||||
@@ -273,17 +282,11 @@ function animateLeaveClassRunner(
|
||||
tNode: TNode,
|
||||
classList: string[],
|
||||
renderer: Renderer,
|
||||
animationsDisabled: boolean,
|
||||
ngZone: NgZone,
|
||||
resolver: VoidFunction,
|
||||
) {
|
||||
if (animationsDisabled) {
|
||||
longestAnimations.delete(el);
|
||||
resolver();
|
||||
return;
|
||||
}
|
||||
|
||||
cancelAnimationsIfRunning(el, renderer);
|
||||
const cleanupFns: Function[] = [];
|
||||
|
||||
const handleOutAnimationEnd = (event: AnimationEvent | TransitionEvent | CustomEvent) => {
|
||||
// this early exit case is to prevent issues with bubbling events that are from child element animations
|
||||
@@ -307,11 +310,14 @@ function animateLeaveClassRunner(
|
||||
}
|
||||
}
|
||||
resolver();
|
||||
for (const fn of cleanupFns) {
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
ngZone.runOutsideAngular(() => {
|
||||
renderer.listen(el, 'animationend', handleOutAnimationEnd);
|
||||
renderer.listen(el, 'transitionend', handleOutAnimationEnd);
|
||||
cleanupFns.push(renderer.listen(el, 'animationend', handleOutAnimationEnd));
|
||||
cleanupFns.push(renderer.listen(el, 'transitionend', handleOutAnimationEnd));
|
||||
});
|
||||
trackLeavingNodes(tNode, el);
|
||||
for (const item of classList) {
|
||||
@@ -326,6 +332,9 @@ function animateLeaveClassRunner(
|
||||
if (!longestAnimations.has(el)) {
|
||||
clearLeavingNodes(tNode, el);
|
||||
resolver();
|
||||
for (const fn of cleanupFns) {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -359,7 +368,11 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
|
||||
const tNode = getCurrentTNode()!;
|
||||
allLeavingAnimations.add(lView);
|
||||
|
||||
getLViewLeaveAnimations(lView).push(() => runLeaveAnimationFunction(lView, tNode, value));
|
||||
addAnimationToLView(getLViewLeaveAnimations(lView), tNode, () =>
|
||||
runLeaveAnimationFunction(lView, tNode, value),
|
||||
);
|
||||
|
||||
enableAnimationQueueScheduler(lView[INJECTOR]);
|
||||
|
||||
return ɵɵanimateLeaveListener; // For chaining
|
||||
}
|
||||
@@ -416,3 +429,38 @@ function runLeaveAnimationFunction(
|
||||
// Ensure cleanup if the LView is destroyed before the animation runs.
|
||||
return promise;
|
||||
}
|
||||
|
||||
function queueEnterAnimations(lView: LView) {
|
||||
enableAnimationQueueScheduler(lView[INJECTOR]);
|
||||
const enterAnimations = lView[ANIMATIONS]?.enter;
|
||||
if (enterAnimations) {
|
||||
const animationQueue = lView[INJECTOR].get(ANIMATION_QUEUE);
|
||||
for (const [_, animateFns] of enterAnimations) {
|
||||
for (const animateFn of animateFns) {
|
||||
animationQueue.queue.add(animateFn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enableAnimationQueueScheduler(injector: Injector) {
|
||||
const animationQueue = injector.get(ANIMATION_QUEUE);
|
||||
// We only need to schedule the animation queue runner once per application.
|
||||
if (!animationQueue.isScheduled) {
|
||||
afterEveryRender(
|
||||
() => {
|
||||
runQueuedAnimations(injector);
|
||||
},
|
||||
{injector},
|
||||
);
|
||||
animationQueue.isScheduled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function runQueuedAnimations(injector: Injector) {
|
||||
const animationQueue = injector.get(ANIMATION_QUEUE);
|
||||
for (let animateFn of animationQueue.queue) {
|
||||
animateFn();
|
||||
}
|
||||
animationQueue.queue.clear();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import {ComponentTemplate, HostBindingsFunction, RenderFlags} from '../interface
|
||||
import {
|
||||
CONTEXT,
|
||||
EFFECTS_TO_SCHEDULE,
|
||||
ANIMATIONS,
|
||||
ENVIRONMENT,
|
||||
FLAGS,
|
||||
InitPhaseState,
|
||||
@@ -232,7 +231,6 @@ export function refreshView<T>(
|
||||
if (templateFn !== null) {
|
||||
executeTemplate(tView, lView, templateFn, RenderFlags.Update, context);
|
||||
}
|
||||
runEnterAnimations(lView);
|
||||
|
||||
const hooksInitPhaseCompleted =
|
||||
(flags & LViewFlags.InitPhaseStateMask) === InitPhaseState.InitPhaseCompleted;
|
||||
@@ -374,16 +372,6 @@ export function refreshView<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function runEnterAnimations(lView: LView) {
|
||||
const animationData = lView[ANIMATIONS];
|
||||
if (animationData?.enter) {
|
||||
for (const animateFn of animationData.enter) {
|
||||
animateFn();
|
||||
}
|
||||
animationData.enter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes over embedded views (ones created through ViewContainerRef APIs) and refreshes
|
||||
* them by executing an associated template function.
|
||||
|
||||
@@ -75,12 +75,15 @@ import {
|
||||
TVIEW,
|
||||
TView,
|
||||
TViewType,
|
||||
INJECTOR,
|
||||
} from './interfaces/view';
|
||||
import {assertTNodeType} from './node_assert';
|
||||
import {profiler} from './profiler';
|
||||
import {ProfilerEvent} from './profiler_types';
|
||||
import {getLViewParent, getNativeByTNode, unwrapRNode} from './util/view_utils';
|
||||
import {allLeavingAnimations} from '../animation/longest_animation';
|
||||
import {ANIMATION_QUEUE} from '../animation/interfaces';
|
||||
import {Injector} from '../di';
|
||||
|
||||
const enum WalkTNodeTreeAction {
|
||||
/** node create in the native environment. Run on initial creation. */
|
||||
@@ -99,6 +102,21 @@ const enum WalkTNodeTreeAction {
|
||||
Destroy = 3,
|
||||
}
|
||||
|
||||
function maybeQueueEnterAnimation(
|
||||
parentLView: LView | undefined,
|
||||
parent: RElement | null,
|
||||
tNode: TNode,
|
||||
injector: Injector,
|
||||
): void {
|
||||
const enterAnimations = parentLView?.[ANIMATIONS]?.enter;
|
||||
if (parent !== null && enterAnimations && enterAnimations.has(tNode.index)) {
|
||||
const animationQueue = injector.get(ANIMATION_QUEUE);
|
||||
for (const animateFn of enterAnimations.get(tNode.index)!) {
|
||||
animationQueue.queue.add(animateFn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: for performance reasons, the possible actions are inlined within the function instead of
|
||||
* being passed as an argument.
|
||||
@@ -106,8 +124,10 @@ const enum WalkTNodeTreeAction {
|
||||
function applyToElementOrContainer(
|
||||
action: WalkTNodeTreeAction,
|
||||
renderer: Renderer,
|
||||
injector: Injector,
|
||||
parent: RElement | null,
|
||||
lNodeToHandle: RNode | LContainer | LView,
|
||||
tNode: TNode,
|
||||
beforeNode?: RNode | null,
|
||||
parentLView?: LView,
|
||||
) {
|
||||
@@ -129,33 +149,50 @@ function applyToElementOrContainer(
|
||||
lNodeToHandle = lNodeToHandle[HOST]!;
|
||||
}
|
||||
const rNode: RNode = unwrapRNode(lNodeToHandle);
|
||||
|
||||
if (action === WalkTNodeTreeAction.Create && parent !== null) {
|
||||
maybeQueueEnterAnimation(parentLView, parent, tNode, injector);
|
||||
if (beforeNode == null) {
|
||||
nativeAppendChild(renderer, parent, rNode);
|
||||
} else {
|
||||
nativeInsertBefore(renderer, parent, rNode, beforeNode || null, true);
|
||||
}
|
||||
} else if (action === WalkTNodeTreeAction.Insert && parent !== null) {
|
||||
maybeQueueEnterAnimation(parentLView, parent, tNode, injector);
|
||||
nativeInsertBefore(renderer, parent, rNode, beforeNode || null, true);
|
||||
} else if (action === WalkTNodeTreeAction.Detach) {
|
||||
runLeaveAnimationsWithCallback(parentLView, (nodeHasLeaveAnimations: boolean) => {
|
||||
// the nodeHasLeaveAnimations indicates to the renderer that the element needs to
|
||||
// be removed synchronously and sets the requireSynchronousElementRemoval flag in
|
||||
// the renderer.
|
||||
nativeRemoveNode(renderer, rNode, isComponent, nodeHasLeaveAnimations);
|
||||
});
|
||||
runLeaveAnimationsWithCallback(
|
||||
parentLView,
|
||||
tNode,
|
||||
injector,
|
||||
(nodeHasLeaveAnimations: boolean) => {
|
||||
// the nodeHasLeaveAnimations indicates to the renderer that the element needs to
|
||||
// be removed synchronously and sets the requireSynchronousElementRemoval flag in
|
||||
// the renderer.
|
||||
nativeRemoveNode(renderer, rNode, isComponent, nodeHasLeaveAnimations);
|
||||
},
|
||||
);
|
||||
} else if (action === WalkTNodeTreeAction.Destroy) {
|
||||
runLeaveAnimationsWithCallback(parentLView, () => {
|
||||
runLeaveAnimationsWithCallback(parentLView, tNode, injector, () => {
|
||||
renderer.destroyNode!(rNode);
|
||||
});
|
||||
}
|
||||
if (lContainer != null) {
|
||||
applyContainer(renderer, action, lContainer, parent, beforeNode);
|
||||
applyContainer(renderer, action, injector, lContainer, tNode, parent, beforeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToAnimationQueue(injector: Injector, animationFns: Function | Function[]) {
|
||||
const animationQueue = injector.get(ANIMATION_QUEUE);
|
||||
if (Array.isArray(animationFns)) {
|
||||
for (const animateFn of animationFns) {
|
||||
animationQueue.queue.add(animateFn);
|
||||
}
|
||||
} else {
|
||||
animationQueue.queue.add(animationFns);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all DOM elements associated with a view.
|
||||
*
|
||||
@@ -353,34 +390,51 @@ function cleanUpView(tView: TView, lView: LView): void {
|
||||
}
|
||||
}
|
||||
|
||||
function hasLeaveAnimations(lView: LView | undefined): boolean {
|
||||
return lView !== undefined && lView[ANIMATIONS] !== null && lView[ANIMATIONS].leave !== undefined;
|
||||
}
|
||||
function runLeaveAnimationsWithCallback(
|
||||
lView: LView | undefined,
|
||||
tNode: TNode,
|
||||
injector: Injector,
|
||||
callback: Function,
|
||||
) {
|
||||
const animations = lView?.[ANIMATIONS];
|
||||
if (animations == null || (animations.leave && !animations.leave.has(tNode.index)))
|
||||
return callback(false);
|
||||
|
||||
function runLeaveAnimationsWithCallback(lView: LView | undefined, callback: Function) {
|
||||
if (lView && lView[ANIMATIONS] && lView[ANIMATIONS].leave) {
|
||||
if (lView[ANIMATIONS].skipLeaveAnimations) {
|
||||
lView[ANIMATIONS].skipLeaveAnimations = false;
|
||||
} else {
|
||||
const leaveAnimations = lView[ANIMATIONS].leave;
|
||||
const runningAnimations = [];
|
||||
for (let index = 0; index < leaveAnimations.length; index++) {
|
||||
const animateFn = leaveAnimations[index];
|
||||
runningAnimations.push(animateFn());
|
||||
}
|
||||
lView[ANIMATIONS].running = Promise.allSettled(runningAnimations);
|
||||
lView[ANIMATIONS].leave = undefined;
|
||||
}
|
||||
// this is solely for move operations to prevent leave animations from running
|
||||
// on the moved nodes, which would have deleted the node.
|
||||
if (animations.skipLeaveAnimations) {
|
||||
animations.skipLeaveAnimations = false;
|
||||
return callback(false);
|
||||
}
|
||||
runAfterLeaveAnimations(lView, callback);
|
||||
|
||||
addToAnimationQueue(injector, () => {
|
||||
// it's possible that in the time between when the leave animation was
|
||||
// and the time it was executed, the data structure changed. So we need
|
||||
// to be safe here.
|
||||
if (animations.leave && animations.leave.has(tNode.index)) {
|
||||
const leaveAnimationMap = animations.leave;
|
||||
const leaveAnimations = leaveAnimationMap.get(tNode.index);
|
||||
const runningAnimations = [];
|
||||
if (leaveAnimations) {
|
||||
for (let index = 0; index < leaveAnimations.length; index++) {
|
||||
const animationFn = leaveAnimations[index];
|
||||
runningAnimations.push(animationFn());
|
||||
}
|
||||
}
|
||||
animations.running = Promise.allSettled(runningAnimations);
|
||||
runAfterLeaveAnimations(lView!, callback);
|
||||
} else {
|
||||
if (lView) allLeavingAnimations.delete(lView);
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function runAfterLeaveAnimations(lView: LView | undefined, callback: Function) {
|
||||
if (lView && lView[ANIMATIONS] && lView[ANIMATIONS].running) {
|
||||
lView[ANIMATIONS].running.then(() => {
|
||||
if (lView[ANIMATIONS] && lView[ANIMATIONS].running) {
|
||||
lView[ANIMATIONS].running = undefined;
|
||||
}
|
||||
function runAfterLeaveAnimations(lView: LView, callback: Function) {
|
||||
const runningAnimations = lView[ANIMATIONS]?.running;
|
||||
if (runningAnimations) {
|
||||
runningAnimations.then(() => {
|
||||
lView[ANIMATIONS]!.running = undefined;
|
||||
allLeavingAnimations.delete(lView);
|
||||
callback(true);
|
||||
});
|
||||
@@ -776,6 +830,7 @@ function applyNodes(
|
||||
) {
|
||||
while (tNode != null) {
|
||||
ngDevMode && assertTNodeForLView(tNode, lView);
|
||||
const injector = lView[INJECTOR];
|
||||
|
||||
// Let declarations don't have corresponding DOM nodes so we skip over them.
|
||||
if (tNode.type === TNodeType.LetDeclaration) {
|
||||
@@ -802,8 +857,10 @@ function applyNodes(
|
||||
applyToElementOrContainer(
|
||||
action,
|
||||
renderer,
|
||||
injector,
|
||||
parentRElement,
|
||||
rawSlotValue,
|
||||
tNode,
|
||||
beforeNode,
|
||||
lView,
|
||||
);
|
||||
@@ -811,13 +868,24 @@ function applyNodes(
|
||||
const nextRNode = icuContainerIterate(tNode as TIcuContainerNode, lView);
|
||||
let rNode: RNode | null;
|
||||
while ((rNode = nextRNode())) {
|
||||
applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode, lView);
|
||||
applyToElementOrContainer(
|
||||
action,
|
||||
renderer,
|
||||
injector,
|
||||
parentRElement,
|
||||
rNode,
|
||||
tNode,
|
||||
beforeNode,
|
||||
lView,
|
||||
);
|
||||
}
|
||||
applyToElementOrContainer(
|
||||
action,
|
||||
renderer,
|
||||
injector,
|
||||
parentRElement,
|
||||
rawSlotValue,
|
||||
tNode,
|
||||
beforeNode,
|
||||
lView,
|
||||
);
|
||||
@@ -835,8 +903,10 @@ function applyNodes(
|
||||
applyToElementOrContainer(
|
||||
action,
|
||||
renderer,
|
||||
injector,
|
||||
parentRElement,
|
||||
rawSlotValue,
|
||||
tNode,
|
||||
beforeNode,
|
||||
lView,
|
||||
);
|
||||
@@ -956,7 +1026,16 @@ function applyProjectionRecursive(
|
||||
// This should be refactored and cleaned up.
|
||||
for (let i = 0; i < nodeToProjectOrRNodes.length; i++) {
|
||||
const rNode = nodeToProjectOrRNodes[i];
|
||||
applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode, lView);
|
||||
applyToElementOrContainer(
|
||||
action,
|
||||
renderer,
|
||||
lView[INJECTOR],
|
||||
parentRElement,
|
||||
rNode,
|
||||
tProjectionNode,
|
||||
beforeNode,
|
||||
lView,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let nodeToProject: TNode | null = nodeToProjectOrRNodes;
|
||||
@@ -994,7 +1073,9 @@ function applyProjectionRecursive(
|
||||
function applyContainer(
|
||||
renderer: Renderer,
|
||||
action: WalkTNodeTreeAction,
|
||||
injector: Injector,
|
||||
lContainer: LContainer,
|
||||
tNode: TNode,
|
||||
parentRElement: RElement | null,
|
||||
beforeNode: RNode | null | undefined,
|
||||
) {
|
||||
@@ -1012,7 +1093,15 @@ function applyContainer(
|
||||
// don't see a reason why they should be different, but they are.
|
||||
//
|
||||
// If they are we need to process the second anchor as well.
|
||||
applyToElementOrContainer(action, renderer, parentRElement, anchor, beforeNode);
|
||||
applyToElementOrContainer(
|
||||
action,
|
||||
renderer,
|
||||
injector,
|
||||
parentRElement,
|
||||
anchor,
|
||||
tNode,
|
||||
beforeNode,
|
||||
);
|
||||
}
|
||||
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
|
||||
const lView = lContainer[i] as LView;
|
||||
|
||||
@@ -400,9 +400,7 @@ describe('Animation', () => {
|
||||
expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"');
|
||||
cmp.show.set(false);
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
expect(cmp.show()).toBeFalsy();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.outerHTML).toContain('class="fade"');
|
||||
fixture.detectChanges();
|
||||
fadeCmp.nativeElement.dispatchEvent(
|
||||
@@ -443,9 +441,7 @@ describe('Animation', () => {
|
||||
expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"');
|
||||
cmp.show.set(false);
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
expect(cmp.show()).toBeFalsy();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.outerHTML).toContain('class="fade"');
|
||||
fixture.detectChanges();
|
||||
fadeCmp.nativeElement.dispatchEvent(
|
||||
@@ -809,6 +805,25 @@ describe('Animation', () => {
|
||||
}
|
||||
`;
|
||||
|
||||
it('should apply classes on entry when animation is specified with no control flow', fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
styles: styles,
|
||||
template: '<div><p animate.enter="slide-in" #el>I should slide in</p></div>',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
class TestComponent {
|
||||
@ViewChild('el', {read: ElementRef}) el!: ElementRef<HTMLParagraphElement>;
|
||||
}
|
||||
TestBed.configureTestingModule({animationsEnabled: true});
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
const cmp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
expect(cmp.el.nativeElement.outerHTML).toContain('class="slide-in"');
|
||||
}));
|
||||
|
||||
it('should apply classes on entry when animation is specified', fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
|
||||
@@ -12,9 +12,14 @@ import {
|
||||
Directive,
|
||||
effect,
|
||||
EventEmitter,
|
||||
inject,
|
||||
input,
|
||||
Output,
|
||||
signal,
|
||||
TemplateRef,
|
||||
viewChild,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import {SIGNAL} from '../../../primitives/signals';
|
||||
import {fakeAsync, TestBed, tick} from '../../../testing';
|
||||
@@ -361,5 +366,158 @@ describe('signal inputs', () => {
|
||||
tick();
|
||||
expect(childCmp.nativeElement.className).not.toContain('fade-in');
|
||||
}));
|
||||
|
||||
it('should support content projection', fakeAsync(() => {
|
||||
const animateStyles = `
|
||||
.fade-in {
|
||||
animation: fade 1ms forwards;
|
||||
}
|
||||
.fade-out {
|
||||
animation: fade 2ms reverse;
|
||||
}
|
||||
@keyframes fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@Component({
|
||||
selector: 'app-container',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
template: `
|
||||
<button (click)="toggle()">{{ state() ? 'Leave' : 'Enter' }}</button>
|
||||
|
||||
<ng-template #template>
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
||||
`,
|
||||
})
|
||||
class Container {
|
||||
private readonly viewContainerRef = inject(ViewContainerRef);
|
||||
|
||||
protected readonly template = viewChild.required<TemplateRef<any>>('template');
|
||||
|
||||
protected state = signal<boolean>(false);
|
||||
|
||||
protected toggle() {
|
||||
this.state() ? this.destroy() : this.create();
|
||||
}
|
||||
|
||||
protected create() {
|
||||
this.state.set(true);
|
||||
this.viewContainerRef.createEmbeddedView(this.template());
|
||||
}
|
||||
|
||||
protected destroy() {
|
||||
this.state.set(false);
|
||||
this.viewContainerRef.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-content',
|
||||
template: `
|
||||
<ng-content></ng-content>
|
||||
`,
|
||||
styles: animateStyles,
|
||||
host: {
|
||||
'animate.leave': 'fade-out',
|
||||
'animate.enter': 'fade-in',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
class Content {}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
template: `
|
||||
<app-container>
|
||||
<app-content>This content will be animated on enter/leave</app-content>
|
||||
</app-container>
|
||||
`,
|
||||
imports: [Container, Content],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
class TestComponent {}
|
||||
TestBed.configureTestingModule({animationsEnabled: true});
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
const button = fixture.nativeElement.querySelector('button');
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('app-content')).toBeNull();
|
||||
expect(button).not.toBeNull();
|
||||
|
||||
button.click();
|
||||
|
||||
// show first time
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
const enterAppContent = fixture.nativeElement.querySelector('app-content');
|
||||
|
||||
expect(enterAppContent).not.toBeNull();
|
||||
expect(enterAppContent.className).toEqual('fade-in');
|
||||
enterAppContent.dispatchEvent(new AnimationEvent('animationstart'));
|
||||
enterAppContent.dispatchEvent(new AnimationEvent('animationend', {animationName: 'fade'}));
|
||||
tick();
|
||||
expect(enterAppContent.className).not.toEqual('fade-in');
|
||||
expect(fixture.debugElement.query(By.css('app-content'))).not.toBeNull();
|
||||
|
||||
button.click();
|
||||
|
||||
// hide first time
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
const leaveAppContent = fixture.nativeElement.querySelector('app-content');
|
||||
|
||||
expect(fixture.nativeElement.outerHTML).toContain('app-content class="fade-out"');
|
||||
expect(leaveAppContent.className).toEqual('fade-out');
|
||||
leaveAppContent.dispatchEvent(new AnimationEvent('animationstart'));
|
||||
leaveAppContent.dispatchEvent(new AnimationEvent('animationend', {animationName: 'fade'}));
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
|
||||
expect(fixture.debugElement.query(By.css('app-content'))).toBeNull();
|
||||
|
||||
button.click();
|
||||
|
||||
// show second time
|
||||
fixture.detectChanges();
|
||||
fixture.changeDetectorRef.markForCheck();
|
||||
tickAnimationFrames(1);
|
||||
const enterAppContent2 = fixture.nativeElement.querySelector('app-content');
|
||||
|
||||
expect(enterAppContent2).not.toBeNull();
|
||||
expect(enterAppContent2.className).toEqual('fade-in');
|
||||
const fadeInEl = fixture.nativeElement.querySelector('.fade-in');
|
||||
fadeInEl.dispatchEvent(new AnimationEvent('animationstart'));
|
||||
fadeInEl.dispatchEvent(new AnimationEvent('animationend', {animationName: 'fade'}));
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
|
||||
expect(fixture.nativeElement.querySelector('app-content').className).not.toEqual('fade-in');
|
||||
expect(fixture.debugElement.query(By.css('app-content'))).not.toBeNull();
|
||||
|
||||
button.click();
|
||||
|
||||
// hide second time
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
const leaveAppContent2 = fixture.nativeElement.querySelector('app-content');
|
||||
|
||||
expect(fixture.nativeElement.outerHTML).toContain('app-content class="fade-out"');
|
||||
expect(leaveAppContent2.className).toEqual('fade-out');
|
||||
leaveAppContent2.dispatchEvent(new AnimationEvent('animationstart'));
|
||||
leaveAppContent2.dispatchEvent(new AnimationEvent('animationend', {animationName: 'fade'}));
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
|
||||
expect(fixture.debugElement.query(By.css('app-content'))).toBeNull();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,6 +48,7 @@ import {ComponentFixture, TestBed, TestComponentRenderer} from '../../testing';
|
||||
import {clearTranslations, loadTranslations} from '@angular/localize';
|
||||
import {By, DomSanitizer} from '@angular/platform-browser';
|
||||
import {expect} from '@angular/private/testing/matchers';
|
||||
import {ANIMATION_QUEUE} from '../../src/animation/interfaces';
|
||||
|
||||
describe('ViewContainerRef', () => {
|
||||
/**
|
||||
@@ -1413,6 +1414,7 @@ describe('ViewContainerRef', () => {
|
||||
{provide: Sanitizer, useValue: TestBed.inject(DomSanitizer)},
|
||||
{provide: ErrorHandler, useValue: TestBed.inject(ErrorHandler)},
|
||||
{provide: RendererFactory2, useValue: TestBed.inject(RendererFactory2)},
|
||||
{provide: ANIMATION_QUEUE, useValue: TestBed.inject(ANIMATION_QUEUE)},
|
||||
],
|
||||
})
|
||||
class MyAppModule {}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"ANIMATIONS",
|
||||
"ANIMATION_MODULE_TYPE",
|
||||
"ANIMATION_PREFIX",
|
||||
"ANIMATION_QUEUE",
|
||||
"ANY_STATE",
|
||||
"APP_BOOTSTRAP_LISTENER",
|
||||
"APP_ID",
|
||||
@@ -321,6 +322,7 @@
|
||||
"addAfterRenderSequencesForView",
|
||||
"addClass",
|
||||
"addServerStyles",
|
||||
"addToAnimationQueue",
|
||||
"addToEndOfViewTree",
|
||||
"allLeavingAnimations",
|
||||
"allocExpando",
|
||||
@@ -680,6 +682,7 @@
|
||||
"markViewForRefresh",
|
||||
"markedFeatures",
|
||||
"matchTemplateAttribute",
|
||||
"maybeQueueEnterAnimation",
|
||||
"maybeReturnReactiveLViewConsumer",
|
||||
"maybeWrapInNotSelector",
|
||||
"mergeHostAttribute",
|
||||
@@ -779,7 +782,6 @@
|
||||
"roundOffset",
|
||||
"runAfterLeaveAnimations",
|
||||
"runEffectsInView",
|
||||
"runEnterAnimations",
|
||||
"runInInjectionContext",
|
||||
"runLeaveAnimationsWithCallback",
|
||||
"runPlatformInitializers",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"lazy": [
|
||||
"AFTER_RENDER_SEQUENCES_TO_ADD",
|
||||
"ANIMATIONS",
|
||||
"ANIMATION_QUEUE",
|
||||
"APP_BOOTSTRAP_LISTENER",
|
||||
"APP_ID",
|
||||
"APP_INITIALIZER",
|
||||
@@ -291,6 +292,7 @@
|
||||
"addAfterRenderSequencesForView",
|
||||
"addDepsToRegistry",
|
||||
"addLViewToLContainer",
|
||||
"addToAnimationQueue",
|
||||
"addToArray",
|
||||
"addToEndOfViewTree",
|
||||
"addViewToDOM",
|
||||
@@ -605,6 +607,7 @@
|
||||
"markViewForRefresh",
|
||||
"markedFeatures",
|
||||
"matchTemplateAttribute",
|
||||
"maybeQueueEnterAnimation",
|
||||
"maybeReturnReactiveLViewConsumer",
|
||||
"maybeWrapInNotSelector",
|
||||
"mergeHostAttribute",
|
||||
@@ -674,7 +677,6 @@
|
||||
"retrieveHydrationInfo",
|
||||
"runAfterLeaveAnimations",
|
||||
"runEffectsInView",
|
||||
"runEnterAnimations",
|
||||
"runInInjectionContext",
|
||||
"runLeaveAnimationsWithCallback",
|
||||
"runPlatformInitializers",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"main": [
|
||||
"AFTER_RENDER_SEQUENCES_TO_ADD",
|
||||
"ANIMATIONS",
|
||||
"ANIMATION_QUEUE",
|
||||
"APP_BOOTSTRAP_LISTENER",
|
||||
"APP_ID",
|
||||
"APP_ID_ATTRIBUTE_NAME",
|
||||
@@ -346,6 +347,7 @@
|
||||
"addAfterRenderSequencesForView",
|
||||
"addLViewToLContainer",
|
||||
"addServerStyles",
|
||||
"addToAnimationQueue",
|
||||
"addToArray",
|
||||
"addToEndOfViewTree",
|
||||
"addValidators",
|
||||
@@ -787,6 +789,7 @@
|
||||
"matchTemplateAttribute",
|
||||
"maxLengthValidator",
|
||||
"maxValidator",
|
||||
"maybeQueueEnterAnimation",
|
||||
"maybeReturnReactiveLViewConsumer",
|
||||
"maybeUnwrapEmpty",
|
||||
"maybeUnwrapFn",
|
||||
@@ -899,7 +902,6 @@
|
||||
"retrieveHydrationInfo",
|
||||
"runAfterLeaveAnimations",
|
||||
"runEffectsInView",
|
||||
"runEnterAnimations",
|
||||
"runInInjectionContext",
|
||||
"runLeaveAnimationsWithCallback",
|
||||
"runPlatformInitializers",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"main": [
|
||||
"AFTER_RENDER_SEQUENCES_TO_ADD",
|
||||
"ANIMATIONS",
|
||||
"ANIMATION_QUEUE",
|
||||
"APP_BOOTSTRAP_LISTENER",
|
||||
"APP_ID",
|
||||
"APP_ID_ATTRIBUTE_NAME",
|
||||
@@ -350,6 +351,7 @@
|
||||
"addAfterRenderSequencesForView",
|
||||
"addLViewToLContainer",
|
||||
"addServerStyles",
|
||||
"addToAnimationQueue",
|
||||
"addToArray",
|
||||
"addToEndOfViewTree",
|
||||
"addValidators",
|
||||
@@ -786,6 +788,7 @@
|
||||
"markViewForRefresh",
|
||||
"markedFeatures",
|
||||
"matchTemplateAttribute",
|
||||
"maybeQueueEnterAnimation",
|
||||
"maybeReturnReactiveLViewConsumer",
|
||||
"maybeUnwrapEmpty",
|
||||
"maybeUnwrapFn",
|
||||
@@ -897,7 +900,6 @@
|
||||
"retrieveHydrationInfo",
|
||||
"runAfterLeaveAnimations",
|
||||
"runEffectsInView",
|
||||
"runEnterAnimations",
|
||||
"runInInjectionContext",
|
||||
"runLeaveAnimationsWithCallback",
|
||||
"runPlatformInitializers",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"AFTER_RENDER_SEQUENCES_TO_ADD",
|
||||
"ALLOWED_METHODS",
|
||||
"ANIMATIONS",
|
||||
"ANIMATION_QUEUE",
|
||||
"APP_BOOTSTRAP_LISTENER",
|
||||
"APP_ID",
|
||||
"APP_ID_ATTRIBUTE_NAME",
|
||||
@@ -287,6 +288,7 @@
|
||||
"activeConsumer",
|
||||
"addAfterRenderSequencesForView",
|
||||
"addServerStyles",
|
||||
"addToAnimationQueue",
|
||||
"addToEndOfViewTree",
|
||||
"allLeavingAnimations",
|
||||
"allocExpando",
|
||||
@@ -636,6 +638,7 @@
|
||||
"markViewDirty",
|
||||
"markViewForRefresh",
|
||||
"markedFeatures",
|
||||
"maybeQueueEnterAnimation",
|
||||
"maybeReturnReactiveLViewConsumer",
|
||||
"maybeWrapInNotSelector",
|
||||
"mergeHostAttribute",
|
||||
@@ -715,7 +718,6 @@
|
||||
"retrieveTransferredState",
|
||||
"runAfterLeaveAnimations",
|
||||
"runEffectsInView",
|
||||
"runEnterAnimations",
|
||||
"runInInjectionContext",
|
||||
"runLeaveAnimationsWithCallback",
|
||||
"runPlatformInitializers",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"main": [
|
||||
"AFTER_RENDER_SEQUENCES_TO_ADD",
|
||||
"ANIMATIONS",
|
||||
"ANIMATION_QUEUE",
|
||||
"APP_BASE_HREF",
|
||||
"APP_BOOTSTRAP_LISTENER",
|
||||
"APP_ID",
|
||||
@@ -396,6 +397,7 @@
|
||||
"addEmptyPathsToChildrenIfNeeded",
|
||||
"addLViewToLContainer",
|
||||
"addServerStyles",
|
||||
"addToAnimationQueue",
|
||||
"addToArray",
|
||||
"addToEndOfViewTree",
|
||||
"addViewToDOM",
|
||||
@@ -896,6 +898,7 @@
|
||||
"matchWithChecks",
|
||||
"materializeViewResults",
|
||||
"matrixParamsMatch",
|
||||
"maybeQueueEnterAnimation",
|
||||
"maybeResolveResources",
|
||||
"maybeReturnReactiveLViewConsumer",
|
||||
"maybeSchedule",
|
||||
@@ -1014,7 +1017,6 @@
|
||||
"runCanLoadGuards",
|
||||
"runCanMatchGuards",
|
||||
"runEffectsInView",
|
||||
"runEnterAnimations",
|
||||
"runInInjectionContext",
|
||||
"runLeaveAnimationsWithCallback",
|
||||
"runPlatformInitializers",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"main": [
|
||||
"AFTER_RENDER_SEQUENCES_TO_ADD",
|
||||
"ANIMATIONS",
|
||||
"ANIMATION_QUEUE",
|
||||
"APP_BOOTSTRAP_LISTENER",
|
||||
"APP_ID",
|
||||
"APP_ID_ATTRIBUTE_NAME",
|
||||
@@ -231,6 +232,7 @@
|
||||
"activeConsumer",
|
||||
"addAfterRenderSequencesForView",
|
||||
"addServerStyles",
|
||||
"addToAnimationQueue",
|
||||
"addToEndOfViewTree",
|
||||
"allLeavingAnimations",
|
||||
"allocExpando",
|
||||
@@ -502,6 +504,7 @@
|
||||
"markViewDirty",
|
||||
"markViewForRefresh",
|
||||
"markedFeatures",
|
||||
"maybeQueueEnterAnimation",
|
||||
"maybeReturnReactiveLViewConsumer",
|
||||
"maybeWrapInNotSelector",
|
||||
"mergeHostAttribute",
|
||||
@@ -568,7 +571,6 @@
|
||||
"retrieveHydrationInfo",
|
||||
"runAfterLeaveAnimations",
|
||||
"runEffectsInView",
|
||||
"runEnterAnimations",
|
||||
"runInInjectionContext",
|
||||
"runLeaveAnimationsWithCallback",
|
||||
"runPlatformInitializers",
|
||||
|
||||
Reference in New Issue
Block a user