mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(core): track enter and leave animations in LView (#63450)
This tracks the enter and leave functions in the LView to be executed at a safe time for change detection. PR Close #63450
This commit is contained in:
@@ -69,3 +69,7 @@ export interface AnimationDetails {
|
||||
animateFn: AnimationRemoveFunction;
|
||||
isEventBinding: boolean;
|
||||
}
|
||||
export interface LeaveAnimation {
|
||||
index: number;
|
||||
animateFn: Function;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
LView,
|
||||
TView,
|
||||
DECLARATION_LCONTAINER,
|
||||
ENTER_ANIMATIONS,
|
||||
LEAVE_ANIMATIONS,
|
||||
} from '../interfaces/view';
|
||||
import {RuntimeError, RuntimeErrorCode} from '../../errors';
|
||||
import {getNativeByTNode, storeCleanupWithContext} from '../util/view_utils';
|
||||
@@ -121,7 +123,9 @@ function cancelLeavingNodes(tNode: TNode, lView: LView): void {
|
||||
// this is the insertion point for the new TNode element.
|
||||
// it will be inserted before the declaring containers anchor.
|
||||
const beforeNode = getBeforeNodeForView(tNode.index, lContainer);
|
||||
// here we need to check the previous sibling of that anchor
|
||||
// here we need to check the previous sibling of that anchor. The first
|
||||
// previousSibling node will be the new element added. The second
|
||||
// previousSibling will be the one that's being removed.
|
||||
const previousNode = beforeNode?.previousSibling;
|
||||
// We really only want to cancel animations if the leaving node is the
|
||||
// same as the node before where the new node will be inserted. This is
|
||||
@@ -143,6 +147,20 @@ function trackLeavingNodes(tNode: TNode, el: HTMLElement): void {
|
||||
}
|
||||
}
|
||||
|
||||
function getLViewEnterAnimations(lView: LView): Function[] {
|
||||
if (lView[ENTER_ANIMATIONS] === null) {
|
||||
lView[ENTER_ANIMATIONS] = [];
|
||||
}
|
||||
return lView[ENTER_ANIMATIONS];
|
||||
}
|
||||
|
||||
// function getLViewLeaveAnimations(lView: LView): Function[] {
|
||||
// if (lView[LEAVE_ANIMATIONS] === null) {
|
||||
// lView[LEAVE_ANIMATIONS] = [];
|
||||
// }
|
||||
// return lView[LEAVE_ANIMATIONS];
|
||||
// }
|
||||
|
||||
/**
|
||||
* Instruction to handle the `animate.enter` behavior for class bindings.
|
||||
*
|
||||
@@ -165,6 +183,15 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
|
||||
}
|
||||
|
||||
const tNode = getCurrentTNode()!;
|
||||
|
||||
cancelLeavingNodes(tNode, lView);
|
||||
|
||||
getLViewEnterAnimations(lView).push(() => runEnterAnimation(lView, tNode, value));
|
||||
|
||||
return ɵɵanimateEnter; // For chaining
|
||||
}
|
||||
|
||||
export function runEnterAnimation(lView: LView, tNode: TNode, value: string | Function): void {
|
||||
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
|
||||
|
||||
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
|
||||
@@ -200,8 +227,6 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
|
||||
cleanupFns.push(renderer.listen(nativeElement, 'transitionstart', handleAnimationStart));
|
||||
});
|
||||
|
||||
cancelLeavingNodes(tNode, lView);
|
||||
|
||||
trackEnterClasses(nativeElement, activeClasses, cleanupFns);
|
||||
|
||||
for (const klass of activeClasses) {
|
||||
@@ -222,8 +247,6 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return ɵɵanimateEnter; // For chaining
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,15 +291,17 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
|
||||
if (areAnimationsDisabled(lView)) {
|
||||
return ɵɵanimateEnterListener;
|
||||
}
|
||||
|
||||
const tNode = getCurrentTNode()!;
|
||||
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
|
||||
|
||||
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
|
||||
|
||||
cancelLeavingNodes(tNode, lView);
|
||||
|
||||
value.call(lView[CONTEXT], {target: nativeElement, animationComplete: noOpAnimationComplete});
|
||||
getLViewEnterAnimations(lView).push(() => {
|
||||
const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement;
|
||||
|
||||
ngDevMode && assertElementNodes(nativeElement, 'animate.enter');
|
||||
|
||||
value.call(lView[CONTEXT], {target: nativeElement, animationComplete: noOpAnimationComplete});
|
||||
});
|
||||
|
||||
return ɵɵanimateEnterListener;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {ComponentTemplate, HostBindingsFunction, RenderFlags} from '../interface
|
||||
import {
|
||||
CONTEXT,
|
||||
EFFECTS_TO_SCHEDULE,
|
||||
ENTER_ANIMATIONS,
|
||||
ENVIRONMENT,
|
||||
FLAGS,
|
||||
InitPhaseState,
|
||||
@@ -260,6 +261,7 @@ export function refreshView<T>(
|
||||
// `LView` but its declaration appears after the insertion component.
|
||||
markTransplantedViewsForRefresh(lView);
|
||||
}
|
||||
runEnterAnimations(lView);
|
||||
runEffectsInView(lView);
|
||||
detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Global);
|
||||
|
||||
@@ -372,6 +374,15 @@ export function refreshView<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function runEnterAnimations(lView: LView) {
|
||||
if (lView[ENTER_ANIMATIONS]) {
|
||||
for (let animateFn of lView[ENTER_ANIMATIONS]) {
|
||||
animateFn();
|
||||
}
|
||||
lView[ENTER_ANIMATIONS] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes over embedded views (ones created through ViewContainerRef APIs) and refreshes
|
||||
* them by executing an associated template function.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.dev/license
|
||||
*/
|
||||
|
||||
import {LeaveAnimation} from '../../animation/interfaces';
|
||||
import type {ChangeDetectionScheduler} from '../../change_detection/scheduling/zoneless_scheduling';
|
||||
import {TDeferBlockDetails} from '../../defer/interfaces';
|
||||
import type {Injector} from '../../di/injector';
|
||||
@@ -69,6 +70,8 @@ export const EFFECTS_TO_SCHEDULE = 22;
|
||||
export const EFFECTS = 23;
|
||||
export const REACTIVE_TEMPLATE_CONSUMER = 24;
|
||||
export const AFTER_RENDER_SEQUENCES_TO_ADD = 25;
|
||||
export const ENTER_ANIMATIONS = 26;
|
||||
export const LEAVE_ANIMATIONS = 27;
|
||||
|
||||
/**
|
||||
* Size of LView's header. Necessary to adjust for it when setting slots.
|
||||
@@ -77,7 +80,7 @@ export const AFTER_RENDER_SEQUENCES_TO_ADD = 25;
|
||||
* instruction index into `LView` index. All other indexes should be in the `LView` index space and
|
||||
* there should be no need to refer to `HEADER_OFFSET` anywhere else.
|
||||
*/
|
||||
export const HEADER_OFFSET = 26;
|
||||
export const HEADER_OFFSET = 28;
|
||||
|
||||
// This interface replaces the real LView interface if it is an arg or a
|
||||
// return value of a public instruction. This ensures we don't need to expose
|
||||
@@ -367,6 +370,12 @@ export interface LView<T = unknown> extends Array<any> {
|
||||
|
||||
// AfterRenderSequences that need to be scheduled
|
||||
[AFTER_RENDER_SEQUENCES_TO_ADD]: AfterRenderSequence[] | null;
|
||||
|
||||
// Enter animations that apply to nodes in this view
|
||||
[ENTER_ANIMATIONS]: Function[] | null;
|
||||
|
||||
// Leave animations that apply to nodes in this view
|
||||
[LEAVE_ANIMATIONS]: Set<LeaveAnimation> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
Component,
|
||||
Directive,
|
||||
ElementRef,
|
||||
input,
|
||||
InputSignal,
|
||||
signal,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
@@ -933,7 +935,7 @@ describe('Animation', () => {
|
||||
expect(cmp.el.nativeElement.outerHTML).not.toContain('class="slide-in fade-in"');
|
||||
}));
|
||||
|
||||
it('should support multple classes as a single string separated by a space', fakeAsync(() => {
|
||||
it('should support multiple classes as a single string separated by a space', fakeAsync(() => {
|
||||
const multiple = `
|
||||
.slide-in {
|
||||
animation: slide-in 1ms;
|
||||
@@ -988,7 +990,7 @@ describe('Animation', () => {
|
||||
expect(cmp.el.nativeElement.outerHTML).not.toContain('class="slide-in fade-in"');
|
||||
}));
|
||||
|
||||
it('should support multple classes as a single string separated by a space', fakeAsync(() => {
|
||||
it('should support multiple classes as a single string separated by a space', fakeAsync(() => {
|
||||
const multiple = `
|
||||
.slide-in {
|
||||
animation: slide-in 1ms;
|
||||
@@ -1243,6 +1245,41 @@ describe('Animation', () => {
|
||||
expect(childCmp.nativeElement.className).not.toContain('slide-in fade-in');
|
||||
}));
|
||||
|
||||
xit('should support signal inputs', fakeAsync(() => {
|
||||
@Component({
|
||||
selector: 'child-cmp',
|
||||
styles: styles,
|
||||
template: '<p [animate.enter]="enterAnim()">I should fade</p>',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
class ChildComponent {
|
||||
public enterAnim: InputSignal<string | string[]> = input.required<string | string[]>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'test-cmp',
|
||||
styles: styles,
|
||||
imports: [ChildComponent],
|
||||
template: '<child-cmp enterAnim="fade-in" />',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
class TestComponent {}
|
||||
TestBed.configureTestingModule({animationsEnabled: true});
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
const childCmp = fixture.debugElement.query(By.css('child-cmp'));
|
||||
|
||||
expect(childCmp.nativeElement.className).toContain('fade-in');
|
||||
childCmp.nativeElement.dispatchEvent(new AnimationEvent('animationstart'));
|
||||
childCmp.nativeElement.dispatchEvent(
|
||||
new AnimationEvent('animationend', {animationName: 'fade-in'}),
|
||||
);
|
||||
fixture.detectChanges();
|
||||
expect(childCmp.nativeElement.className).not.toContain('fade-in');
|
||||
}));
|
||||
|
||||
it('should reset leave animation and not duplicate node when toggled quickly', fakeAsync(() => {
|
||||
const animateStyles = `
|
||||
.slide-in {
|
||||
@@ -1297,6 +1334,8 @@ describe('Animation', () => {
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
expect(cmp.show()).toBeTruthy();
|
||||
fixture.detectChanges();
|
||||
tickAnimationFrames(1);
|
||||
const paragraphs = fixture.debugElement.queryAll(By.css('p'));
|
||||
expect(paragraphs.length).toBe(1);
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user