Skip to content

Commit 622aa92

Browse files
fix(core): update animation scheduling
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
1 parent 3d1777b commit 622aa92

13 files changed

Lines changed: 223 additions & 81 deletions

File tree

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: 12 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,10 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
7775
runEnterAnimation(lView, tNode, value),
7876
);
7977

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

8283
return ɵɵanimateEnter; // For chaining
8384
}
@@ -198,7 +199,10 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
198199
runEnterAnimationFunction(lView, tNode, value),
199200
);
200201

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

203207
return ɵɵanimateEnterListener;
204208
}
@@ -244,7 +248,7 @@ export function ɵɵanimateLeave(value: string | Function): typeof ɵɵanimateLe
244248
runLeaveAnimations(lView, tNode, value),
245249
);
246250

247-
enableAnimationQueueScheduler(lView[INJECTOR]);
251+
initializeAnimationQueueScheduler(lView[INJECTOR]);
248252

249253
return ɵɵanimateLeave; // For chaining
250254
}
@@ -377,7 +381,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
377381
runLeaveAnimationFunction(lView, tNode, value),
378382
);
379383

380-
enableAnimationQueueScheduler(lView[INJECTOR]);
384+
initializeAnimationQueueScheduler(lView[INJECTOR]);
381385

382386
return ɵɵanimateLeaveListener; // For chaining
383387
}
@@ -465,38 +469,3 @@ function runLeaveAnimationFunction(
465469
// Ensure cleanup if the LView is destroyed before the animation runs.
466470
return {promise, resolve};
467471
}
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)