Revert "fix(core): Fixes animations in conjunction with content projection (#63776)" (#64189)

This reverts commit 1cb16fddb5.

PR Close #64189
This commit is contained in:
kirjs
2025-10-01 18:30:25 -04:00
committed by Miles Malerba
parent be0455adda
commit d892aabf6e
15 changed files with 95 additions and 431 deletions
+2 -23
View File
@@ -18,27 +18,6 @@ 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.
@@ -99,10 +78,10 @@ export interface LongestAnimation {
export interface AnimationLViewData {
// Enter animations that apply to nodes in this view
enter?: Map<number, Function[]>;
enter?: Function[];
// Leave animations that apply to nodes in this view
leave?: Map<number, Function[]>;
leave?: (() => Promise<void>)[];
// Leave animations that apply to nodes in this view
// We chose to use unknown instead of PromiseSettledResult<void> to avoid requiring the type
+7 -17
View File
@@ -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): Map<number, Function[]> {
export function getLViewEnterAnimations(lView: LView): Function[] {
const animationData = (lView[ANIMATIONS] ??= {});
return (animationData.enter ??= new Map<number, Function[]>());
return (animationData.enter ??= []);
}
/**
* Retrieves the list of specified leave animations from the lView
*/
export function getLViewLeaveAnimations(lView: LView): Map<number, Function[]> {
export function getLViewLeaveAnimations(lView: LView): Function[] {
const animationData = (lView[ANIMATIONS] ??= {});
return (animationData.leave ??= new Map<number, Function[]>());
return (animationData.leave ??= []);
}
/**
@@ -244,18 +244,8 @@ export function isLongestAnimation(
}
/**
* 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
* Determines if a given tNode is a content projection root node.
*/
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);
export function isTNodeContentProjectionRoot(tNode: TNode): boolean {
return Array.isArray(tNode.projection);
}
@@ -7,13 +7,12 @@
*/
import {
ANIMATION_QUEUE,
AnimationCallbackEvent,
AnimationFunction,
MAX_ANIMATION_TIMEOUT,
} from '../../animation/interfaces';
import {getLView, getCurrentTNode} from '../state';
import {RENDERER, INJECTOR, CONTEXT, LView, ANIMATIONS} from '../interfaces/view';
import {RENDERER, INJECTOR, CONTEXT, LView} from '../interfaces/view';
import {getNativeByTNode} from '../util/view_utils';
import {performanceMarkFeature} from '../../util/performance';
import {Renderer} from '../interfaces/renderer';
@@ -21,11 +20,8 @@ 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,
@@ -70,16 +66,12 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
cancelLeavingNodes(tNode, lView);
addAnimationToLView(getLViewEnterAnimations(lView), tNode, () =>
runEnterAnimation(lView, tNode, value),
);
queueEnterAnimations(lView);
getLViewEnterAnimations(lView).push(() => runEnterAnimation(lView, tNode, value));
return ɵɵanimateEnter; // For chaining
}
export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Function) {
export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Function): void {
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
@@ -102,7 +94,7 @@ export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Fu
const eventName = event instanceof AnimationEvent ? 'animationend' : 'transitionend';
ngZone.runOutsideAngular(() => {
renderer.listen(nativeElement, eventName, handleEnterAnimationEnd);
cleanupFns.push(renderer.listen(nativeElement, eventName, handleEnterAnimationEnd));
});
};
@@ -126,7 +118,6 @@ 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.
@@ -191,11 +182,7 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
cancelLeavingNodes(tNode, lView);
addAnimationToLView(getLViewEnterAnimations(lView), tNode, () =>
runEnterAnimationFunction(lView, tNode, value),
);
queueEnterAnimations(lView);
getLViewEnterAnimations(lView).push(() => runEnterAnimationFunction(lView, tNode, value));
return ɵɵanimateEnterListener;
}
@@ -203,7 +190,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): void {
function runEnterAnimationFunction(lView: LView, tNode: TNode, value: AnimationFunction) {
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
@@ -237,16 +224,19 @@ export function ɵɵanimateLeave(value: string | Function): typeof ɵɵanimateLe
const tNode = getCurrentTNode()!;
addAnimationToLView(getLViewLeaveAnimations(lView), tNode, () =>
runLeaveAnimations(lView, tNode, value),
getLViewLeaveAnimations(lView).push(() =>
runLeaveAnimations(lView, tNode, value, animationsDisabled),
);
enableAnimationQueueScheduler(lView[INJECTOR]);
return ɵɵanimateLeave; // For chaining
}
function runLeaveAnimations(lView: LView, tNode: TNode, value: string | Function): Promise<void> {
function runLeaveAnimations(
lView: LView,
tNode: TNode,
value: string | Function,
animationsDisabled: boolean,
): Promise<void> {
const {promise, resolve} = promiseWithResolvers<void>();
const nativeElement = getNativeByTNode(tNode, lView) as Element;
@@ -263,6 +253,7 @@ function runLeaveAnimations(lView: LView, tNode: TNode, value: string | Function
tNode,
activeClasses,
renderer,
animationsDisabled,
ngZone,
resolve,
);
@@ -282,11 +273,17 @@ 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
@@ -310,14 +307,11 @@ function animateLeaveClassRunner(
}
}
resolver();
for (const fn of cleanupFns) {
fn();
}
};
ngZone.runOutsideAngular(() => {
cleanupFns.push(renderer.listen(el, 'animationend', handleOutAnimationEnd));
cleanupFns.push(renderer.listen(el, 'transitionend', handleOutAnimationEnd));
renderer.listen(el, 'animationend', handleOutAnimationEnd);
renderer.listen(el, 'transitionend', handleOutAnimationEnd);
});
trackLeavingNodes(tNode, el);
for (const item of classList) {
@@ -332,9 +326,6 @@ function animateLeaveClassRunner(
if (!longestAnimations.has(el)) {
clearLeavingNodes(tNode, el);
resolver();
for (const fn of cleanupFns) {
fn();
}
}
});
});
@@ -368,11 +359,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
const tNode = getCurrentTNode()!;
allLeavingAnimations.add(lView);
addAnimationToLView(getLViewLeaveAnimations(lView), tNode, () =>
runLeaveAnimationFunction(lView, tNode, value),
);
enableAnimationQueueScheduler(lView[INJECTOR]);
getLViewLeaveAnimations(lView).push(() => runLeaveAnimationFunction(lView, tNode, value));
return ɵɵanimateLeaveListener; // For chaining
}
@@ -429,38 +416,3 @@ 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,6 +25,7 @@ import {ComponentTemplate, HostBindingsFunction, RenderFlags} from '../interface
import {
CONTEXT,
EFFECTS_TO_SCHEDULE,
ANIMATIONS,
ENVIRONMENT,
FLAGS,
InitPhaseState,
@@ -231,6 +232,7 @@ export function refreshView<T>(
if (templateFn !== null) {
executeTemplate(tView, lView, templateFn, RenderFlags.Update, context);
}
runEnterAnimations(lView);
const hooksInitPhaseCompleted =
(flags & LViewFlags.InitPhaseStateMask) === InitPhaseState.InitPhaseCompleted;
@@ -372,6 +374,16 @@ 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.
+38 -118
View File
@@ -75,15 +75,12 @@ 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. */
@@ -102,21 +99,6 @@ 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.
@@ -124,10 +106,8 @@ function maybeQueueEnterAnimation(
function applyToElementOrContainer(
action: WalkTNodeTreeAction,
renderer: Renderer,
injector: Injector,
parent: RElement | null,
lNodeToHandle: RNode | LContainer | LView,
tNode: TNode,
beforeNode?: RNode | null,
parentLView?: LView,
) {
@@ -149,50 +129,33 @@ 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,
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);
},
);
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);
});
} else if (action === WalkTNodeTreeAction.Destroy) {
runLeaveAnimationsWithCallback(parentLView, tNode, injector, () => {
runLeaveAnimationsWithCallback(parentLView, () => {
renderer.destroyNode!(rNode);
});
}
if (lContainer != null) {
applyContainer(renderer, action, injector, lContainer, tNode, parent, beforeNode);
applyContainer(renderer, action, lContainer, 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.
*
@@ -390,42 +353,34 @@ function cleanUpView(tView: TView, lView: LView): void {
}
}
function runLeaveAnimationsWithCallback(
lView: LView | undefined,
tNode: TNode,
injector: Injector,
callback: Function,
) {
const animations = lView?.[ANIMATIONS];
if (animations == null) return callback(false);
// 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);
}
addToAnimationQueue(injector, () => {
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);
});
function hasLeaveAnimations(lView: LView | undefined): boolean {
return lView !== undefined && lView[ANIMATIONS] !== null && lView[ANIMATIONS].leave !== undefined;
}
function runAfterLeaveAnimations(lView: LView, callback: Function) {
const runningAnimations = lView[ANIMATIONS]?.running;
if (runningAnimations) {
runningAnimations.then(() => {
lView[ANIMATIONS]!.running = undefined;
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;
}
}
runAfterLeaveAnimations(lView, callback);
}
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;
}
allLeavingAnimations.delete(lView);
callback(true);
});
@@ -821,7 +776,6 @@ 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) {
@@ -848,10 +802,8 @@ function applyNodes(
applyToElementOrContainer(
action,
renderer,
injector,
parentRElement,
rawSlotValue,
tNode,
beforeNode,
lView,
);
@@ -859,24 +811,13 @@ function applyNodes(
const nextRNode = icuContainerIterate(tNode as TIcuContainerNode, lView);
let rNode: RNode | null;
while ((rNode = nextRNode())) {
applyToElementOrContainer(
action,
renderer,
injector,
parentRElement,
rNode,
tNode,
beforeNode,
lView,
);
applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode, lView);
}
applyToElementOrContainer(
action,
renderer,
injector,
parentRElement,
rawSlotValue,
tNode,
beforeNode,
lView,
);
@@ -894,10 +835,8 @@ function applyNodes(
applyToElementOrContainer(
action,
renderer,
injector,
parentRElement,
rawSlotValue,
tNode,
beforeNode,
lView,
);
@@ -1017,16 +956,7 @@ function applyProjectionRecursive(
// This should be refactored and cleaned up.
for (let i = 0; i < nodeToProjectOrRNodes.length; i++) {
const rNode = nodeToProjectOrRNodes[i];
applyToElementOrContainer(
action,
renderer,
lView[INJECTOR],
parentRElement,
rNode,
tProjectionNode,
beforeNode,
lView,
);
applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode, lView);
}
} else {
let nodeToProject: TNode | null = nodeToProjectOrRNodes;
@@ -1064,9 +994,7 @@ function applyProjectionRecursive(
function applyContainer(
renderer: Renderer,
action: WalkTNodeTreeAction,
injector: Injector,
lContainer: LContainer,
tNode: TNode,
parentRElement: RElement | null,
beforeNode: RNode | null | undefined,
) {
@@ -1084,15 +1012,7 @@ 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,
injector,
parentRElement,
anchor,
tNode,
beforeNode,
);
applyToElementOrContainer(action, renderer, parentRElement, anchor, beforeNode);
}
for (let i = CONTAINER_HEADER_OFFSET; i < lContainer.length; i++) {
const lView = lContainer[i] as LView;
@@ -400,7 +400,9 @@ 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(
@@ -441,7 +443,9 @@ 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(
@@ -805,25 +809,6 @@ 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,14 +12,9 @@ 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';
@@ -366,158 +361,5 @@ 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,7 +48,6 @@ 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', () => {
/**
@@ -1414,7 +1413,6 @@ 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,7 +5,6 @@
"ANIMATIONS",
"ANIMATION_MODULE_TYPE",
"ANIMATION_PREFIX",
"ANIMATION_QUEUE",
"ANY_STATE",
"APP_BOOTSTRAP_LISTENER",
"APP_ID",
@@ -322,7 +321,6 @@
"addAfterRenderSequencesForView",
"addClass",
"addServerStyles",
"addToAnimationQueue",
"addToEndOfViewTree",
"allLeavingAnimations",
"allocExpando",
@@ -682,7 +680,6 @@
"markViewForRefresh",
"markedFeatures",
"matchTemplateAttribute",
"maybeQueueEnterAnimation",
"maybeReturnReactiveLViewConsumer",
"maybeWrapInNotSelector",
"mergeHostAttribute",
@@ -782,6 +779,7 @@
"roundOffset",
"runAfterLeaveAnimations",
"runEffectsInView",
"runEnterAnimations",
"runInInjectionContext",
"runLeaveAnimationsWithCallback",
"runPlatformInitializers",
@@ -60,7 +60,6 @@
"lazy": [
"AFTER_RENDER_SEQUENCES_TO_ADD",
"ANIMATIONS",
"ANIMATION_QUEUE",
"APP_BOOTSTRAP_LISTENER",
"APP_ID",
"APP_INITIALIZER",
@@ -292,7 +291,6 @@
"addAfterRenderSequencesForView",
"addDepsToRegistry",
"addLViewToLContainer",
"addToAnimationQueue",
"addToArray",
"addToEndOfViewTree",
"addViewToDOM",
@@ -607,7 +605,6 @@
"markViewForRefresh",
"markedFeatures",
"matchTemplateAttribute",
"maybeQueueEnterAnimation",
"maybeReturnReactiveLViewConsumer",
"maybeWrapInNotSelector",
"mergeHostAttribute",
@@ -677,6 +674,7 @@
"retrieveHydrationInfo",
"runAfterLeaveAnimations",
"runEffectsInView",
"runEnterAnimations",
"runInInjectionContext",
"runLeaveAnimationsWithCallback",
"runPlatformInitializers",
@@ -3,7 +3,6 @@
"main": [
"AFTER_RENDER_SEQUENCES_TO_ADD",
"ANIMATIONS",
"ANIMATION_QUEUE",
"APP_BOOTSTRAP_LISTENER",
"APP_ID",
"APP_ID_ATTRIBUTE_NAME",
@@ -347,7 +346,6 @@
"addAfterRenderSequencesForView",
"addLViewToLContainer",
"addServerStyles",
"addToAnimationQueue",
"addToArray",
"addToEndOfViewTree",
"addValidators",
@@ -789,7 +787,6 @@
"matchTemplateAttribute",
"maxLengthValidator",
"maxValidator",
"maybeQueueEnterAnimation",
"maybeReturnReactiveLViewConsumer",
"maybeUnwrapEmpty",
"maybeUnwrapFn",
@@ -902,6 +899,7 @@
"retrieveHydrationInfo",
"runAfterLeaveAnimations",
"runEffectsInView",
"runEnterAnimations",
"runInInjectionContext",
"runLeaveAnimationsWithCallback",
"runPlatformInitializers",
@@ -3,7 +3,6 @@
"main": [
"AFTER_RENDER_SEQUENCES_TO_ADD",
"ANIMATIONS",
"ANIMATION_QUEUE",
"APP_BOOTSTRAP_LISTENER",
"APP_ID",
"APP_ID_ATTRIBUTE_NAME",
@@ -351,7 +350,6 @@
"addAfterRenderSequencesForView",
"addLViewToLContainer",
"addServerStyles",
"addToAnimationQueue",
"addToArray",
"addToEndOfViewTree",
"addValidators",
@@ -788,7 +786,6 @@
"markViewForRefresh",
"markedFeatures",
"matchTemplateAttribute",
"maybeQueueEnterAnimation",
"maybeReturnReactiveLViewConsumer",
"maybeUnwrapEmpty",
"maybeUnwrapFn",
@@ -900,6 +897,7 @@
"retrieveHydrationInfo",
"runAfterLeaveAnimations",
"runEffectsInView",
"runEnterAnimations",
"runInInjectionContext",
"runLeaveAnimationsWithCallback",
"runPlatformInitializers",
@@ -5,7 +5,6 @@
"AFTER_RENDER_SEQUENCES_TO_ADD",
"ALLOWED_METHODS",
"ANIMATIONS",
"ANIMATION_QUEUE",
"APP_BOOTSTRAP_LISTENER",
"APP_ID",
"APP_ID_ATTRIBUTE_NAME",
@@ -288,7 +287,6 @@
"activeConsumer",
"addAfterRenderSequencesForView",
"addServerStyles",
"addToAnimationQueue",
"addToEndOfViewTree",
"allLeavingAnimations",
"allocExpando",
@@ -638,7 +636,6 @@
"markViewDirty",
"markViewForRefresh",
"markedFeatures",
"maybeQueueEnterAnimation",
"maybeReturnReactiveLViewConsumer",
"maybeWrapInNotSelector",
"mergeHostAttribute",
@@ -718,6 +715,7 @@
"retrieveTransferredState",
"runAfterLeaveAnimations",
"runEffectsInView",
"runEnterAnimations",
"runInInjectionContext",
"runLeaveAnimationsWithCallback",
"runPlatformInitializers",
@@ -3,7 +3,6 @@
"main": [
"AFTER_RENDER_SEQUENCES_TO_ADD",
"ANIMATIONS",
"ANIMATION_QUEUE",
"APP_BASE_HREF",
"APP_BOOTSTRAP_LISTENER",
"APP_ID",
@@ -397,7 +396,6 @@
"addEmptyPathsToChildrenIfNeeded",
"addLViewToLContainer",
"addServerStyles",
"addToAnimationQueue",
"addToArray",
"addToEndOfViewTree",
"addViewToDOM",
@@ -898,7 +896,6 @@
"matchWithChecks",
"materializeViewResults",
"matrixParamsMatch",
"maybeQueueEnterAnimation",
"maybeResolveResources",
"maybeReturnReactiveLViewConsumer",
"maybeSchedule",
@@ -1017,6 +1014,7 @@
"runCanLoadGuards",
"runCanMatchGuards",
"runEffectsInView",
"runEnterAnimations",
"runInInjectionContext",
"runLeaveAnimationsWithCallback",
"runPlatformInitializers",
@@ -3,7 +3,6 @@
"main": [
"AFTER_RENDER_SEQUENCES_TO_ADD",
"ANIMATIONS",
"ANIMATION_QUEUE",
"APP_BOOTSTRAP_LISTENER",
"APP_ID",
"APP_ID_ATTRIBUTE_NAME",
@@ -232,7 +231,6 @@
"activeConsumer",
"addAfterRenderSequencesForView",
"addServerStyles",
"addToAnimationQueue",
"addToEndOfViewTree",
"allLeavingAnimations",
"allocExpando",
@@ -504,7 +502,6 @@
"markViewDirty",
"markViewForRefresh",
"markedFeatures",
"maybeQueueEnterAnimation",
"maybeReturnReactiveLViewConsumer",
"maybeWrapInNotSelector",
"mergeHostAttribute",
@@ -571,6 +568,7 @@
"retrieveHydrationInfo",
"runAfterLeaveAnimations",
"runEffectsInView",
"runEnterAnimations",
"runInInjectionContext",
"runLeaveAnimationsWithCallback",
"runPlatformInitializers",