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
This commit is contained in:
Jessica Janiuk 2025-08-27 10:44:24 +02:00 committed by Andrew Scott
parent 9096d45377
commit 802dbcc2a0
5 changed files with 33 additions and 13 deletions

View file

@ -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

View file

@ -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: '<div>@if (show()) {<p animate.leave="fade">I should fade</p>}</div>',
template:
'<div>@if (show()) {<p animate.leave="fade" (animationend)="logMe($event)">I should fade</p>}</div>',
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(() => {

View file

@ -696,6 +696,7 @@
"invokeHostBindingsInCreationMode",
"isAbstractControlOptions",
"isAngularZoneProperty",
"isAnimationEventType",
"isAnimationProp",
"isApplicationBootstrapConfig",
"isArrayLike",

View file

@ -697,6 +697,7 @@
"invokeDirectivesHostBindings",
"invokeHostBindingsInCreationMode",
"isAngularZoneProperty",
"isAnimationEventType",
"isAnimationProp",
"isApplicationBootstrapConfig",
"isArrayLike",

View file

@ -791,6 +791,7 @@
"invokeHostBindingsInCreationMode",
"isActiveMatchOptions",
"isAngularZoneProperty",
"isAnimationEventType",
"isAnimationProp",
"isApplicationBootstrapConfig",
"isArrayLike",