From 802dbcc2a0c5d3784cb04b4c78ea71ed0925327c Mon Sep 17 00:00:00 2001 From: Jessica Janiuk Date: Wed, 27 Aug 2025 10:44:24 +0200 Subject: [PATCH] fix(core): prevent animation events from being cleaned up on destroy (#63414) This will allow manually subscribed animation events to still fire when using `animate.leave`. Otherwise they were being cleaned up before the animations happened. fixes: #63391 PR Close #63414 --- packages/core/src/render3/view/listeners.ts | 34 ++++++++++++------- .../core/test/acceptance/animation_spec.ts | 9 ++++- .../forms_reactive/bundle.golden_symbols.json | 1 + .../bundle.golden_symbols.json | 1 + .../router/bundle.golden_symbols.json | 1 + 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/core/src/render3/view/listeners.ts b/packages/core/src/render3/view/listeners.ts index 3fcb4b39bc0..de94331c113 100644 --- a/packages/core/src/render3/view/listeners.ts +++ b/packages/core/src/render3/view/listeners.ts @@ -163,23 +163,33 @@ export function listenToDomEvent( stashEventListenerImpl(lView, target, eventName, wrappedListener); const cleanupFn = renderer.listen(target as RElement, eventName, wrappedListener); - const idxOrTargetGetter = eventTargetResolver - ? (_lView: LView) => eventTargetResolver(unwrapRNode(_lView[tNode.index])) - : tNode.index; - storeListenerCleanup( - idxOrTargetGetter, - tView, - lView, - eventName, - wrappedListener, - cleanupFn, - false, - ); + // We skip cleaning up animation event types to ensure leaving animation events can be used. + // These events should be automatically garbage collected anyway after the element is + // removed from the DOM. + if (!isAnimationEventType(eventName)) { + const idxOrTargetGetter = eventTargetResolver + ? (_lView: LView) => eventTargetResolver(unwrapRNode(_lView[tNode.index])) + : tNode.index; + + storeListenerCleanup( + idxOrTargetGetter, + tView, + lView, + eventName, + wrappedListener, + cleanupFn, + false, + ); + } } return hasCoalesced; } +function isAnimationEventType(eventName: string): boolean { + return eventName.startsWith('animation') || eventName.startsWith('transition'); +} + /** * A utility function that checks if a given element has already an event handler registered for an * event with a specified name. The TView.cleanup data structure is used to find out which events diff --git a/packages/core/test/acceptance/animation_spec.ts b/packages/core/test/acceptance/animation_spec.ts index 5441bcefe2a..979097a479b 100644 --- a/packages/core/test/acceptance/animation_spec.ts +++ b/packages/core/test/acceptance/animation_spec.ts @@ -47,14 +47,20 @@ describe('Animation', () => { `; it('should delay element removal when an animation is specified', fakeAsync(() => { + const logSpy = jasmine.createSpy('logSpy'); @Component({ selector: 'test-cmp', styles: styles, - template: '
@if (show()) {

I should fade

}
', + template: + '
@if (show()) {

I should fade

}
', encapsulation: ViewEncapsulation.None, }) class TestComponent { show = signal(true); + + logMe(event: AnimationEvent) { + logSpy(); + } } TestBed.configureTestingModule({animationsEnabled: true}); @@ -76,6 +82,7 @@ describe('Animation', () => { new AnimationEvent('animationend', {animationName: 'fade-out'}), ); expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"'); + expect(logSpy).toHaveBeenCalled(); })); it('should remove right away when animations are disabled', fakeAsync(() => { diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index 2796d0f122c..cc9ac4a68db 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -696,6 +696,7 @@ "invokeHostBindingsInCreationMode", "isAbstractControlOptions", "isAngularZoneProperty", + "isAnimationEventType", "isAnimationProp", "isApplicationBootstrapConfig", "isArrayLike", diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index aaa5ac72275..1aa247c42cf 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -697,6 +697,7 @@ "invokeDirectivesHostBindings", "invokeHostBindingsInCreationMode", "isAngularZoneProperty", + "isAnimationEventType", "isAnimationProp", "isApplicationBootstrapConfig", "isArrayLike", diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index a245562b1b1..5506fc8170a 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -791,6 +791,7 @@ "invokeHostBindingsInCreationMode", "isActiveMatchOptions", "isAngularZoneProperty", + "isAnimationEventType", "isAnimationProp", "isApplicationBootstrapConfig", "isArrayLike",