Skip to content

Commit aab7367

Browse files
fix(core): update animation scheduling (#64441)
In some rare cases, it seems the animation queue disappears despite being afterEveryRender. This updates the animation scheduler to be afterNextRender instead and only schedules it when we need to. fixes: #64423 PR Close #64441
1 parent 9e4ba3e commit aab7367

File tree

13 files changed

+225
-81
lines changed

13 files changed

+225
-81
lines changed

packages/core/src/animation/interfaces.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,6 @@ export const ANIMATIONS_DISABLED = new InjectionToken<boolean>(
1818
},
1919
);
2020

21-
export interface AnimationQueue {
22-
queue: Set<Function>;
23-
isScheduled: boolean;
24-
}
25-
26-
/**
27-
* A [DI token](api/core/InjectionToken) for the queue of all animations.
28-
*/
29-
export const ANIMATION_QUEUE = new InjectionToken<AnimationQueue>(
30-
typeof ngDevMode !== 'undefined' && ngDevMode ? 'AnimationQueue' : '',
31-
{
32-
providedIn: 'root',
33-
factory: () => {
34-
return {
35-
queue: new Set(),
36-
isScheduled: false,
37-
};
38-
},
39-
},
40-
);
41-
4221
/**
4322
* The event type for when `animate.enter` and `animate.leave` are used with function
4423
* callbacks.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {afterNextRender} from '../render3/after_render/hooks';
10+
import {InjectionToken, Injector} from '../di';
11+
import {NodeAnimations} from './interfaces';
12+
13+
export interface AnimationQueue {
14+
queue: Set<Function>;
15+
isScheduled: boolean;
16+
scheduler: Function | null;
17+
}
18+
19+
/**
20+
* A [DI token](api/core/InjectionToken) for the queue of all animations.
21+
*/
22+
export const ANIMATION_QUEUE = new InjectionToken<AnimationQueue>(
23+
typeof ngDevMode !== 'undefined' && ngDevMode ? 'AnimationQueue' : '',
24+
{
25+
providedIn: 'root',
26+
factory: () => {
27+
return {
28+
queue: new Set(),
29+
isScheduled: false,
30+
scheduler: null,
31+
};
32+
},
33+
},
34+
);
35+
36+
export function addToAnimationQueue(injector: Injector, animationFns: Function | Function[]) {
37+
const animationQueue = injector.get(ANIMATION_QUEUE);
38+
if (Array.isArray(animationFns)) {
39+
for (const animateFn of animationFns) {
40+
animationQueue.queue.add(animateFn);
41+
}
42+
} else {
43+
animationQueue.queue.add(animationFns);
44+
}
45+
animationQueue.scheduler && animationQueue.scheduler(injector);
46+
}
47+
48+
export function scheduleAnimationQueue(injector: Injector) {
49+
const animationQueue = injector.get(ANIMATION_QUEUE);
50+
// We only want to schedule the animation queue if it hasn't already been scheduled.
51+
if (!animationQueue.isScheduled) {
52+
afterNextRender(
53+
() => {
54+
animationQueue.isScheduled = false;
55+
for (let animateFn of animationQueue.queue) {
56+
animateFn();
57+
}
58+
animationQueue.queue.clear();
59+
},
60+
{injector},
61+
);
62+
animationQueue.isScheduled = true;
63+
}
64+
}
65+
66+
export function initializeAnimationQueueScheduler(injector: Injector) {
67+
const animationQueue = injector.get(ANIMATION_QUEUE);
68+
animationQueue.scheduler = scheduleAnimationQueue;
69+
animationQueue.scheduler(injector);
70+
}
71+
72+
export function queueEnterAnimations(
73+
injector: Injector,
74+
enterAnimations: Map<number, NodeAnimations>,
75+
) {
76+
for (const [_, nodeAnimations] of enterAnimations) {
77+
addToAnimationQueue(injector, nodeAnimations.animateFns);
78+
}
79+
}

packages/core/src/render3/instructions/animation.ts

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,19 @@
77
*/
88

99
import {
10-
ANIMATION_QUEUE,
1110
AnimationCallbackEvent,
1211
AnimationFunction,
1312
MAX_ANIMATION_TIMEOUT,
1413
} from '../../animation/interfaces';
1514
import {getLView, getCurrentTNode} from '../state';
16-
import {RENDERER, INJECTOR, CONTEXT, LView, ANIMATIONS} from '../interfaces/view';
15+
import {RENDERER, INJECTOR, CONTEXT, LView} from '../interfaces/view';
1716
import {getNativeByTNode} from '../util/view_utils';
1817
import {performanceMarkFeature} from '../../util/performance';
1918
import {Renderer} from '../interfaces/renderer';
2019
import {NgZone} from '../../zone';
2120
import {determineLongestAnimation, allLeavingAnimations} from '../../animation/longest_animation';
2221
import {TNode} from '../interfaces/node';
2322
import {promiseWithResolvers} from '../../util/promise_with_resolvers';
24-
import {Injector} from '../../di';
25-
import {afterEveryRender} from '../after_render/hooks';
2623

