diff --git a/packages/core/src/render3/instructions/animation.ts b/packages/core/src/render3/instructions/animation.ts index 432d63726bb..815cfcfb759 100644 --- a/packages/core/src/render3/instructions/animation.ts +++ b/packages/core/src/render3/instructions/animation.ts @@ -319,15 +319,18 @@ function animateLeaveClassRunner( cleanupFns.push(renderer.listen(el, 'animationend', handleOutAnimationEnd)); cleanupFns.push(renderer.listen(el, 'transitionend', handleOutAnimationEnd)); }); + trackLeavingNodes(tNode, el); + for (const item of classList) { renderer.addClass(el, item); } - // In the case that the classes added have no animations, we need to remove - // the element right away. This could happen because someone is intentionally - // preventing an animation via selector specificity. + ngZone.runOutsideAngular(() => { requestAnimationFrame(() => { + // In the case that the classes added have no animations, we need to remove + // the element right away. This could happen because someone is intentionally + // preventing an animation via selector specificity. determineLongestAnimation(el, longestAnimations, areAnimationSupported); if (!longestAnimations.has(el)) { clearLeavingNodes(tNode, el); diff --git a/packages/core/test/acceptance/animation_spec.ts b/packages/core/test/acceptance/animation_spec.ts index 32761bd2010..41b1f76cf87 100644 --- a/packages/core/test/acceptance/animation_spec.ts +++ b/packages/core/test/acceptance/animation_spec.ts @@ -374,7 +374,6 @@ describe('Animation', () => { it('should be host bindable', fakeAsync(() => { @Component({ selector: 'fade-cmp', - styles: styles, host: {'animate.leave': 'fade'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -383,6 +382,7 @@ describe('Animation', () => { @Component({ selector: 'test-cmp', + styles: styles, imports: [FadeComponent], template: '@if (show()) { }', encapsulation: ViewEncapsulation.None, @@ -400,6 +400,7 @@ describe('Animation', () => { expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"'); cmp.show.set(false); fixture.detectChanges(); + tickAnimationFrames(1); expect(cmp.show()).toBeFalsy(); expect(fixture.nativeElement.outerHTML).toContain('class="fade"'); fixture.detectChanges(); @@ -413,7 +414,6 @@ describe('Animation', () => { it('should be host bindable with brackets', fakeAsync(() => { @Component({ selector: 'fade-cmp', - styles: styles, host: {'[animate.leave]': 'fade()'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -424,6 +424,7 @@ describe('Animation', () => { @Component({ selector: 'test-cmp', + styles: styles, imports: [FadeComponent], template: '@if (show()) { }', encapsulation: ViewEncapsulation.None, @@ -441,6 +442,7 @@ describe('Animation', () => { expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"'); cmp.show.set(false); fixture.detectChanges(); + tickAnimationFrames(1); expect(cmp.show()).toBeFalsy(); expect(fixture.nativeElement.outerHTML).toContain('class="fade"'); fixture.detectChanges(); @@ -519,7 +521,6 @@ describe('Animation', () => { `; @Component({ selector: 'child-cmp', - styles: multiple, host: {'[animate.leave]': 'slide()'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -670,7 +671,6 @@ describe('Animation', () => { `; @Component({ selector: 'child-cmp', - styles: multiple, host: {'animate.leave': 'slide-out'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -1203,12 +1203,21 @@ describe('Animation', () => { it('should be host bindable', fakeAsync(() => { @Component({ - selector: 'test-cmp', - styles: styles, + selector: 'child-cmp', host: {'animate.enter': 'slide-in'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, }) + class ChildComponent {} + + @Component({ + selector: 'test-cmp', + styles: styles, + imports: [ChildComponent], + host: {'animate.enter': 'slide-in'}, + template: '', + encapsulation: ViewEncapsulation.None, + }) class TestComponent {} TestBed.configureTestingModule({animationsEnabled: true}); @@ -1227,15 +1236,23 @@ describe('Animation', () => { it('should be host bindable with brackets', fakeAsync(() => { @Component({ - selector: 'test-cmp', - styles: styles, + selector: 'child-cmp', host: {'[animate.enter]': 'slideIn()'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, }) - class TestComponent { + class ChildComponent { slideIn = signal('slide-in'); } + + @Component({ + selector: 'test-cmp', + styles: styles, + imports: [ChildComponent], + template: '', + encapsulation: ViewEncapsulation.None, + }) + class TestComponent {} TestBed.configureTestingModule({animationsEnabled: true}); const fixture = TestBed.createComponent(TestComponent); @@ -1254,12 +1271,26 @@ describe('Animation', () => { it('should be host bindable with events', fakeAsync(() => { const slideInCalled = jasmine.createSpy('slideInCalled'); @Component({ - selector: 'test-cmp', - styles: styles, + selector: 'child-cmp', host: {'(animate.enter)': 'slideIn($event)'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, }) + class ChildComponent { + slideIn(event: AnimationCallbackEvent) { + slideInCalled(); + event.target.classList.add('slide-in'); + event.animationComplete(); + } + } + + @Component({ + selector: 'test-cmp', + styles: styles, + imports: [ChildComponent], + template: '', + encapsulation: ViewEncapsulation.None, + }) class TestComponent { slideIn(event: AnimationCallbackEvent) { slideInCalled(); @@ -1277,7 +1308,6 @@ describe('Animation', () => { it('should compose class list when host binding and regular binding', fakeAsync(() => { @Component({ selector: 'child-cmp', - styles: styles, host: {'[animate.enter]': 'clazz'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, @@ -1320,7 +1350,6 @@ describe('Animation', () => { it('should compose class list when host binding a string and regular class strings', fakeAsync(() => { @Component({ selector: 'child-cmp', - styles: styles, host: {'animate.enter': 'slide-in'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None,