Skip to content

Commit c8d3439

Browse files
refactor(core): Reorganize and cleanup animations code (#63775)
This is a pure re-organization of the animations code. No functionality changes, but it should be easier to navigate now. Utility classes have been moved to a `utils.ts` file. The related functions in the instructions have been grouped closer together. PR Close #63775
1 parent 9def492 commit c8d3439

File tree

2 files changed

+358
-286
lines changed

2 files changed

+358
-286
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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 {stringify} from '../util/stringify'; // Adjust imports as per actual location
10+
import {ANIMATIONS_DISABLED, LongestAnimation} from './interfaces';
11+
import {INJECTOR, LView, DECLARATION_LCONTAINER, ANIMATIONS} from '../render3/interfaces/view';
12+
import {RuntimeError, RuntimeErrorCode} from '../errors';
13+
import {Renderer} from '../render3/interfaces/renderer';
14+
import {RElement} from '../render3/interfaces/renderer_dom';
15+
import {TNode} from '../render3/interfaces/node';
16+
import {getBeforeNodeForView} from '../render3/node_manipulation';
17+
18+
const DEFAULT_ANIMATIONS_DISABLED = false;
19+
20+
export const areAnimationSupported =
21+
(typeof ngServerMode === 'undefined' || !ngServerMode) &&
22+
typeof document !== 'undefined' &&
23+
// tslint:disable-next-line:no-toplevel-property-access
24+
typeof document?.documentElement?.getAnimations === 'function';
25+
26+
/**
27+
* Helper function to check if animations are disabled via injection token
28+
*/
29+
export function areAnimationsDisabled(lView: LView): boolean {
30+
const injector = lView[INJECTOR]!;
31+
return injector.get(ANIMATIONS_DISABLED, DEFAULT_ANIMATIONS_DISABLED);
32+
}
33+
34+
/**
35+
* Asserts a value passed in is actually an animation type and not something else
36+
*/
37+
export function assertAnimationTypes(value: string | Function, instruction: string) {
38+
if (value == null || (typeof value !== 'string' && typeof value !== 'function')) {
39+
throw new RuntimeError(
40+
RuntimeErrorCode.ANIMATE_INVALID_VALUE,
41+
`'${instruction}' value must be a string of CSS classes or an animation function, got ${stringify(value)}`,
42+
);
43+
}
44+
}
45+
46+
/**
47+
* Asserts a given native element is an actual Element node and not something like a comment node.
48+
*/
49+
export function assertElementNodes(nativeElement: Element, instruction: string) {
50+
if ((nativeElement as Node).nodeType !== Node.ELEMENT_NODE) {
51+
throw new RuntimeError(
52+
RuntimeErrorCode.ANIMATE_INVALID_VALUE,
53+
`'${instruction}' can only be used on an element node, got ${stringify((nativeElement as Node).nodeType)}`,
54+
);
55+
}
56+
}
57+
58+
/**
59+
* trackEnterClasses is necessary in the case of composition where animate.enter
60+
* is used on the same element in multiple places, like on the element and in a
61+
* host binding. When removing classes, we need the entire list of animation classes
62+
* added to properly remove them when the longest animation fires.
63+
*/
64+
export function trackEnterClasses(el: HTMLElement, classList: string[], cleanupFns: Function[]) {
65+
const elementData = enterClassMap.get(el);
66+
if (elementData) {
67+
for (const klass of classList) {
68+
elementData.classList.push(klass);
69+
}
70+
for (const fn of cleanupFns) {
71+
elementData.cleanupFns.push(fn);
72+
}
73+
} else {
74+
enterClassMap.set(el, {classList, cleanupFns});
75+
}
76+
}
77+
78+
/**
79+
* Helper function to cleanup enterClassMap data safely
80+
*/
81+
export function cleanupEnterClassData(element: HTMLElement): void {
82+
const elementData = enterClassMap.get(element);
83+
if (elementData) {
84+
for (const fn of elementData.cleanupFns) {
85+
fn();
86+
}
87+
enterClassMap.delete(element);
88+
}
89+
longestAnimations.delete(element);
90+
}
91+
92+
export const noOpAnimationComplete = () => {};
93+
94+
// Tracks the list of classes added to a DOM node from `animate.enter` calls to ensure
95+
// we remove all of the classes in the case of animation composition via host bindings.
96+
export const enterClassMap = new WeakMap<
97+
HTMLElement,
98+
{classList: string[]; cleanupFns: Function[]}
99+
>();
100+
export const longestAnimations = new WeakMap<HTMLElement, LongestAnimation>();
101+
102+
// Tracks nodes that are animating away for the duration of the animation. This is
103+
// used to prevent duplicate nodes from showing up when nodes have been toggled quickly
104+
// from an `@if` or `@for`.
105+
export const leavingNodes = new WeakMap<TNode, HTMLElement[]>();
106+
107+
/**
108+
* This actually removes the leaving HTML Element in the TNode
109+
*/
110+
export function clearLeavingNodes(tNode: TNode, el: HTMLElement): void {
111+
const nodes = leavingNodes.get(tNode);
112+
if (nodes && nodes.length > 0) {
113+
const ix = nodes.findIndex((node) => node === el);
114+
if (ix > -1) nodes.splice(ix, 1);
115+
}
116+
if (nodes?.length === 0) {
117+
leavingNodes.delete(tNode);
118+
}
119+
}
120+
121+
/**
122+
* In the case that we have an existing node that's animating away, like when
123+
* an `@if` toggles quickly, we need to end the animation for the former node
124+
* and remove it right away to prevent duplicate nodes showing up.
125+
*/
126+
export function cancelLeavingNodes(tNode: TNode, lView: LView): void {
127+
const leavingEl = leavingNodes.get(tNode)?.shift();
128+
const lContainer = lView[DECLARATION_LCONTAINER];
129+
if (lContainer) {
130+
// this is the insertion point for the new TNode element.
131+
// it will be inserted before the declaring containers anchor.
132+
const beforeNode = getBeforeNodeForView(tNode.index, lContainer);
133+
// here we need to check the previous sibling of that anchor. The first
134+
// previousSibling node will be the new element added. The second
135+
// previousSibling will be the one that's being removed.
136+
const previousNode = beforeNode?.previousSibling;
137+
// We really only want to cancel animations if the leaving node is the
138+
// same as the node before where the new node will be inserted. This is
139+
// the control flow scenario where an if was toggled.
140+
if (leavingEl && previousNode && leavingEl === previousNode) {
141+
leavingEl.dispatchEvent(new CustomEvent('animationend', {detail: {cancel: true}}));
142+
}
143+
}
144+
}
145+
146+
/**
147+
* Tracks the nodes list of nodes that are leaving the DOM so we can cancel any leave animations
148+
* and remove the node before adding a new entering instance of the DOM node. This prevents
149+
* duplicates from showing up on screen mid-animation.
150+
*/
151+
export function trackLeavingNodes(tNode: TNode, el: HTMLElement): void {
152+
// We need to track this tNode's element just to be sure we don't add
153+
// a new RNode for this TNode while this one is still animating away.
154+
// once the animation is complete, we remove this reference.
155+
if (leavingNodes.has(tNode)) {
156+
leavingNodes.get(tNode)?.push(el);
157+
} else {
158+
leavingNodes.set(tNode, [el]);
159+
}
160+
}
161+
162+
/**
163+
* Retrieves the list of specified enter animations from the lView
164+
*/
165+
export function getLViewEnterAnimations(lView: LView): Function[] {
166+
const animationData = (lView[ANIMATIONS] ??= {});
167+
return (animationData.enter ??= []);
168+
}
169+
170+
/**
171+
* Retrieves the list of specified leave animations from the lView
172+
*/
173+
export function getLViewLeaveAnimations(lView: LView): Function[] {
174+
const animationData = (lView[ANIMATIONS] ??= {});
175+
return (animationData.leave ??= []);
176+
}
177+
178+
/**
179+
* Gets the list of classes from a passed in value
180+
*/
181+
export function getClassListFromValue(value: string | Function | string[]): string[] | null {
182+
const classes = typeof value === 'function' ? value() : value;
183+
let classList: string[] | null = Array.isArray(classes) ? classes : null;
184+
if (typeof classes === 'string') {
185+
classList = classes
186+
.trim()
187+
.split(/\s+/)
188+
.filter((k) => k);
189+
}
190+
return classList;
191+
}
192+
193+
/**
194+
* Cancels any running enter animations on a given element to prevent them from interfering
195+
* with leave animations.
196+
*/
197+
export function cancelAnimationsIfRunning(element: HTMLElement, renderer: Renderer): void {
198+
if (!areAnimationSupported) return;
199+
const elementData = enterClassMap.get(element);
200+
if (
201+
elementData &&
202+
elementData.classList.length > 0 &&
203+
elementHasClassList(element, elementData.classList)
204+
) {
205+
for (const klass of elementData.classList) {
206+
renderer.removeClass(element as unknown as RElement, klass);
207+
}
208+
}
209+
// We need to prevent any enter animation listeners from firing if they exist.
210+
cleanupEnterClassData(element);
211+
}
212+
213+
/**
214+
* Checks if a given element contains the classes is a provided list
215+
*/
216+
export function elementHasClassList(element: HTMLElement, classList: string[]): boolean {
217+
for (const className of classList) {
218+
if (element.classList.contains(className)) return true;
219+
}
220+
return false;
221+
}
222+
223+
/**
224+
* Determines if the animation or transition event is currently the expected longest animation
225+
* based on earlier determined data in `longestAnimations`
226+
*
227+
* @param event
228+
* @param nativeElement
229+
* @returns
230+
*/
231+
export function isLongestAnimation(
232+
event: AnimationEvent | TransitionEvent,
233+
nativeElement: HTMLElement,
234+
): boolean {
235+
const longestAnimation = longestAnimations.get(nativeElement);
236+
return (
237+
nativeElement === event.target &&
238+
longestAnimation !== undefined &&
239+
((longestAnimation.animationName !== undefined &&
240+
(event as AnimationEvent).animationName === longestAnimation.animationName) ||
241+
(longestAnimation.propertyName !== undefined &&
242+
(event as TransitionEvent).propertyName === longestAnimation.propertyName))
243+
);
244+
}
245+
246+
/**
247+
* Determines if a given tNode is a content projection root node.
248+
*/
249+
export function isTNodeContentProjectionRoot(tNode: TNode): boolean {
250+
return Array.isArray(tNode.projection);
251+
}

0 commit comments

Comments
 (0)