2724
import {
2825
addAnimationToLView,
@@ -47,6 +44,7 @@ import {
4744
trackEnterClasses,
4845
trackLeavingNodes,
4946
} from '../../animation/utils';
47+
import {initializeAnimationQueueScheduler, queueEnterAnimations} from '../../animation/queue';
5048

5149
/**
5250
* Instruction to handle the `animate.enter` behavior for class bindings.
@@ -77,7 +75,11 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
7775
runEnterAnimation(lView, tNode, value),
7876
);
7977

80-
queueEnterAnimations(lView);
78+
initializeAnimationQueueScheduler(lView[INJECTOR]);
79+
80+
// TODO(thePunderWoman): it's unclear why we need to queue animations here, but without this,
81+
// animating through host bindings fails
82+
queueEnterAnimations(lView[INJECTOR], getLViewEnterAnimations(lView));
8183

8284
return ɵɵanimateEnter; // For chaining
8385
}
@@ -198,7 +200,11 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
198200
runEnterAnimationFunction(lView, tNode, value),
199201
);
200202

201-
queueEnterAnimations(lView);
203+
initializeAnimationQueueScheduler(lView[INJECTOR]);
204+
205+
// TODO(thePunderWoman): it's unclear why we need to queue animations here, but without this,
206+
// animating through host bindings fails
207+
queueEnterAnimations(lView[INJECTOR], getLViewEnterAnimations(lView));
202208

203209
return ɵɵanimateEnterListener;
204210
}
@@ -244,7 +250,7 @@ export function ɵɵanimateLeave(value: string | Function): typeof ɵɵanimateLe
244250
runLeaveAnimations(lView, tNode, value),
245251
);
246252

247-
enableAnimationQueueScheduler(lView[INJECTOR]);
253+
initializeAnimationQueueScheduler(lView[INJECTOR]);
248254

249255
return ɵɵanimateLeave; // For chaining
250256
}
@@ -377,7 +383,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
377383
runLeaveAnimationFunction(lView, tNode, value),
378384
);
379385

380-
enableAnimationQueueScheduler(lView[INJECTOR]);
386+
initializeAnimationQueueScheduler(lView[INJECTOR]);
381387

382388
return ɵɵanimateLeaveListener; // For chaining
383389
}
@@ -465,38 +471,3 @@ function runLeaveAnimationFunction(
465471
// Ensure cleanup if the LView is destroyed before the animation runs.
466472
return {promise, resolve};
467473
}
468-
469-
function queueEnterAnimations(lView: LView) {
470-
enableAnimationQueueScheduler(lView[INJECTOR]);
471-
const enterAnimations = lView[ANIMATIONS]?.enter;
472-
if (enterAnimations) {
473-
const animationQueue = lView[INJECTOR].get(ANIMATION_QUEUE);
474-
for (const [_, nodeAnimations] of enterAnimations) {
475-
for (const animateFn of nodeAnimations.animateFns) {
476-
animationQueue.queue.add(animateFn);
477-
}
478-
}
479-
}
480-
}
481-
482-
function enableAnimationQueueScheduler(injector: Injector) {
483-
const animationQueue = injector.get(ANIMATION_QUEUE);
484-
// We only need to schedule the animation queue runner once per application.
485-
if (!animationQueue.isScheduled) {
486-
afterEveryRender(
487-
() => {
488-
runQueuedAnimations(injector);
489-
},
490-
{injector},
491-
);
492-
animationQueue.isScheduled = true;
493-
}
494-
}
495-
496-
function runQueuedAnimations(injector: Injector) {
497-
const animationQueue = injector.get(ANIMATION_QUEUE);
498-
for (let animateFn of animationQueue.queue) {
499-
animateFn();
500-
}
501-
animationQueue.queue.clear();
502-
}

packages/core/src/render3/node_manipulation.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ import {profiler} from './profiler';
8282
import {ProfilerEvent} from './profiler_types';
8383
import {getLViewParent, getNativeByTNode, unwrapRNode} from './util/view_utils';
8484
import {allLeavingAnimations} from '../animation/longest_animation';
85-
import {ANIMATION_QUEUE} from '../animation/interfaces';
8685
import {Injector} from '../di';
86+
import {addToAnimationQueue, queueEnterAnimations} from '../animation/queue';
8787

8888
const enum WalkTNodeTreeAction {
8989
/** node create in the native environment. Run on initial creation. */
@@ -110,10 +110,7 @@ function maybeQueueEnterAnimation(
110110
): void {
111111
const enterAnimations = parentLView?.[ANIMATIONS]?.enter;
112112
if (parent !== null && enterAnimations && enterAnimations.has(tNode.index)) {
113-
const animationQueue = injector.get(ANIMATION_QUEUE);
114-
for (const animateFn of enterAnimations.get(tNode.index)!.animateFns) {
115-
animationQueue.queue.add(animateFn);
116-
}
113+
queueEnterAnimations(injector, enterAnimations);
117114
}
118115
}
119116

@@ -182,17 +179,6 @@ function applyToElementOrContainer(
182179
}
183180
}
184181

185-
function addToAnimationQueue(injector: Injector, animationFns: Function | Function[]) {
186-
const animationQueue = injector.get(ANIMATION_QUEUE);
187-
if (Array.isArray(animationFns)) {
188-
for (const animateFn of animationFns) {
189-
animationQueue.queue.add(animateFn);
190-
}
191-
} else {
192-
animationQueue.queue.add(animationFns);
193-
}
194-
}
195-
196182
/**
197183
* Removes all DOM elements associated with a view.
198184
*

0 commit comments

Comments
 (0)