Skip to content

Commit 1576ff0

Browse files
fix(core): skip leave animations on view swaps
We accounted for skipping leave animations during moves, but not swaps. This accounts for the swap cases and updates how we deal with swaps and moves. Now we always queue animations and then essentially dequeue them if we attach them back in the same render pass. fixes: angular#64818 fixes: angular#64730
1 parent a0fe177 commit 1576ff0

File tree

7 files changed

+156
-72
lines changed

7 files changed

+156
-72
lines changed

packages/core/src/animation/interfaces.ts

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -52,51 +52,38 @@ const MAX_ANIMATION_TIMEOUT_DEFAULT = 4000;
5252
*/
5353
export type AnimationFunction = (event: AnimationCallbackEvent) => void;
5454

55-
export type AnimationEventFunction = (
56-
el: Element,
57-
value: AnimationFunction,
58-
) => AnimationRemoveFunction;
59-
export type AnimationClassFunction = (
60-
el: Element,
61-
value: Set<string> | null,
62-
resolvers: Function[] | undefined,
63-
) => AnimationRemoveFunction;
64-
export type AnimationRemoveFunction = (removeFn: VoidFunction) => void;
65-
66-
export interface AnimationDetails {
67-
classes: Set<string> | null;
68-
classFns?: Function[];
69-
animateFn: AnimationRemoveFunction;
70-
isEventBinding: boolean;
71-
}
55+
export type RunEnterAnimationFn = VoidFunction;
56+
export type RunLeaveAnimationFn = () => {promise: Promise<void>; resolve: VoidFunction};
7257

7358
export interface LongestAnimation {
7459
animationName: string | undefined;
7560
propertyName: string | undefined;
7661
duration: number;
7762
}
7863

79-
export interface NodeAnimations {
80-
animateFns: Function[];
64+
export interface EnterNodeAnimations {
65+
animateFns: RunEnterAnimationFn[];
66+
resolvers?: VoidFunction[];
67+
}
68+
export interface LeaveNodeAnimations {
69+
animateFns: RunLeaveAnimationFn[];
8170
resolvers?: VoidFunction[];
8271
}
8372

8473
export interface AnimationLViewData {
8574
// Enter animations that apply to nodes in this view
86-
enter?: Map<number, NodeAnimations>;
75+
enter?: Map<number, EnterNodeAnimations>;
8776

8877
// Leave animations that apply to nodes in this view
89-
leave?: Map<number, NodeAnimations>;
78+
leave?: Map<number, LeaveNodeAnimations>;
9079

9180
// Leave animations that apply to nodes in this view
9281
// We chose to use unknown instead of PromiseSettledResult<void> to avoid requiring the type
9382
running?: Promise<unknown>;
9483

95-
// Skip leave animations
96-
// This flag is solely used when move operations occur. DOM Node move
97-
// operations occur in lists, like `@for` loops, and use the same code
98-
// path during move that detaching or removing does. So to prevent
99-
// unexpected disappearing of moving nodes, we use this flag to skip
100-
// the animations in that case.
101-
skipLeaveAnimations?: boolean;
84+
// Animation functions that have been queued for this view when the view is detached.
85+
// This is used to later remove them from the global animation queue if the view
86+
// is attached before the animation queue runs. This is used in cases where views are
87+
// moved or swapped during list reconciliation.
88+
detachFnQueue?: VoidFunction[];
10289
}

packages/core/src/animation/queue.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
import {afterNextRender} from '../render3/after_render/hooks';
1010
import {InjectionToken, Injector} from '../di';
11-
import {NodeAnimations} from './interfaces';
11+
import {AnimationLViewData, EnterNodeAnimations} from './interfaces';
1212

1313
export interface AnimationQueue {
14-
queue: Set<Function>;
14+
queue: Set<VoidFunction>;
1515
isScheduled: boolean;
1616
scheduler: Function | null;
1717
}
@@ -33,18 +33,40 @@ export const ANIMATION_QUEUE = new InjectionToken<AnimationQueue>(
3333
},
3434
);
3535

