diff --git a/packages/core/src/render3/instructions/animation.ts b/packages/core/src/render3/instructions/animation.ts index 916f60493fe..14e3ed7656e 100644 --- a/packages/core/src/render3/instructions/animation.ts +++ b/packages/core/src/render3/instructions/animation.ts @@ -77,8 +77,11 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn initializeAnimationQueueScheduler(lView[INJECTOR]); - // TODO(thePunderWoman): it's unclear why we need to queue animations here, but without this, - // animating through host bindings fails + // We have to queue here due to the animation instruction being invoked after the element + // instruction. The DOM node has to exist before we can queue an animation. Any node that + // is not inside of control flow needs to get queued here. For nodes inside of control + // flow, those are queued in node_manipulation.ts and are deduped by a Set in the animation + // queue. queueEnterAnimations(lView[INJECTOR], getLViewEnterAnimations(lView)); return ɵɵanimateEnter; // For chaining @@ -202,8 +205,11 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa initializeAnimationQueueScheduler(lView[INJECTOR]); - // TODO(thePunderWoman): it's unclear why we need to queue animations here, but without this, - // animating through host bindings fails + // We have to queue here due to the animation instruction being invoked after the element + // instruction. The DOM node has to exist before we can queue an animation. Any node that + // is not inside of control flow needs to get queued here. For nodes inside of control + // flow, those are queued in node_manipulation.ts and are deduped by a Set in the animation + // queue. queueEnterAnimations(lView[INJECTOR], getLViewEnterAnimations(lView)); return ɵɵanimateEnterListener; diff --git a/packages/core/test/acceptance/animation_spec.ts b/packages/core/test/acceptance/animation_spec.ts index 94044029c93..9faa29cc46b 100644 --- a/packages/core/test/acceptance/animation_spec.ts +++ b/packages/core/test/acceptance/animation_spec.ts @@ -886,6 +886,59 @@ describe('Animation', () => { expect(cmp.el.nativeElement.outerHTML).toContain('class="slide-in"'); })); + it('should call animation function on entry when animation is specified with no control flow', fakeAsync(() => { + @Component({ + selector: 'test-cmp', + styles: styles, + template: '

I should slide in

', + encapsulation: ViewEncapsulation.None, + }) + class TestComponent { + count = signal(0); + slideIn(event: AnimationCallbackEvent) { + this.count.update((c) => (c += 1)); + event.animationComplete(); + } + } + TestBed.configureTestingModule({animationsEnabled: true}); + + const fixture = TestBed.createComponent(TestComponent); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + tickAnimationFrames(1); + expect(cmp.count()).toBe(1); + })); + + it('should call animation function only once on entry when animation is specified with control flow', fakeAsync(() => { + @Component({ + selector: 'test-cmp', + styles: styles, + template: + '
@if(show()) {

I should slide in

}
', + encapsulation: ViewEncapsulation.None, + }) + class TestComponent { + count = signal(0); + show = signal(false); + slideIn(event: AnimationCallbackEvent) { + this.count.update((c) => (c += 1)); + event.animationComplete(); + } + } + TestBed.configureTestingModule({animationsEnabled: true}); + + const fixture = TestBed.createComponent(TestComponent); + const cmp = fixture.componentInstance; + fixture.detectChanges(); + tickAnimationFrames(1); + expect(cmp.count()).toBe(0); + + cmp.show.update((s) => !s); + fixture.detectChanges(); + tickAnimationFrames(1); + expect(cmp.count()).toBe(1); + })); + it('should apply classes on entry when animation is specified', fakeAsync(() => { @Component({ selector: 'test-cmp',