36-
export function addToAnimationQueue(injector: Injector, animationFns: Function | Function[]) {
36+
export function addToAnimationQueue(
37+
injector: Injector,
38+
animationFns: VoidFunction | VoidFunction[],
39+
animationData?: AnimationLViewData,
40+
) {
3741
const animationQueue = injector.get(ANIMATION_QUEUE);
3842
if (Array.isArray(animationFns)) {
3943
for (const animateFn of animationFns) {
4044
animationQueue.queue.add(animateFn);
45+
// If a node is detached, we need to keep track of the queued animation functions
46+
// so we can later remove them from the global animation queue if the view
47+
// is re-attached before the animation queue runs.
48+
animationData?.detachFnQueue?.push(animateFn);
4149
}
4250
} else {
4351
animationQueue.queue.add(animationFns);
52+
// If a node is detached, we need to keep track of the queued animation functions
53+
// so we can later remove them from the global animation queue if the view
54+
// is re-attached before the animation queue runs.
55+
animationData?.detachFnQueue?.push(animationFns);
4456
}
4557
animationQueue.scheduler && animationQueue.scheduler(injector);
4658
}
4759

60+
export function removeFromAnimationQueue(injector: Injector, animationData: AnimationLViewData) {
61+
const animationQueue = injector.get(ANIMATION_QUEUE);
62+
if (animationData.detachFnQueue) {
63+
for (const animationFn of animationData.detachFnQueue) {
64+
animationQueue.queue.delete(animationFn);
65+
}
66+
animationData.detachFnQueue = undefined;
67+
}
68+
}
69+
4870
export function scheduleAnimationQueue(injector: Injector) {
4971
const animationQueue = injector.get(ANIMATION_QUEUE);
5072
// We only want to schedule the animation queue if it hasn't already been scheduled.
@@ -71,7 +93,7 @@ export function initializeAnimationQueueScheduler(injector: Injector) {
7193

7294
export function queueEnterAnimations(
7395
injector: Injector,
74-
enterAnimations: Map<number, NodeAnimations>,
96+
enterAnimations: Map<number, EnterNodeAnimations>,
7597
) {
7698
for (const [_, nodeAnimations] of enterAnimations) {
7799
addToAnimationQueue(injector, nodeAnimations.animateFns);

packages/core/src/animation/utils.ts

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

99
import {stringify} from '../util/stringify'; // Adjust imports as per actual location
10-
import {ANIMATIONS_DISABLED, LongestAnimation, NodeAnimations} from './interfaces';
10+
import {
11+
ANIMATIONS_DISABLED,
12+
LongestAnimation,
13+
EnterNodeAnimations,
14+
LeaveNodeAnimations,
15+
RunLeaveAnimationFn,
16+
RunEnterAnimationFn,
17+
} from './interfaces';
1118
import {INJECTOR, LView, DECLARATION_LCONTAINER, ANIMATIONS} from '../render3/interfaces/view';
1219
import {RuntimeError, RuntimeErrorCode} from '../errors';
1320
import {Renderer} from '../render3/interfaces/renderer';
@@ -162,17 +169,17 @@ export function trackLeavingNodes(tNode: TNode, el: HTMLElement): void {
162169
/**
163170
* Retrieves the list of specified enter animations from the lView
164171
*/
165-
export function getLViewEnterAnimations(lView: LView): Map<number, NodeAnimations> {
172+
export function getLViewEnterAnimations(lView: LView): Map<number, EnterNodeAnimations> {
166173
const animationData = (lView[ANIMATIONS] ??= {});
167-
return (animationData.enter ??= new Map<number, NodeAnimations>());
174+
return (animationData.enter ??= new Map<number, EnterNodeAnimations>());
168175
}
169176

170177
/**
171178
* Retrieves the list of specified leave animations from the lView
172179
*/
173-
export function getLViewLeaveAnimations(lView: LView): Map<number, NodeAnimations> {
180+
export function getLViewLeaveAnimations(lView: LView): Map<number, LeaveNodeAnimations> {
174181
const animationData = (lView[ANIMATIONS] ??= {});
175-
return (animationData.leave ??= new Map<number, NodeAnimations>());
182+
return (animationData.leave ??= new Map<number, LeaveNodeAnimations>());
176183
}
177184

178185
/**
@@ -253,9 +260,9 @@ export function isLongestAnimation(
253260
* @param fn The animation function to be called later
254261
*/
255262
export function addAnimationToLView(
256-
animations: Map<number, NodeAnimations>,
263+
animations: Map<number, EnterNodeAnimations | LeaveNodeAnimations>,
257264
tNode: TNode,
258-
fn: Function,
265+
fn: RunEnterAnimationFn | RunLeaveAnimationFn,
259266
) {
260267
const nodeAnimations = animations.get(tNode.index) ?? {animateFns: []};
261268
nodeAnimations.animateFns.push(fn);

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
DECLARATION_COMPONENT_VIEW,
2929
HEADER_OFFSET,
3030
HYDRATION,
31+
INJECTOR,
3132
LView,
3233
TVIEW,
3334
TView,
@@ -48,6 +49,8 @@ import {
4849
removeLViewFromLContainer,
4950
} from '../view/container';
5051
import {declareNoDirectiveHostTemplate} from './template';
52+
import {removeFromAnimationQueue} from '../../animation/queue';
53+
import {allLeavingAnimations} from '../../animation/longest_animation';
5154

5255
/**
5356
* Creates an LContainer for an ng-template representing a root node
@@ -419,10 +422,11 @@ class LiveCollectionLContainerImpl extends LiveCollection<
419422
index,
420423
shouldAddViewToDom(this.templateTNode, dehydratedView),
421424
);
425+
clearDetachAnimationQueue(this.lContainer, index);
422426
}
423-
override detach(index: number, skipLeaveAnimations?: boolean): LView<RepeaterContext<unknown>> {
427+
override detach(index: number): LView<RepeaterContext<unknown>> {
424428
this.needsIndexUpdate ||= index !== this.length - 1;
425-
if (skipLeaveAnimations) setSkipLeaveAnimations(this.lContainer, index);
429+
maybeInitDetachAnimationQueue(this.lContainer, index);
426430
return detachExistingView<RepeaterContext<unknown>>(this.lContainer, index);
427431
}
428432
override create(index: number, value: unknown): LView<RepeaterContext<unknown>> {
@@ -570,13 +574,35 @@ function getLContainer(lView: LView, index: number): LContainer {
570574
return lContainer;
571575
}
572576

573-
function setSkipLeaveAnimations(lContainer: LContainer, index: number): void {
577+
function clearDetachAnimationQueue(lContainer: LContainer, index: number): void {
578+
if (lContainer.length <= CONTAINER_HEADER_OFFSET) return;
579+
580+
const indexInContainer = CONTAINER_HEADER_OFFSET + index;
581+
const viewToDetach = lContainer[indexInContainer];
582+
const animations = viewToDetach
583+
? (viewToDetach[ANIMATIONS] as AnimationLViewData | undefined)
584+
: undefined;
585+
if (
586+
viewToDetach &&
587+
animations &&
588+
animations.detachFnQueue &&
589+
animations.detachFnQueue.length > 0
590+
) {
591+
const injector = viewToDetach[INJECTOR];
592+
removeFromAnimationQueue(injector, animations);
593+
allLeavingAnimations.delete(viewToDetach);
594+
animations.detachFnQueue = undefined;
595+
}
596+
}
597+
598+
function maybeInitDetachAnimationQueue(lContainer: LContainer, index: number): void {
574599
if (lContainer.length <= CONTAINER_HEADER_OFFSET) return;
575600

576601
const indexInContainer = CONTAINER_HEADER_OFFSET + index;
577602
const viewToDetach = lContainer[indexInContainer];
578603
if (viewToDetach && viewToDetach[ANIMATIONS]) {
579-
(viewToDetach[ANIMATIONS] as AnimationLViewData).skipLeaveAnimations = true;
604+
const animations = viewToDetach[ANIMATIONS] as AnimationLViewData;
605+
animations.detachFnQueue = [];
580606
}
581607
}
582608

packages/core/src/render3/list_reconciliation.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export abstract class LiveCollection<T, V> {
2121
abstract get length(): number;
2222
abstract at(index: number): V;
2323
abstract attach(index: number, item: T): void;
24-
abstract detach(index: number, skipLeaveAnimations?: boolean): T;
24+
abstract detach(index: number): T;
2525
abstract create(index: number, value: V): T;
2626
destroy(item: T): void {
2727
// noop by default
@@ -50,7 +50,7 @@ export abstract class LiveCollection<T, V> {
5050
// DOM nodes, which would trigger `animate.leave` bindings. We need to skip
5151
// those animations in the case of a move operation so the moving elements don't
5252
// unexpectedly disappear.
53-
this.attach(newIdx, this.detach(prevIndex, true /* skipLeaveAnimations */));
53+
this.attach(newIdx, this.detach(prevIndex));
5454
}
5555
}
5656

@@ -131,6 +131,7 @@ export function reconcile<T, V>(
131131
// compare from the beginning
132132
const liveStartValue = liveCollection.at(liveStartIdx);
133133
const newStartValue = newCollection[liveStartIdx];
134+
const newLastValue = newCollection[newEndIdx];
134135

135136
if (ngDevMode) {
136137
recordDuplicateKeys(duplicateKeys!, trackByFn(liveStartIdx, newStartValue), liveStartIdx);
@@ -180,6 +181,7 @@ export function reconcile<T, V>(
180181
const liveStartKey = trackByFn(liveStartIdx, liveStartValue);
181182
const liveEndKey = trackByFn(liveEndIdx, liveEndValue);
182183
const newStartKey = trackByFn(liveStartIdx, newStartValue);
184+
183185
if (Object.is(newStartKey, liveEndKey)) {
184186
const newEndKey = trackByFn(newEndIdx, newEndValue);
185187
// detect swap on both ends;

packages/core/src/render3/node_manipulation.ts

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {getLViewParent, getNativeByTNode, unwrapRNode} from './util/view_utils';
8484
import {allLeavingAnimations} from '../animation/longest_animation';
8585
import {Injector} from '../di';
8686
import {addToAnimationQueue, queueEnterAnimations} from '../animation/queue';
87+
import {RunLeaveAnimationFn} from '../animation/interfaces';
8788

8889
const enum WalkTNodeTreeAction {
8990
/** node create in the native environment. Run on initial creation. */
@@ -386,37 +387,35 @@ function runLeaveAnimationsWithCallback(
386387
if (animations == null || animations.leave == undefined || !animations.leave.has(tNode.index))
387388
return callback(false);
388389

389-
// this is solely for move operations to prevent leave animations from running
390-
// on the moved nodes, which would have deleted the node.
391-
if (animations.skipLeaveAnimations) {
392-
animations.skipLeaveAnimations = false;
393-
return callback(false);
394-
}
395-
396390
if (lView) allLeavingAnimations.add(lView);
397391

398-
addToAnimationQueue(injector, () => {
399-
// it's possible that in the time between when the leave animation was
400-
// and the time it was executed, the data structure changed. So we need
401-
// to be safe here.
402-
if (animations.leave && animations.leave.has(tNode.index)) {
403-
const leaveAnimationMap = animations.leave;
404-
const leaveAnimations = leaveAnimationMap.get(tNode.index);
405-
const runningAnimations = [];
406-
if (leaveAnimations) {
407-
for (let index = 0; index < leaveAnimations.animateFns.length; index++) {
408-
const animationFn = leaveAnimations.animateFns[index];
409-
const {promise} = animationFn();
410-
runningAnimations.push(promise);
392+
addToAnimationQueue(
393+
injector,
394+
() => {
395+
// it's possible that in the time between when the leave animation was
396+
// and the time it was executed, the data structure changed. So we need
397+
// to be safe here.
398+
if (animations.leave && animations.leave.has(tNode.index)) {
399+
const leaveAnimationMap = animations.leave;
400+
const leaveAnimations = leaveAnimationMap.get(tNode.index);
401+
const runningAnimations = [];
402+
if (leaveAnimations) {
403+
for (let index = 0; index < leaveAnimations.animateFns.length; index++) {
404+
const animationFn = leaveAnimations.animateFns[index];
405+
const {promise} = animationFn() as ReturnType<RunLeaveAnimationFn>;
406+
runningAnimations.push(promise);
407+
}
408+
animations.detachFnQueue = undefined;
411409
}
410+
animations.running = Promise.allSettled(runningAnimations);
411+
runAfterLeaveAnimations(lView!, callback);
412+
} else {
413+
if (lView) allLeavingAnimations.delete(lView);
414+
callback(false);
412415
}
413-
animations.running = Promise.allSettled(runningAnimations);
414-
runAfterLeaveAnimations(lView!, callback);
415-
} else {
416-
if (lView) allLeavingAnimations.delete(lView);
417-
callback(false);
418-
}
419-
});
416+
},
417+
animations,
418+
);
420419
}
421420

422421
function runAfterLeaveAnimations(lView: LView, callback: Function) {

0 commit comments

Comments
 (0)