diff --git a/integration/cli-hello-world-ivy-i18n/tsconfig.json b/integration/cli-hello-world-ivy-i18n/tsconfig.json index 3210a9ea425..f65a7b74fa7 100644 --- a/integration/cli-hello-world-ivy-i18n/tsconfig.json +++ b/integration/cli-hello-world-ivy-i18n/tsconfig.json @@ -16,7 +16,8 @@ ], "lib": [ "es2018", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/cli-hello-world-lazy/tsconfig.json b/integration/cli-hello-world-lazy/tsconfig.json index 3210a9ea425..f65a7b74fa7 100644 --- a/integration/cli-hello-world-lazy/tsconfig.json +++ b/integration/cli-hello-world-lazy/tsconfig.json @@ -16,7 +16,8 @@ ], "lib": [ "es2018", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/cli-hello-world/tsconfig.json b/integration/cli-hello-world/tsconfig.json index 6d01924436c..f121aa0b7db 100644 --- a/integration/cli-hello-world/tsconfig.json +++ b/integration/cli-hello-world/tsconfig.json @@ -12,7 +12,7 @@ "importHelpers": true, "target": "es2022", "typeRoots": ["node_modules/@types"], - "lib": ["es2018", "dom"] + "lib": ["es2018", "dom", "ES2020.Promise"] }, "angularCompilerOptions": { "strictTemplates": true diff --git a/integration/cli-signal-inputs/e2e/tsconfig.json b/integration/cli-signal-inputs/e2e/tsconfig.json index 39b800f7896..31e439f4c8b 100644 --- a/integration/cli-signal-inputs/e2e/tsconfig.json +++ b/integration/cli-signal-inputs/e2e/tsconfig.json @@ -7,7 +7,8 @@ "types": [ "jasmine", "jasminewd2", - "node" + "node", + "ES2020.Promise" ] } } diff --git a/integration/cli-signal-inputs/tsconfig.json b/integration/cli-signal-inputs/tsconfig.json index d07f2a62715..02b1283ec3c 100644 --- a/integration/cli-signal-inputs/tsconfig.json +++ b/integration/cli-signal-inputs/tsconfig.json @@ -13,7 +13,7 @@ "importHelpers": true, "target": "es2022", "typeRoots": ["node_modules/@types"], - "lib": ["es2018", "dom"] + "lib": ["es2018", "dom", "ES2020.Promise"] }, "angularCompilerOptions": { "strictTemplates": true diff --git a/integration/defer/tsconfig.json b/integration/defer/tsconfig.json index 3210a9ea425..f65a7b74fa7 100644 --- a/integration/defer/tsconfig.json +++ b/integration/defer/tsconfig.json @@ -16,7 +16,8 @@ ], "lib": [ "es2018", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/legacy-animations-async/tsconfig.json b/integration/legacy-animations-async/tsconfig.json index 195ad48ae3c..9aedef64ef5 100644 --- a/integration/legacy-animations-async/tsconfig.json +++ b/integration/legacy-animations-async/tsconfig.json @@ -17,7 +17,8 @@ ], "lib": [ "es2018", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/legacy-animations/tsconfig.json b/integration/legacy-animations/tsconfig.json index 3210a9ea425..f65a7b74fa7 100644 --- a/integration/legacy-animations/tsconfig.json +++ b/integration/legacy-animations/tsconfig.json @@ -16,7 +16,8 @@ ], "lib": [ "es2018", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/ng-add-localize/tsconfig.json b/integration/ng-add-localize/tsconfig.json index dd149a77926..a58d891dd44 100644 --- a/integration/ng-add-localize/tsconfig.json +++ b/integration/ng-add-localize/tsconfig.json @@ -20,7 +20,8 @@ "module": "es2022", "lib": [ "es2018", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/nodenext_resolution/tsconfig.json b/integration/nodenext_resolution/tsconfig.json index 38fa5ee4ceb..2aac49e8af2 100644 --- a/integration/nodenext_resolution/tsconfig.json +++ b/integration/nodenext_resolution/tsconfig.json @@ -9,13 +9,11 @@ "moduleResolution": "NodeNext", "outDir": "./dist/out-tsc", "rootDir": ".", - "target": "es5", + "target": "es2020", "lib": [ - "es5", + "es2020", "dom", - "es2015.collection", - "es2015.iterable", - "es2015.promise" + "es2020.promise" ], "types": [], }, diff --git a/integration/platform-server-hydration/tsconfig.json b/integration/platform-server-hydration/tsconfig.json index 56b644d6a6e..3b55593959c 100644 --- a/integration/platform-server-hydration/tsconfig.json +++ b/integration/platform-server-hydration/tsconfig.json @@ -20,7 +20,8 @@ "useDefineForClassFields": false, "lib": [ "ES2022", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/platform-server-zoneless/tsconfig.json b/integration/platform-server-zoneless/tsconfig.json index 239a3521c41..04cfac83da5 100644 --- a/integration/platform-server-zoneless/tsconfig.json +++ b/integration/platform-server-zoneless/tsconfig.json @@ -21,7 +21,8 @@ "useDefineForClassFields": false, "lib": [ "ES2022", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/platform-server/e2e/tsconfig.json b/integration/platform-server/e2e/tsconfig.json index 9fccc3e5081..4e7a98aef5b 100644 --- a/integration/platform-server/e2e/tsconfig.json +++ b/integration/platform-server/e2e/tsconfig.json @@ -7,7 +7,8 @@ "target": "es2019", "types": [ "jasmine", - "node" + "node", + "ES2020.Promise" ] } } diff --git a/integration/platform-server/tsconfig.json b/integration/platform-server/tsconfig.json index 239a3521c41..04cfac83da5 100644 --- a/integration/platform-server/tsconfig.json +++ b/integration/platform-server/tsconfig.json @@ -21,7 +21,8 @@ "useDefineForClassFields": false, "lib": [ "ES2022", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/standalone-bootstrap/tsconfig.json b/integration/standalone-bootstrap/tsconfig.json index 3210a9ea425..f65a7b74fa7 100644 --- a/integration/standalone-bootstrap/tsconfig.json +++ b/integration/standalone-bootstrap/tsconfig.json @@ -16,7 +16,8 @@ ], "lib": [ "es2018", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/trusted-types/tsconfig.json b/integration/trusted-types/tsconfig.json index f431b70173f..bb879bfb4f0 100644 --- a/integration/trusted-types/tsconfig.json +++ b/integration/trusted-types/tsconfig.json @@ -18,7 +18,8 @@ "module": "es2022", "lib": [ "es2018", - "dom" + "dom", + "ES2020.Promise" ] }, "angularCompilerOptions": { diff --git a/integration/typings_test_rxjs7/tsconfig.json b/integration/typings_test_rxjs7/tsconfig.json index 30e25c22097..4b747a3f321 100644 --- a/integration/typings_test_rxjs7/tsconfig.json +++ b/integration/typings_test_rxjs7/tsconfig.json @@ -9,13 +9,11 @@ "moduleResolution": "node", "outDir": "./dist/out-tsc", "rootDir": ".", - "target": "es5", + "target": "es2020", "lib": [ - "es5", + "ES2020", "dom", - "es2015.collection", - "es2015.iterable", - "es2015.promise" + "ES2020.promise" ], "types": [], }, diff --git a/integration/typings_test_ts59/tsconfig.json b/integration/typings_test_ts59/tsconfig.json index 9305a8c949c..5457fed9ab0 100644 --- a/integration/typings_test_ts59/tsconfig.json +++ b/integration/typings_test_ts59/tsconfig.json @@ -11,11 +11,9 @@ "rootDir": ".", "target": "ES2020", "lib": [ - "es5", + "ES2020", "dom", - "es2015.collection", - "es2015.iterable", - "es2015.promise" + "ES2020.promise" ], "types": [], }, diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_binding_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_binding_template.js index 30fd616820b..a237e7a244b 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_binding_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_binding_template.js @@ -1,4 +1,4 @@ -MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], features: [i0.ɵɵAnimationsFeature()], decls: 3, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { +MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 3, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵdomElementStart(0, "div")(1, "p"); i0.ɵɵanimateLeave(function MyComponent_Template_animateleave_cb() { return ctx.leaveClass(); }); i0.ɵɵtext(2, "Fading Content"); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_host_bindings_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_host_bindings_template.js index 984f4b4c130..c41aa3a4e94 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_host_bindings_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_host_bindings_template.js @@ -1,12 +1,12 @@ ChildComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ChildComponent, selectors: [["child-component"]], hostBindings: function ChildComponent_HostBindings(rf, ctx) { if (rf & 1) { i0.ɵɵanimateLeaveListener(function ChildComponent_animateleave_HostBindingHandler($event) { return ctx.fadeFn($event); }); - } }, features: [i0.ɵɵAnimationsFeature()], decls: 2, vars: 0, template: function ChildComponent_Template(rf, ctx) { if (rf & 1) { + } }, decls: 2, vars: 0, template: function ChildComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵdomElementStart(0, "p"); i0.ɵɵtext(1, "Fading Content"); i0.ɵɵdomElementEnd(); } }, encapsulation: 2 }); … -MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], features: [i0.ɵɵAnimationsFeature()], decls: 1, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { +MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 1, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelementStart(0, "child-component"); i0.ɵɵanimateLeave("slide"); i0.ɵɵelementEnd(); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_listener_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_listener_template.js index ed38375c178..1f4678afa82 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_listener_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_listener_template.js @@ -1,4 +1,4 @@ -MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], features: [i0.ɵɵAnimationsFeature()], decls: 3, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { +MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 3, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵdomElementStart(0, "div")(1, "p"); i0.ɵɵanimateLeaveListener(function MyComponent_Template_p_animateleave_1_listener($event) { return ctx.fadeFn($event); }); i0.ɵɵtext(2, "Fading Content"); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_host_bindings_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_host_bindings_template.js index e19ca0e2692..f14a82a9fef 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_host_bindings_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_host_bindings_template.js @@ -1,12 +1,12 @@ ChildComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ChildComponent, selectors: [["child-component"]], hostBindings: function ChildComponent_HostBindings(rf, ctx) { if (rf & 1) { i0.ɵɵanimateLeave("fade"); - } }, features: [i0.ɵɵAnimationsFeature()], decls: 2, vars: 0, template: function ChildComponent_Template(rf, ctx) { if (rf & 1) { + } }, decls: 2, vars: 0, template: function ChildComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵdomElementStart(0, "p"); i0.ɵɵtext(1, "Fading Content"); i0.ɵɵdomElementEnd(); } }, encapsulation: 2 }); … -MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], features: [i0.ɵɵAnimationsFeature()], decls: 1, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { +MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 1, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelementStart(0, "child-component"); i0.ɵɵanimateLeave("slide"); i0.ɵɵelementEnd(); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_template.js index 1060c653cc4..7a810bc80f4 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_template.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_template.js @@ -1,4 +1,4 @@ -MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], features: [i0.ɵɵAnimationsFeature()], decls: 3, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { +MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 3, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { i0.ɵɵdomElementStart(0, "div")(1, "p"); i0.ɵɵanimateLeave("fade"); i0.ɵɵtext(2, "Fading Content"); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 44186e2939f..3c48d64de48 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -447,11 +447,6 @@ export class Identifiers { moduleName: CORE, }; - static AnimationsFeature: o.ExternalReference = { - name: 'ɵɵAnimationsFeature', - moduleName: CORE, - }; - static listener: o.ExternalReference = {name: 'ɵɵlistener', moduleName: CORE}; static getInheritedFactory: o.ExternalReference = { diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 7047e037419..4b3ab6c3124 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -32,7 +32,6 @@ import {getTemplateSourceLocationsEnabled} from './config'; import {createContentQueriesFunction, createViewQueriesFunction} from './query_generation'; import {makeBindingParser} from './template'; import {asLiteral, conditionallyCreateDirectiveBindingLiteral, DefinitionMap} from './util'; -import {analyzeTemplateForAnimations} from '../../template_parser/animation_analyzer'; const COMPONENT_VARIABLE = '%COMP%'; const HOST_ATTR = `_nghost-${COMPONENT_VARIABLE}`; @@ -158,12 +157,6 @@ function addFeatures( o.importExpr(R3.ExternalStylesFeature).callFn([o.literalArr(externalStyleNodes)]), ); } - const template = (meta as R3ComponentMetadata).template; - if (hasAnimationHostBinding(meta) || (template && template.nodes.length > 0)) { - if (hasAnimationHostBinding(meta) || analyzeTemplateForAnimations(template.nodes)) { - features.push(o.importExpr(R3.AnimationsFeature).callFn([])); - } - } if (features.length) { definitionMap.set('features', o.literalArr(features)); diff --git a/packages/compiler/src/template_parser/animation_analyzer.ts b/packages/compiler/src/template_parser/animation_analyzer.ts deleted file mode 100644 index d8ace70a6e9..00000000000 --- a/packages/compiler/src/template_parser/animation_analyzer.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*! - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {CombinedRecursiveAstVisitor} from '../combined_visitor'; -import {Node, Element, visitAll} from '../render3/r3_ast'; -const ANIMATE_LEAVE = `animate.leave`; - -/** - * Analyzes a component's template to determine if it's using animate.enter - * or animate.leave syntax. - */ -export function analyzeTemplateForAnimations(template: Node[]): boolean { - const analyzer = new AnimationsAnalyzer(); - visitAll(analyzer, template); - - // The template is considered selectorless only if there - // are direct references to directives or pipes. - return analyzer.hasAnimations; -} - -/** - * Visitor that traverses all the template nodes and - * expressions to look for selectorless references. - */ -class AnimationsAnalyzer extends CombinedRecursiveAstVisitor { - hasAnimations: boolean = false; - - override visitElement(element: Element): void { - // check for regular strings - for (const attr of element.attributes) { - if (attr.name === ANIMATE_LEAVE) { - this.hasAnimations = true; - } - } - // check for attribute bindings - for (const input of element.inputs) { - if (input.name === ANIMATE_LEAVE) { - this.hasAnimations = true; - } - } - // check for event bindings - for (const output of element.outputs) { - if (output.name === ANIMATE_LEAVE) { - this.hasAnimations = true; - } - } - super.visitElement(element); - } -} diff --git a/packages/core/src/animation/element_removal_registry.ts b/packages/core/src/animation/element_removal_registry.ts deleted file mode 100644 index 82056f0121a..00000000000 --- a/packages/core/src/animation/element_removal_registry.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import { - AnimationClassFunction, - AnimationDetails, - AnimationEventFunction, - AnimationFunction, -} from './interfaces'; - -export interface AnimationRemovalRegistry { - elements: ElementRegistry | undefined; -} - -/** - * Registers elements for delayed removal action for animation in the case - * that `animate.leave` is used. This stores the target element and any - * classes, class resolvers, and callback functions that may be needed - * to apply the removal animation, and then stashes the actual element - * removal function from the dom renderer to be called after the - * animation is finished. - */ -export class ElementRegistry { - private outElements = new WeakMap(); - - remove(el: Element): void { - this.outElements.delete(el); - } - - /** Used when animate.leave is only applying classes */ - trackClasses(details: AnimationDetails, classes: string | string[]): void { - const classList = getClassListFromValue(classes); - if (!classList) return; - for (let klass of classList) { - details.classes?.add(klass); - } - } - - /** Used when animate.leave is applying classes via a bound attribute - * which requires resolving the binding function at the right time - * to get the proper class list. There may be multiple resolvers due - * to composition via host bindings. - */ - trackResolver(details: AnimationDetails, resolver: Function): void { - if (!details.classFns) { - details.classFns = [resolver]; - } else { - details.classFns.push(resolver); - } - } - - /** Used when `animate.leave` is using the function signature and will have a - * callback function, rather than a list of classes. - */ - addCallback( - el: Element, - value: AnimationFunction, - animateWrapperFn: AnimationEventFunction, - ): void { - const details = this.outElements.get(el) ?? { - classes: null, - animateFn: () => {}, - isEventBinding: true, - }; - details.animateFn = animateWrapperFn(el, value); - this.outElements.set(el, details); - } - - /** Used when `animate.leave` is using classes. */ - add(el: Element, value: string | string[] | Function, animateWrapperFn: AnimationClassFunction) { - const details = this.outElements.get(el) ?? { - classes: new Set(), - animateFn: (): void => {}, - isEventBinding: false, - }; - if (typeof value === 'function') { - this.trackResolver(details, value); - } else { - this.trackClasses(details, value); - } - details.animateFn = animateWrapperFn(el, details.classes, details.classFns); - this.outElements.set(el, details); - } - - has(el: Element): boolean { - return this.outElements.has(el); - } - - /** This is called by the dom renderer to actually initiate the animation - * using the animateFn stored in the registry. The DOM renderer passes in - * the removal function to be fired off when the animation finishes. - */ - animate(el: Element, removeFn: Function, maxAnimationTimeout: number): void { - if (!this.outElements.has(el)) return removeFn(); - const details = this.outElements.get(el)!; - let timeoutId: ReturnType; - let called = false; - const remove = () => { - // This called check is to prevent a rare race condition where the timing of removal - // might result in the removal function being called twice. - if (called) return; - called = true; - clearTimeout(timeoutId); - this.remove(el); - removeFn(); - }; - // this timeout is used to ensure elements actually get removed in the case - // that the user forgot to call the remove callback. The timeout is cleared - // in the DOM renderer during the remove child process. It only applies - // to the event binding use case. - if (details.isEventBinding) { - timeoutId = setTimeout(remove, maxAnimationTimeout); - } - details.animateFn(remove); - } -} - -export function getClassListFromValue(value: string | Function | string[]): string[] | null { - const classes = typeof value === 'function' ? value() : value; - let classList: string[] | null = Array.isArray(classes) ? classes : null; - if (typeof classes === 'string') { - classList = classes - .trim() - .split(/\s+/) - .filter((k) => k); - } - return classList; -} diff --git a/packages/core/src/animation/interfaces.ts b/packages/core/src/animation/interfaces.ts index 268ab62af18..aac45817dd0 100644 --- a/packages/core/src/animation/interfaces.ts +++ b/packages/core/src/animation/interfaces.ts @@ -69,7 +69,20 @@ export interface AnimationDetails { animateFn: AnimationRemoveFunction; isEventBinding: boolean; } -export interface LeaveAnimation { - index: number; - animateFn: Function; + +export interface LongestAnimation { + animationName: string | undefined; + propertyName: string | undefined; + duration: number; +} + +export interface AnimationLViewData { + // Enter animations that apply to nodes in this view + enter?: Function[]; + + // Leave animations that apply to nodes in this view + leave?: (() => Promise)[]; + + // Leave animations that apply to nodes in this view + running?: Promise[]>; } diff --git a/packages/core/src/animation/longest_animation.ts b/packages/core/src/animation/longest_animation.ts index 366e0a47c93..f86aa06ff82 100644 --- a/packages/core/src/animation/longest_animation.ts +++ b/packages/core/src/animation/longest_animation.ts @@ -6,11 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -export interface LongestAnimation { - animationName: string | undefined; - propertyName: string | undefined; - duration: number; -} +import {LView} from '../render3/interfaces/view'; +import {LongestAnimation} from './interfaces'; /** Parses a CSS time value to milliseconds. */ function parseCssTimeUnitsToMs(value: string): number { @@ -148,3 +145,5 @@ function determineLongestAnimationFromElementAnimations( animationsMap.set(el, longest); } } + +export const allLeavingAnimations = new Set(); diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 39996ca4434..047712137ab 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -173,8 +173,5 @@ export {getComponentDef as ɵgetComponentDef} from './render3/def_getters'; export {DEHYDRATED_BLOCK_REGISTRY as ɵDEHYDRATED_BLOCK_REGISTRY} from './defer/registry'; export {TimerScheduler as ɵTimerScheduler} from './defer/timer_scheduler'; export {ɵassertType} from './type_checking'; -export { - ElementRegistry as ɵElementRegistry, - AnimationRemovalRegistry as ɵAnimationRemovalRegistry, -} from './animation/element_removal_registry'; export {ANIMATIONS_DISABLED as ɵANIMATIONS_DISABLED} from './animation/interfaces'; +export {allLeavingAnimations as ɵallLeavingAnimations} from './animation/longest_animation'; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 01288903245..632c083511c 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -152,7 +152,6 @@ export { ɵɵproperty, ɵɵProvidersFeature, ɵɵHostDirectivesFeature, - ɵɵAnimationsFeature, ɵɵpureFunction0, ɵɵpureFunction1, ɵɵpureFunction2, @@ -305,4 +304,3 @@ export {depsTracker as ɵdepsTracker} from './render3/deps_tracker/deps_tracker' export {generateStandaloneInDeclarationsError as ɵgenerateStandaloneInDeclarationsError} from './render3/jit/module'; export {getAsyncClassMetadataFn as ɵgetAsyncClassMetadataFn} from './render3/metadata'; export {DeferBlockData as ɵDeferBlockData} from './render3/util/defer'; -export {getAnimationElementRemovalRegistry as ɵgetAnimationElementRemovalRegistry} from './render3/state'; diff --git a/packages/core/src/render3/features/animations_feature.ts b/packages/core/src/render3/features/animations_feature.ts deleted file mode 100644 index 677be6ae5ef..00000000000 --- a/packages/core/src/render3/features/animations_feature.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {ElementRegistry} from '../../animation/element_removal_registry'; -import {setAnimationElementRemovalRegistry} from '../state'; - -/** - * This feature adds the element registry for delayed element removal when animate.leave - * is utilized. - * - * @codeGenApi - */ -export function ɵɵAnimationsFeature() { - return () => { - setAnimationElementRemovalRegistry(new ElementRegistry()); - }; -} diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index c495759a2db..b70f994f147 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -12,7 +12,6 @@ import {ɵɵInheritDefinitionFeature} from './features/inherit_definition_featur import {ɵɵNgOnChangesFeature} from './features/ng_onchanges_feature'; import {ɵɵProvidersFeature} from './features/providers_feature'; import {ɵɵExternalStylesFeature} from './features/external_styles_feature'; -import {ɵɵAnimationsFeature} from './features/animations_feature'; import { ComponentDef, ComponentTemplate, @@ -249,5 +248,4 @@ export { ɵɵsetComponentScope, ɵɵsetNgModuleScope, ɵɵExternalStylesFeature, - ɵɵAnimationsFeature, }; diff --git a/packages/core/src/render3/instructions/animation.ts b/packages/core/src/render3/instructions/animation.ts index 9c19323f098..982ad72cd7a 100644 --- a/packages/core/src/render3/instructions/animation.ts +++ b/packages/core/src/render3/instructions/animation.ts @@ -9,39 +9,30 @@ import {stringify} from '../../util/stringify'; // Adjust imports as per actual location import { AnimationCallbackEvent, - AnimationClassFunction, - AnimationEventFunction, AnimationFunction, - AnimationRemoveFunction, ANIMATIONS_DISABLED, + MAX_ANIMATION_TIMEOUT, + LongestAnimation, } from '../../animation/interfaces'; -import { - AnimationRemovalRegistry, - getClassListFromValue, -} from '../../animation/element_removal_registry'; -import {getLView, getCurrentTNode, getTView, getAnimationElementRemovalRegistry} from '../state'; +import {getLView, getCurrentTNode} from '../state'; import { RENDERER, INJECTOR, CONTEXT, - FLAGS, - LViewFlags, LView, - TView, DECLARATION_LCONTAINER, - ENTER_ANIMATIONS, - LEAVE_ANIMATIONS, + ANIMATIONS, } from '../interfaces/view'; import {RuntimeError, RuntimeErrorCode} from '../../errors'; -import {getNativeByTNode, storeCleanupWithContext} from '../util/view_utils'; +import {getNativeByTNode} from '../util/view_utils'; import {performanceMarkFeature} from '../../util/performance'; import {Renderer} from '../interfaces/renderer'; import {RElement} from '../interfaces/renderer_dom'; import {NgZone} from '../../zone'; -import {assertDefined} from '../../util/assert'; -import {determineLongestAnimation, LongestAnimation} from '../../animation/longest_animation'; +import {determineLongestAnimation, allLeavingAnimations} from '../../animation/longest_animation'; import {TNode} from '../interfaces/node'; import {getBeforeNodeForView} from '../node_manipulation'; +import type {PromiseConstructor} from '../../util/promise_with_resolvers'; const DEFAULT_ANIMATIONS_DISABLED = false; const areAnimationSupported = @@ -58,22 +49,6 @@ function areAnimationsDisabled(lView: LView): boolean { return injector.get(ANIMATIONS_DISABLED, DEFAULT_ANIMATIONS_DISABLED); } -/** - * Helper function to setup element registry cleanup when LView is destroyed - */ -function setupElementRegistryCleanup( - elementRegistry: AnimationRemovalRegistry, - lView: LView, - tView: TView, - nativeElement: Element, -): void { - if (lView[FLAGS] & LViewFlags.FirstLViewPass) { - storeCleanupWithContext(tView, lView, nativeElement, (elToClean: Element) => { - elementRegistry.elements!.remove(elToClean); - }); - } -} - /** * Helper function to cleanup enterClassMap data safely */ @@ -148,18 +123,26 @@ function trackLeavingNodes(tNode: TNode, el: HTMLElement): void { } function getLViewEnterAnimations(lView: LView): Function[] { - if (lView[ENTER_ANIMATIONS] === null) { - lView[ENTER_ANIMATIONS] = []; - } - return lView[ENTER_ANIMATIONS]; + const animationData = (lView[ANIMATIONS] ??= {}); + return (animationData.enter ??= []); } -// function getLViewLeaveAnimations(lView: LView): Function[] { -// if (lView[LEAVE_ANIMATIONS] === null) { -// lView[LEAVE_ANIMATIONS] = []; -// } -// return lView[LEAVE_ANIMATIONS]; -// } +function getLViewLeaveAnimations(lView: LView): Function[] { + const animationData = (lView[ANIMATIONS] ??= {}); + return (animationData.leave ??= []); +} + +function getClassListFromValue(value: string | Function | string[]): string[] | null { + const classes = typeof value === 'function' ? value() : value; + let classList: string[] | null = Array.isArray(classes) ? classes : null; + if (typeof classes === 'string') { + classList = classes + .trim() + .split(/\s+/) + .filter((k) => k); + } + return classList; +} /** * Instruction to handle the `animate.enter` behavior for class bindings. @@ -295,21 +278,25 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa cancelLeavingNodes(tNode, lView); - getLViewEnterAnimations(lView).push(() => { - const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement; - - ngDevMode && assertElementNodes(nativeElement, 'animate.enter'); - - value.call(lView[CONTEXT], {target: nativeElement, animationComplete: noOpAnimationComplete}); - }); + getLViewEnterAnimations(lView).push(() => runEnterAnimationFunction(lView, tNode, value)); return ɵɵanimateEnterListener; } +/** + * runs enter animations when a custom function is provided + */ +function runEnterAnimationFunction(lView: LView, tNode: TNode, value: AnimationFunction) { + const nativeElement = getNativeByTNode(tNode, lView) as HTMLElement; + ngDevMode && assertElementNodes(nativeElement, 'animate.enter'); + + value.call(lView[CONTEXT], {target: nativeElement, animationComplete: noOpAnimationComplete}); +} + /** * Instruction to handle the `animate.leave` behavior for class animations. - * It registers an animation with the ElementRegistry to be run when the element - * is scheduled for removal from the DOM. + * It creates a leave animation function that's tracked in the LView to + * be run before DOM node removal and cleanup. * * @param value The value bound to `animate.leave`, which can be a string or a function. * @returns This function returns itself so that it may be chained. @@ -331,56 +318,53 @@ export function ɵɵanimateLeave(value: string | Function): typeof ɵɵanimateLe return ɵɵanimateLeave; } - const tView = getTView(); const tNode = getCurrentTNode()!; + + getLViewLeaveAnimations(lView).push(() => + runLeaveAnimations(lView, tNode, value, animationsDisabled), + ); + + return ɵɵanimateLeave; // For chaining +} + +function runLeaveAnimations( + lView: LView, + tNode: TNode, + value: string | Function, + animationsDisabled: boolean, +): Promise { + const {promise, resolve} = (Promise as unknown as PromiseConstructor).withResolvers(); const nativeElement = getNativeByTNode(tNode, lView) as Element; ngDevMode && assertElementNodes(nativeElement, 'animate.leave'); - // This instruction is called in the update pass. const renderer = lView[RENDERER]; - const elementRegistry = getAnimationElementRemovalRegistry(); - ngDevMode && - assertDefined( - elementRegistry.elements, - 'Expected `ElementRegistry` to be present in animations subsystem', + const ngZone = lView[INJECTOR].get(NgZone); + allLeavingAnimations.add(lView); + + const activeClasses = getClassListFromValue(value); + if (activeClasses && activeClasses.length > 0) { + animateLeaveClassRunner( + nativeElement as HTMLElement, + tNode, + activeClasses, + renderer, + animationsDisabled, + ngZone, + resolve, ); - const ngZone = lView[INJECTOR]!.get(NgZone); + } else { + resolve(); + } - // This function gets stashed in the registry to be used once the element removal process - // begins. We pass in the values and resolvers so as to evaluate the resolved classes - // at the latest possible time, meaning we evaluate them right before the animation - // begins. - const animate: AnimationClassFunction = ( - el: Element, - value: Set | null, - resolvers: Function[] | undefined, - ): AnimationRemoveFunction => { - return (removalFn: VoidFunction) => { - animateLeaveClassRunner( - el as HTMLElement, - tNode, - getClassList(value, resolvers), - removalFn, - renderer, - animationsDisabled, - ngZone, - ); - }; - }; - - // Ensure cleanup if the LView is destroyed before the animation runs. - setupElementRegistryCleanup(elementRegistry, lView, tView, nativeElement); - elementRegistry.elements!.add(nativeElement, value, animate); - - return ɵɵanimateLeave; // For chaining + return promise; } /** * Instruction to handle the `(animate.leave)` behavior for event bindings, aka when * a user wants to use a custom animation function rather than a class. It registers - * an animation with the ElementRegistry to be run when the element is scheduled for - * removal from the DOM. + * a leave animation function in the LView to be run at right before removal from the + * DOM. * * @param value The value bound to `(animate.leave)`, an AnimationFunction. * @returns This function returns itself so that it may be chained. @@ -402,72 +386,64 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa const lView = getLView(); const tNode = getCurrentTNode()!; - const tView = getTView(); - const nativeElement = getNativeByTNode(tNode, lView) as Element; + allLeavingAnimations.add(lView); - ngDevMode && assertElementNodes(nativeElement, 'animate.leave'); - - const elementRegistry = getAnimationElementRemovalRegistry(); - ngDevMode && - assertDefined( - elementRegistry.elements, - 'Expected `ElementRegistry` to be present in animations subsystem', - ); - - const renderer = lView[RENDERER]; - const animationsDisabled = areAnimationsDisabled(lView); - const ngZone = lView[INJECTOR]!.get(NgZone); - - const animate: AnimationEventFunction = ( - _el: Element, - value: AnimationFunction, - ): AnimationRemoveFunction => { - return (removeFn: VoidFunction): void => { - if (animationsDisabled) { - removeFn(); - } else { - const event: AnimationCallbackEvent = { - target: nativeElement, - animationComplete: () => { - clearLeavingNodes(tNode, _el as HTMLElement); - removeFn(); - }, - }; - trackLeavingNodes(tNode, _el as HTMLElement); - - ngZone.runOutsideAngular(() => { - renderer.listen(_el, 'animationend', () => removeFn(), {once: true}); - }); - value.call(lView[CONTEXT], event); - } - }; - }; - - // Ensure cleanup if the LView is destroyed before the animation runs. - setupElementRegistryCleanup(elementRegistry, lView, tView, nativeElement); - elementRegistry.elements!.addCallback(nativeElement, value, animate); + getLViewLeaveAnimations(lView).push(() => runLeaveAnimationFunction(lView, tNode, value)); return ɵɵanimateLeaveListener; // For chaining } /** - * Builds the list of classes to apply to an element based on either the passed in list of strings - * or the set of resolver functions that are coming from bindings. Those resolver functions should - * resolve into either a string or a string array. There may be multiple to support composition. + * runs leave animations when a custom function is provided */ -function getClassList(value: Set | null, resolvers: Function[] | undefined): Set { - const classList = new Set(value); - if (resolvers && resolvers.length) { - for (const resolverFn of resolvers) { - const resolvedValue = getClassListFromValue(resolverFn); - if (resolvedValue) { - for (const rv of resolvedValue) { - classList.add(rv); - } - } - } +function runLeaveAnimationFunction( + lView: LView, + tNode: TNode, + value: AnimationFunction, +): Promise { + const {promise, resolve} = (Promise as unknown as PromiseConstructor).withResolvers(); + const nativeElement = getNativeByTNode(tNode, lView) as Element; + + ngDevMode && assertElementNodes(nativeElement, 'animate.leave'); + + const renderer = lView[RENDERER]; + const animationsDisabled = areAnimationsDisabled(lView); + const ngZone = lView[INJECTOR]!.get(NgZone); + const maxAnimationTimeout = lView[INJECTOR]!.get(MAX_ANIMATION_TIMEOUT); + + if (animationsDisabled) { + resolve(); + } else { + const timeoutId = setTimeout(() => { + clearLeavingNodes(tNode, nativeElement as HTMLElement); + resolve(); + }, maxAnimationTimeout); + + const event: AnimationCallbackEvent = { + target: nativeElement, + animationComplete: () => { + clearLeavingNodes(tNode, nativeElement as HTMLElement); + clearTimeout(timeoutId); + resolve(); + }, + }; + trackLeavingNodes(tNode, nativeElement as HTMLElement); + + ngZone.runOutsideAngular(() => { + renderer.listen( + nativeElement, + 'animationend', + () => { + resolve(); + }, + {once: true}, + ); + }); + value.call(lView[CONTEXT], event); } - return classList; + + // Ensure cleanup if the LView is destroyed before the animation runs. + return promise; } function cancelAnimationsIfRunning(element: HTMLElement, renderer: Renderer): void { @@ -553,15 +529,15 @@ function assertElementNodes(nativeElement: Element, instruction: string) { function animateLeaveClassRunner( el: HTMLElement, tNode: TNode, - classList: Set, - finalRemoveFn: VoidFunction, + classList: string[], renderer: Renderer, animationsDisabled: boolean, ngZone: NgZone, + resolver: VoidFunction, ) { if (animationsDisabled) { longestAnimations.delete(el); - finalRemoveFn(); + resolver(); return; } @@ -576,8 +552,17 @@ function animateLeaveClassRunner( event.stopImmediatePropagation(); longestAnimations.delete(el); clearLeavingNodes(tNode, el); - finalRemoveFn(); + + if (Array.isArray(tNode.projection)) { + // in the content projection case, the element is not destroyed. + // So we need to remove the class at the end so that it isn't left + // behind for whenever the item shows up again. + for (const item of classList) { + renderer.removeClass(el, item); + } + } } + resolver(); }; ngZone.runOutsideAngular(() => { @@ -596,7 +581,7 @@ function animateLeaveClassRunner( determineLongestAnimation(el, longestAnimations, areAnimationSupported); if (!longestAnimations.has(el)) { clearLeavingNodes(tNode, el); - finalRemoveFn(); + resolver(); } }); }); diff --git a/packages/core/src/render3/instructions/change_detection.ts b/packages/core/src/render3/instructions/change_detection.ts index a1bb5b1a30d..53941e04ce3 100644 --- a/packages/core/src/render3/instructions/change_detection.ts +++ b/packages/core/src/render3/instructions/change_detection.ts @@ -25,7 +25,7 @@ import {ComponentTemplate, HostBindingsFunction, RenderFlags} from '../interface import { CONTEXT, EFFECTS_TO_SCHEDULE, - ENTER_ANIMATIONS, + ANIMATIONS, ENVIRONMENT, FLAGS, InitPhaseState, @@ -232,6 +232,7 @@ export function refreshView( if (templateFn !== null) { executeTemplate(tView, lView, templateFn, RenderFlags.Update, context); } + runEnterAnimations(lView); const hooksInitPhaseCompleted = (flags & LViewFlags.InitPhaseStateMask) === InitPhaseState.InitPhaseCompleted; @@ -261,7 +262,6 @@ export function refreshView( // `LView` but its declaration appears after the insertion component. markTransplantedViewsForRefresh(lView); } - runEnterAnimations(lView); runEffectsInView(lView); detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Global); @@ -375,11 +375,12 @@ export function refreshView( } function runEnterAnimations(lView: LView) { - if (lView[ENTER_ANIMATIONS]) { - for (let animateFn of lView[ENTER_ANIMATIONS]) { + const animationData = lView[ANIMATIONS]; + if (animationData?.enter) { + for (const animateFn of animationData.enter) { animateFn(); } - lView[ENTER_ANIMATIONS] = null; + animationData.enter = undefined; } } diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index 267c3a77ef0..43fc50d39ed 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {LeaveAnimation} from '../../animation/interfaces'; +import {AnimationLViewData} from '../../animation/interfaces'; import type {ChangeDetectionScheduler} from '../../change_detection/scheduling/zoneless_scheduling'; import {TDeferBlockDetails} from '../../defer/interfaces'; import type {Injector} from '../../di/injector'; @@ -70,8 +70,7 @@ export const EFFECTS_TO_SCHEDULE = 22; export const EFFECTS = 23; export const REACTIVE_TEMPLATE_CONSUMER = 24; export const AFTER_RENDER_SEQUENCES_TO_ADD = 25; -export const ENTER_ANIMATIONS = 26; -export const LEAVE_ANIMATIONS = 27; +export const ANIMATIONS = 26; /** * Size of LView's header. Necessary to adjust for it when setting slots. @@ -80,7 +79,7 @@ export const LEAVE_ANIMATIONS = 27; * instruction index into `LView` index. All other indexes should be in the `LView` index space and * there should be no need to refer to `HEADER_OFFSET` anywhere else. */ -export const HEADER_OFFSET = 28; +export const HEADER_OFFSET = 27; // This interface replaces the real LView interface if it is an arg or a // return value of a public instruction. This ensures we don't need to expose @@ -372,10 +371,7 @@ export interface LView extends Array { [AFTER_RENDER_SEQUENCES_TO_ADD]: AfterRenderSequence[] | null; // Enter animations that apply to nodes in this view - [ENTER_ANIMATIONS]: Function[] | null; - - // Leave animations that apply to nodes in this view - [LEAVE_ANIMATIONS]: Set | null; + [ANIMATIONS]: AnimationLViewData | null; } /** diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 6c111215a01..7de24cb545a 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -45,7 +45,6 @@ export const angularCoreEnv: {[name: string]: unknown} = (() => ({ 'ɵɵCopyDefinitionFeature': r3.ɵɵCopyDefinitionFeature, 'ɵɵInheritDefinitionFeature': r3.ɵɵInheritDefinitionFeature, 'ɵɵExternalStylesFeature': r3.ɵɵExternalStylesFeature, - 'ɵɵAnimationsFeature': r3.ɵɵAnimationsFeature, 'ɵɵnextContext': r3.ɵɵnextContext, 'ɵɵnamespaceHTML': r3.ɵɵnamespaceHTML, 'ɵɵnamespaceMathML': r3.ɵɵnamespaceMathML, diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index e505e3e8d91..e9bf0178a76 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -62,6 +62,7 @@ import { HookData, HookFn, HOST, + ANIMATIONS, LView, LViewFlags, NEXT, @@ -79,6 +80,7 @@ import {assertTNodeType} from './node_assert'; import {profiler} from './profiler'; import {ProfilerEvent} from './profiler_types'; import {getLViewParent, getNativeByTNode, unwrapRNode} from './util/view_utils'; +import {allLeavingAnimations} from '../animation/longest_animation'; const enum WalkTNodeTreeAction { /** node create in the native environment. Run on initial creation. */ @@ -107,6 +109,7 @@ function applyToElementOrContainer( parent: RElement | null, lNodeToHandle: RNode | LContainer | LView, beforeNode?: RNode | null, + parentLView?: LView, ) { // If this slot was allocated for a text node dynamically created by i18n, the text node itself // won't be created until i18nApply() in the update block, so this node should be skipped. @@ -136,9 +139,13 @@ function applyToElementOrContainer( } else if (action === WalkTNodeTreeAction.Insert && parent !== null) { nativeInsertBefore(renderer, parent, rNode, beforeNode || null, true); } else if (action === WalkTNodeTreeAction.Detach) { - nativeRemoveNode(renderer, rNode, isComponent); + runLeaveAnimationsWithCallback(parentLView, () => { + nativeRemoveNode(renderer, rNode, isComponent); + }); } else if (action === WalkTNodeTreeAction.Destroy) { - renderer.destroyNode!(rNode); + runLeaveAnimationsWithCallback(parentLView, () => { + renderer.destroyNode!(rNode); + }); } if (lContainer != null) { applyContainer(renderer, action, lContainer, parent, beforeNode); @@ -278,13 +285,11 @@ export function destroyLView(tView: TView, lView: LView) { if (isDestroyed(lView)) { return; } - const renderer = lView[RENDERER]; if (renderer.destroyNode) { applyView(tView, lView, renderer, WalkTNodeTreeAction.Destroy, null, null); } - destroyViewTree(lView); } @@ -345,6 +350,32 @@ function cleanUpView(tView: TView, lView: LView): void { } } +function runLeaveAnimationsWithCallback(lView: LView | undefined, callback: Function) { + if (lView && lView[ANIMATIONS] && lView[ANIMATIONS].leave) { + const runningAnimations = []; + for (let animateFn of lView[ANIMATIONS].leave) { + runningAnimations.push(animateFn()); + } + lView[ANIMATIONS].running = Promise.allSettled(runningAnimations); + lView[ANIMATIONS].leave = undefined; + } + runAfterLeaveAnimations(lView, callback); +} + +function runAfterLeaveAnimations(lView: LView | undefined, callback: Function) { + if (lView && lView[ANIMATIONS] && lView[ANIMATIONS].running) { + lView[ANIMATIONS].running.then(() => { + if (lView[ANIMATIONS] && lView[ANIMATIONS].running) { + lView[ANIMATIONS].running = undefined; + } + allLeavingAnimations.delete(lView); + callback(); + }); + return; + } + callback(); +} + /** Removes listeners and unsubscribes from output subscriptions */ function processCleanups(tView: TView, lView: LView): void { ngDevMode && assertNotReactive(processCleanups.name); @@ -755,14 +786,28 @@ function applyNodes( if (!isDetachedByI18n(tNode)) { if (tNodeType & TNodeType.ElementContainer) { applyNodes(renderer, action, tNode.child, lView, parentRElement, beforeNode, false); - applyToElementOrContainer(action, renderer, parentRElement, rawSlotValue, beforeNode); + applyToElementOrContainer( + action, + renderer, + parentRElement, + rawSlotValue, + beforeNode, + lView, + ); } else if (tNodeType & TNodeType.Icu) { const nextRNode = icuContainerIterate(tNode as TIcuContainerNode, lView); let rNode: RNode | null; while ((rNode = nextRNode())) { - applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode); + applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode, lView); } - applyToElementOrContainer(action, renderer, parentRElement, rawSlotValue, beforeNode); + applyToElementOrContainer( + action, + renderer, + parentRElement, + rawSlotValue, + beforeNode, + lView, + ); } else if (tNodeType & TNodeType.Projection) { applyProjectionRecursive( renderer, @@ -774,7 +819,14 @@ function applyNodes( ); } else { ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode | TNodeType.Container); - applyToElementOrContainer(action, renderer, parentRElement, rawSlotValue, beforeNode); + applyToElementOrContainer( + action, + renderer, + parentRElement, + rawSlotValue, + beforeNode, + lView, + ); } } tNode = isProjection ? tNode.projectionNext : tNode.next; @@ -891,7 +943,7 @@ function applyProjectionRecursive( // This should be refactored and cleaned up. for (let i = 0; i < nodeToProjectOrRNodes.length; i++) { const rNode = nodeToProjectOrRNodes[i]; - applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode); + applyToElementOrContainer(action, renderer, parentRElement, rNode, beforeNode, lView); } } else { let nodeToProject: TNode | null = nodeToProjectOrRNodes; diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index 946627c6d66..0731b607a64 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import {AnimationRemovalRegistry, ElementRegistry} from '../animation/element_removal_registry'; import {InternalInjectFlags} from '../di/interface/injector'; import { assertDefined, @@ -842,20 +841,3 @@ export function wasLastNodeCreated(): boolean { export function lastNodeWasCreated(flag: boolean): void { _wasLastNodeCreated = flag; } - -/** - * We create an object here because it's possible the DOM Renderer is created - * before the animation removal registry is defined. The object allows us to - * update the instance once the registry is created. - */ -let registry: AnimationRemovalRegistry = {elements: undefined}; - -export function setAnimationElementRemovalRegistry(value: ElementRegistry) { - if (registry.elements === undefined) { - registry.elements = value; - } -} - -export function getAnimationElementRemovalRegistry(): AnimationRemovalRegistry { - return registry; -} diff --git a/packages/core/test/acceptance/BUILD.bazel b/packages/core/test/acceptance/BUILD.bazel index 4dcf73be275..ad0e1f8aee0 100644 --- a/packages/core/test/acceptance/BUILD.bazel +++ b/packages/core/test/acceptance/BUILD.bazel @@ -27,6 +27,7 @@ ts_project( "//packages/core/primitives/signals", "//packages/core/src/di/interface", "//packages/core/src/util", + "//packages/core/test/animation_utils", "//packages/core/test/render3:matchers", "//packages/core/testing", "//packages/localize", diff --git a/packages/core/test/acceptance/animation_spec.ts b/packages/core/test/acceptance/animation_spec.ts index 9ea8c3e47b6..e7b3c129608 100644 --- a/packages/core/test/acceptance/animation_spec.ts +++ b/packages/core/test/acceptance/animation_spec.ts @@ -13,19 +13,22 @@ import { Component, Directive, ElementRef, - input, - InputSignal, + NgModule, + provideZonelessChangeDetection, signal, ViewChild, } from '@angular/core'; import {fakeAsync, TestBed, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {isNode} from '@angular/private/testing'; +import {tickAnimationFrames} from '../animation_utils/tick_animation_frames'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {BrowserTestingModule, platformBrowserTesting} from '@angular/platform-browser/testing'; -/** Ticks the specified amount of `requestAnimationFrame`-s. */ -export function tickAnimationFrames(amount: number) { - tick(16.6 * amount); // Angular turns rAF calls into 16.6ms timeouts in tests. -} +@NgModule({ + providers: [provideZonelessChangeDetection()], +}) +export class TestModule {} describe('Animation', () => { if (isNode) { @@ -33,6 +36,19 @@ describe('Animation', () => { return; } + beforeAll(() => { + TestBed.resetTestEnvironment(); + TestBed.initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting()); + }); + + afterAll(() => { + TestBed.resetTestEnvironment(); + TestBed.initTestEnvironment( + [BrowserTestingModule, NoopAnimationsModule, TestModule], + platformBrowserTesting(), + ); + }); + describe('animate.leave', () => { const styles = ` .fade { @@ -83,6 +99,7 @@ describe('Animation', () => { paragragh.nativeElement.dispatchEvent( new AnimationEvent('animationend', {animationName: 'fade-out'}), ); + tick(); expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"'); expect(logSpy).toHaveBeenCalled(); })); @@ -191,6 +208,7 @@ describe('Animation', () => { paragragh.nativeElement.dispatchEvent( new AnimationEvent('animationend', {animationName: 'slide-out'}), ); + tick(); expect(fixture.nativeElement.outerHTML).not.toContain('class="slide-out fade"'); })); @@ -251,6 +269,7 @@ describe('Animation', () => { paragragh.nativeElement.dispatchEvent( new AnimationEvent('animationend', {animationName: 'slide-out'}), ); + tick(); expect(fixture.nativeElement.outerHTML).not.toContain('class="slide-out fade"'); })); @@ -312,6 +331,7 @@ describe('Animation', () => { paragragh.nativeElement.dispatchEvent( new AnimationEvent('animationend', {animationName: 'slide-out'}), ); + tick(); expect(fixture.nativeElement.outerHTML).not.toContain('class="slide-out fade"'); })); @@ -379,6 +399,7 @@ describe('Animation', () => { fadeCmp.nativeElement.dispatchEvent( new AnimationEvent('animationend', {animationName: 'fade-out'}), ); + tick(); expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"'); })); @@ -421,6 +442,7 @@ describe('Animation', () => { fadeCmp.nativeElement.dispatchEvent( new AnimationEvent('animationend', {animationName: 'fade-out'}), ); + tick(); expect(fixture.nativeElement.outerHTML).not.toContain('class="fade"'); })); @@ -493,12 +515,12 @@ describe('Animation', () => { @Component({ selector: 'child-cmp', styles: multiple, - host: {'[animate.leave]': 'clazz'}, + host: {'[animate.leave]': 'slide()'}, template: '

I should fade

', encapsulation: ViewEncapsulation.None, }) class ChildComponent { - clazz = 'slide-out'; + slide = signal('slide-out'); } @Component({ @@ -525,10 +547,8 @@ describe('Animation', () => { fixture.detectChanges(); tickAnimationFrames(1); expect(cmp.show()).toBeFalsy(); - fixture.detectChanges(); expect(childCmp.nativeElement.className).toContain('fade'); expect(childCmp.nativeElement.className).toContain('slide-out'); - fixture.detectChanges(); childCmp.nativeElement.dispatchEvent( new AnimationEvent('animationend', {animationName: 'fade-out'}), @@ -536,6 +556,7 @@ describe('Animation', () => { childCmp.nativeElement.dispatchEvent( new AnimationEvent('animationend', {animationName: 'slide-out'}), ); + tick(); expect(fixture.nativeElement.outerHTML).not.toContain('fade'); expect(fixture.nativeElement.outerHTML).not.toContain('slide-out'); @@ -612,7 +633,7 @@ describe('Animation', () => { childCmp.nativeElement.dispatchEvent( new AnimationEvent('animationend', {animationName: 'slide-out'}), ); - + tick(); expect(fixture.nativeElement.outerHTML).not.toContain('slide-out'); expect(fixture.debugElement.query(By.css('child-cmp'))).toBeNull(); })); @@ -1245,41 +1266,6 @@ describe('Animation', () => { expect(childCmp.nativeElement.className).not.toContain('slide-in fade-in'); })); - xit('should support signal inputs', fakeAsync(() => { - @Component({ - selector: 'child-cmp', - styles: styles, - template: '

I should fade

', - encapsulation: ViewEncapsulation.None, - }) - class ChildComponent { - public enterAnim: InputSignal = input.required(); - } - - @Component({ - selector: 'test-cmp', - styles: styles, - imports: [ChildComponent], - template: '', - encapsulation: ViewEncapsulation.None, - }) - class TestComponent {} - TestBed.configureTestingModule({animationsEnabled: true}); - - const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); - tickAnimationFrames(1); - const childCmp = fixture.debugElement.query(By.css('child-cmp')); - - expect(childCmp.nativeElement.className).toContain('fade-in'); - childCmp.nativeElement.dispatchEvent(new AnimationEvent('animationstart')); - childCmp.nativeElement.dispatchEvent( - new AnimationEvent('animationend', {animationName: 'fade-in'}), - ); - fixture.detectChanges(); - expect(childCmp.nativeElement.className).not.toContain('fade-in'); - })); - it('should reset leave animation and not duplicate node when toggled quickly', fakeAsync(() => { const animateStyles = ` .slide-in { diff --git a/packages/core/test/acceptance/authoring/BUILD.bazel b/packages/core/test/acceptance/authoring/BUILD.bazel index dbc0a12c577..0d52482b5fc 100644 --- a/packages/core/test/acceptance/authoring/BUILD.bazel +++ b/packages/core/test/acceptance/authoring/BUILD.bazel @@ -14,6 +14,8 @@ TEST_DEPS = [ "//packages/platform-browser:platform-browser", "//packages/core/primitives/signals:signals", "//:node_modules/rxjs", + "//packages/core/test/animation_utils", + "//packages/private/testing", ] ts_project( diff --git a/packages/core/test/acceptance/authoring/signal_inputs_spec.ts b/packages/core/test/acceptance/authoring/signal_inputs_spec.ts index 8fdeac09726..4289674be9f 100644 --- a/packages/core/test/acceptance/authoring/signal_inputs_spec.ts +++ b/packages/core/test/acceptance/authoring/signal_inputs_spec.ts @@ -17,7 +17,11 @@ import { ViewChild, } from '@angular/core'; import {SIGNAL} from '../../../primitives/signals'; -import {TestBed} from '../../../testing'; +import {fakeAsync, TestBed, tick} from '../../../testing'; +import {ViewEncapsulation} from '@angular/compiler'; +import {By} from '@angular/platform-browser'; +import {tickAnimationFrames} from '../../animation_utils/tick_animation_frames'; +import {isNode} from '@angular/private/testing'; describe('signal inputs', () => { beforeEach(() => @@ -314,4 +318,74 @@ describe('signal inputs', () => { expect(host.dir.value[SIGNAL].debugName).toBe('TEST_DEBUG_NAME'); }); + + describe('animation API', () => { + if (isNode) { + it('should pass', () => expect(true).toBe(true)); + return; + } + + it('should support signal inputs', fakeAsync(() => { + const styles = ` + .slide-in { + animation: slide-in 1ms; + } + .fade-in { + animation: fade-in 2ms; + } + @keyframes slide-in { + from { + transform: translateX(-10px); + } + to { + transform: translateX(0); + } + } + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + `; + @Component({ + selector: 'child-cmp', + styles: styles, + template: '

I should fade

', + encapsulation: ViewEncapsulation.None, + }) + class ChildComponent { + enterAnim = input.required(); + } + + @Component({ + selector: 'test-cmp', + styles: styles, + imports: [ChildComponent], + template: '', + encapsulation: ViewEncapsulation.None, + }) + class TestComponent { + fade = 'fade-in'; + } + + TestBed.configureTestingModule({animationsEnabled: true}); + + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + tickAnimationFrames(1); + const childCmp = fixture.debugElement.query(By.css('p')); + + expect(childCmp.nativeElement.className).toContain('fade-in'); + childCmp.nativeElement.dispatchEvent(new AnimationEvent('animationstart')); + childCmp.nativeElement.dispatchEvent( + new AnimationEvent('animationend', {animationName: 'fade-in'}), + ); + fixture.detectChanges(); + tick(); + expect(childCmp.nativeElement.className).not.toContain('fade-in'); + })); + }); }); diff --git a/packages/core/test/acceptance/renderer_factory_spec.ts b/packages/core/test/acceptance/renderer_factory_spec.ts index 9d5f88b1a51..4a2a071119c 100644 --- a/packages/core/test/acceptance/renderer_factory_spec.ts +++ b/packages/core/test/acceptance/renderer_factory_spec.ts @@ -400,9 +400,6 @@ function getRendererFactory2(document: Document): RendererFactory2 { isNode ? PLATFORM_SERVER_ID : PLATFORM_BROWSER_ID, fakeNgZone, null, - true, - 0, - null, ); const origCreateRenderer = rendererFactory.createRenderer; rendererFactory.createRenderer = function (element: any, type: RendererType2 | null) { diff --git a/packages/core/test/animation_utils/BUILD.bazel b/packages/core/test/animation_utils/BUILD.bazel new file mode 100644 index 00000000000..6415cc1a32d --- /dev/null +++ b/packages/core/test/animation_utils/BUILD.bazel @@ -0,0 +1,14 @@ +load("//tools:defaults.bzl", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "animation_utils", + testonly = True, + srcs = [ + "tick_animation_frames.ts", + ], + deps = [ + "//packages/core/testing", + ], +) diff --git a/packages/core/test/animation_utils/tick_animation_frames.ts b/packages/core/test/animation_utils/tick_animation_frames.ts new file mode 100644 index 00000000000..f50c55888fc --- /dev/null +++ b/packages/core/test/animation_utils/tick_animation_frames.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {tick} from '@angular/core/testing'; + +/** Ticks the specified amount of `requestAnimationFrame`-s. */ +export function tickAnimationFrames(amount: number) { + tick(16.6 * amount); // Angular turns rAF calls into 16.6ms timeouts in tests. +} diff --git a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json index 652fdff1666..44b3c8562fa 100644 --- a/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animations-standalone/bundle.golden_symbols.json @@ -2,7 +2,7 @@ "chunks": { "main": [ "AFTER_RENDER_SEQUENCES_TO_ADD", - "ANIMATIONS_DISABLED", + "ANIMATIONS", "ANIMATION_MODULE_TYPE", "ANIMATION_PREFIX", "ANY_STATE", @@ -141,8 +141,6 @@ "MATH_ML_NAMESPACE", "MAXIMUM_REFRESH_RERUNS", "MAXIMUM_REFRESH_RERUNS", - "MAX_ANIMATION_TIMEOUT", - "MAX_ANIMATION_TIMEOUT_DEFAULT", "MODIFIER_KEYS", "MODIFIER_KEY_GETTERS", "MONKEY_PATCH_KEY_NAME", @@ -403,6 +401,7 @@ "addClass", "addServerStyles", "addToEndOfViewTree", + "allLeavingAnimations", "allocExpando", "allocLFrame", "allowPreviousPlayerStylesMerge", @@ -562,7 +561,6 @@ "forwardRef", "freeConsumers", "getActiveConsumer", - "getAnimationElementRemovalRegistry", "getBaseElementHref", "getBindingsEnabled", "getBodyNode", @@ -836,7 +834,6 @@ "registerLView", "registerPostOrderHooks", "registerPreOrderHooks", - "registry", "relativePath", "rememberChangeHistoryAndInvokeOnChangesHook", "remove", @@ -861,8 +858,11 @@ "resolveTimingValue", "retrieveHydrationInfo", "roundOffset", + "runAfterLeaveAnimations", "runEffectsInView", + "runEnterAnimations", "runInInjectionContext", + "runLeaveAnimationsWithCallback", "runPlatformInitializers", "saveNameToExportMap", "saveResolvedLocalsInData", diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json index fa9aa09bf8c..ac65d6d46de 100644 --- a/packages/core/test/bundling/defer/bundle.golden_symbols.json +++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json @@ -68,7 +68,7 @@ ], "lazy": [ "AFTER_RENDER_SEQUENCES_TO_ADD", - "ANIMATIONS_DISABLED", + "ANIMATIONS", "APP_BOOTSTRAP_LISTENER", "APP_ID", "APP_INITIALIZER", @@ -161,8 +161,6 @@ "MATH_ML_NAMESPACE", "MAXIMUM_REFRESH_RERUNS", "MAXIMUM_REFRESH_RERUNS", - "MAX_ANIMATION_TIMEOUT", - "MAX_ANIMATION_TIMEOUT_DEFAULT", "MINIMUM_SLOT", "MONKEY_PATCH_KEY_NAME", "MOVED_VIEWS", @@ -348,6 +346,7 @@ "addToArray", "addToEndOfViewTree", "addViewToDOM", + "allLeavingAnimations", "allocExpando", "allocLFrame", "angularZoneInstanceIdProperty", @@ -480,7 +479,6 @@ "forwardRef", "freeConsumers", "getActiveConsumer", - "getAnimationElementRemovalRegistry", "getBeforeNodeForView", "getBindingsEnabled", "getCleanupFnKeyByType", @@ -709,7 +707,6 @@ "registerLView", "registerPostOrderHooks", "registerPreOrderHooks", - "registry", "rememberChangeHistoryAndInvokeOnChangesHook", "remove", "removeFromArray", @@ -729,8 +726,11 @@ "resolveDirectives", "resolveForwardRef", "retrieveHydrationInfo", + "runAfterLeaveAnimations", "runEffectsInView", + "runEnterAnimations", "runInInjectionContext", + "runLeaveAnimationsWithCallback", "runPlatformInitializers", "saveNameToExportMap", "saveResolvedLocalsInData", diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index 7ddc37ba14e..71cf4510b7a 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -2,7 +2,7 @@ "chunks": { "main": [ "AFTER_RENDER_SEQUENCES_TO_ADD", - "ANIMATIONS_DISABLED", + "ANIMATIONS", "APP_BOOTSTRAP_LISTENER", "APP_ID", "APP_ID_ATTRIBUTE_NAME", @@ -142,8 +142,6 @@ "MATH_ML_NAMESPACE", "MAXIMUM_REFRESH_RERUNS", "MAXIMUM_REFRESH_RERUNS", - "MAX_ANIMATION_TIMEOUT", - "MAX_ANIMATION_TIMEOUT_DEFAULT", "MODIFIER_KEYS", "MODIFIER_KEY_GETTERS", "MONKEY_PATCH_KEY_NAME", @@ -438,6 +436,7 @@ "addToEndOfViewTree", "addValidators", "addViewToDOM", + "allLeavingAnimations", "allocExpando", "allocLFrame", "angularZoneInstanceIdProperty", @@ -630,7 +629,6 @@ "fromPromise", "fromReadableStreamLike", "getActiveConsumer", - "getAnimationElementRemovalRegistry", "getBaseElementHref", "getBeforeNodeForView", "getBindingsEnabled", @@ -966,7 +964,6 @@ "registerOnValidatorChange", "registerPostOrderHooks", "registerPreOrderHooks", - "registry", "relativePath", "rememberChangeHistoryAndInvokeOnChangesHook", "remove", @@ -991,8 +988,11 @@ "resolveForwardRef", "resolveProvider", "retrieveHydrationInfo", + "runAfterLeaveAnimations", "runEffectsInView", + "runEnterAnimations", "runInInjectionContext", + "runLeaveAnimationsWithCallback", "runPlatformInitializers", "runPostProducerCreatedFn", "saveNameToExportMap", diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index 5ea74abd1f0..97f421fb450 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -2,7 +2,7 @@ "chunks": { "main": [ "AFTER_RENDER_SEQUENCES_TO_ADD", - "ANIMATIONS_DISABLED", + "ANIMATIONS", "APP_BOOTSTRAP_LISTENER", "APP_ID", "APP_ID_ATTRIBUTE_NAME", @@ -136,8 +136,6 @@ "MATH_ML_NAMESPACE", "MAXIMUM_REFRESH_RERUNS", "MAXIMUM_REFRESH_RERUNS", - "MAX_ANIMATION_TIMEOUT", - "MAX_ANIMATION_TIMEOUT_DEFAULT", "MODIFIER_KEYS", "MODIFIER_KEY_GETTERS", "MONKEY_PATCH_KEY_NAME", @@ -439,6 +437,7 @@ "addToEndOfViewTree", "addValidators", "addViewToDOM", + "allLeavingAnimations", "allocExpando", "allocLFrame", "angularZoneInstanceIdProperty", @@ -628,7 +627,6 @@ "fromPromise", "fromReadableStreamLike", "getActiveConsumer", - "getAnimationElementRemovalRegistry", "getBaseElementHref", "getBeforeNodeForView", "getBindingsEnabled", @@ -961,7 +959,6 @@ "registerOnValidatorChange", "registerPostOrderHooks", "registerPreOrderHooks", - "registry", "relativePath", "rememberChangeHistoryAndInvokeOnChangesHook", "remove", @@ -986,8 +983,11 @@ "resolvedPromise", "resolvedPromise", "retrieveHydrationInfo", + "runAfterLeaveAnimations", "runEffectsInView", + "runEnterAnimations", "runInInjectionContext", + "runLeaveAnimationsWithCallback", "runPlatformInitializers", "runPostProducerCreatedFn", "saveNameToExportMap", diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json index e2377c1a5f5..05aa030d9f5 100644 --- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json @@ -4,7 +4,7 @@ "ACCEPT_HEADER_VALUE", "AFTER_RENDER_SEQUENCES_TO_ADD", "ALLOWED_METHODS", - "ANIMATIONS_DISABLED", + "ANIMATIONS", "APP_BOOTSTRAP_LISTENER", "APP_ID", "APP_ID_ATTRIBUTE_NAME", @@ -122,8 +122,6 @@ "MATH_ML_NAMESPACE", "MAXIMUM_REFRESH_RERUNS", "MAXIMUM_REFRESH_RERUNS", - "MAX_ANIMATION_TIMEOUT", - "MAX_ANIMATION_TIMEOUT_DEFAULT", "MODIFIER_KEYS", "MODIFIER_KEY_GETTERS", "MONKEY_PATCH_KEY_NAME", @@ -345,6 +343,7 @@ "addAfterRenderSequencesForView", "addServerStyles", "addToEndOfViewTree", + "allLeavingAnimations", "allocExpando", "allocLFrame", "angularZoneInstanceIdProperty", @@ -505,7 +504,6 @@ "fromReadableStreamLike", "generateHash", "getActiveConsumer", - "getAnimationElementRemovalRegistry", "getBaseElementHref", "getClosestRElement", "getClosureSafeProperty", @@ -750,7 +748,6 @@ "registerLView", "registerPostOrderHooks", "registerPreOrderHooks", - "registry", "relativePath", "rememberChangeHistoryAndInvokeOnChangesHook", "remove", @@ -773,8 +770,11 @@ "retrieveHydrationInfo", "retrieveHydrationInfoImpl", "retrieveTransferredState", + "runAfterLeaveAnimations", "runEffectsInView", + "runEnterAnimations", "runInInjectionContext", + "runLeaveAnimationsWithCallback", "runPlatformInitializers", "saveNameToExportMap", "scheduleArray", diff --git a/packages/core/test/bundling/router/bundle.golden_symbols.json b/packages/core/test/bundling/router/bundle.golden_symbols.json index 0e650d6a2ec..dee3c0a8e26 100644 --- a/packages/core/test/bundling/router/bundle.golden_symbols.json +++ b/packages/core/test/bundling/router/bundle.golden_symbols.json @@ -3,7 +3,7 @@ "main": [ "ABSOLUTE_REDIRECT_ERROR_NAME", "AFTER_RENDER_SEQUENCES_TO_ADD", - "ANIMATIONS_DISABLED", + "ANIMATIONS", "APP_BASE_HREF", "APP_BOOTSTRAP_LISTENER", "APP_ID", @@ -148,8 +148,6 @@ "MAXIMUM_REFRESH_RERUNS", "MAXIMUM_REFRESH_RERUNS", "MAX_ALLOWED_REDIRECTS", - "MAX_ANIMATION_TIMEOUT", - "MAX_ANIMATION_TIMEOUT_DEFAULT", "MODIFIER_KEYS", "MODIFIER_KEY_GETTERS", "MONKEY_PATCH_KEY_NAME", @@ -519,6 +517,7 @@ "addViewToDOM", "advanceActivatedRoute", "afterNextNavigation", + "allLeavingAnimations", "allocExpando", "allocLFrame", "allowSanitizationBypassAndThrow", @@ -749,7 +748,6 @@ "fromReadableStreamLike", "getActiveConsumer", "getAllRouteGuards", - "getAnimationElementRemovalRegistry", "getBaseElementHref", "getBeforeNodeForView", "getBindingsEnabled", @@ -1104,7 +1102,6 @@ "registerLView", "registerPostOrderHooks", "registerPreOrderHooks", - "registry", "relativePath", "rememberChangeHistoryAndInvokeOnChangesHook", "remove", @@ -1126,6 +1123,7 @@ "resolveNode", "retrieveHydrationInfo", "rootRoute", + "runAfterLeaveAnimations", "runCanActivate", "runCanActivateChecks", "runCanActivateChild", @@ -1134,7 +1132,9 @@ "runCanLoadGuards", "runCanMatchGuards", "runEffectsInView", + "runEnterAnimations", "runInInjectionContext", + "runLeaveAnimationsWithCallback", "runPlatformInitializers", "runPostProducerCreatedFn", "runResolve", diff --git a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json index 14a76441f50..d89df4e859b 100644 --- a/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json +++ b/packages/core/test/bundling/standalone_bootstrap/bundle.golden_symbols.json @@ -2,7 +2,7 @@ "chunks": { "main": [ "AFTER_RENDER_SEQUENCES_TO_ADD", - "ANIMATIONS_DISABLED", + "ANIMATIONS", "APP_BOOTSTRAP_LISTENER", "APP_ID", "APP_ID_ATTRIBUTE_NAME", @@ -100,8 +100,6 @@ "MATH_ML_NAMESPACE", "MAXIMUM_REFRESH_RERUNS", "MAXIMUM_REFRESH_RERUNS", - "MAX_ANIMATION_TIMEOUT", - "MAX_ANIMATION_TIMEOUT_DEFAULT", "MODIFIER_KEYS", "MODIFIER_KEY_GETTERS", "MONKEY_PATCH_KEY_NAME", @@ -286,6 +284,7 @@ "addAfterRenderSequencesForView", "addServerStyles", "addToEndOfViewTree", + "allLeavingAnimations", "allocExpando", "allocLFrame", "angularZoneInstanceIdProperty", @@ -408,7 +407,6 @@ "forwardRef", "freeConsumers", "getActiveConsumer", - "getAnimationElementRemovalRegistry", "getBaseElementHref", "getClosestRElement", "getClosureSafeProperty", @@ -605,7 +603,6 @@ "registerLView", "registerPostOrderHooks", "registerPreOrderHooks", - "registry", "relativePath", "rememberChangeHistoryAndInvokeOnChangesHook", "remove", @@ -623,8 +620,11 @@ "resolveDirectives", "resolveForwardRef", "retrieveHydrationInfo", + "runAfterLeaveAnimations", "runEffectsInView", + "runEnterAnimations", "runInInjectionContext", + "runLeaveAnimationsWithCallback", "runPlatformInitializers", "saveNameToExportMap", "scheduleCallbackWithMicrotask", diff --git a/packages/core/test/render3/imported_renderer2.ts b/packages/core/test/render3/imported_renderer2.ts index be3e892d3a0..f0e02a72fe4 100644 --- a/packages/core/test/render3/imported_renderer2.ts +++ b/packages/core/test/render3/imported_renderer2.ts @@ -63,9 +63,6 @@ export function getRendererFactory2(document: any): RendererFactory2 { isNode ? PLATFORM_SERVER_ID : PLATFORM_BROWSER_ID, fakeNgZone, null, - true, - 0, - null, ); const origCreateRenderer = rendererFactory.createRenderer; rendererFactory.createRenderer = function (element: any, type: RendererType2 | null) { diff --git a/packages/platform-browser/src/dom/dom_renderer.ts b/packages/platform-browser/src/dom/dom_renderer.ts index ab1de2cdde8..54a9ddfbb7a 100644 --- a/packages/platform-browser/src/dom/dom_renderer.ts +++ b/packages/platform-browser/src/dom/dom_renderer.ts @@ -26,10 +26,7 @@ import { ɵTracingService as TracingService, ɵTracingSnapshot as TracingSnapshot, Optional, - ɵAnimationRemovalRegistry as AnimationRemovalRegistry, - ɵgetAnimationElementRemovalRegistry as getAnimationElementRemovalRegistry, - ɵANIMATIONS_DISABLED as ANIMATIONS_DISABLED, - MAX_ANIMATION_TIMEOUT, + ɵallLeavingAnimations as allLeavingAnimations, } from '@angular/core'; import {RuntimeErrorCode} from '../errors'; @@ -140,7 +137,6 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { >(); private readonly defaultRenderer: Renderer2; private readonly platformIsServer: boolean; - private registry: AnimationRemovalRegistry; constructor( private readonly eventManager: EventManager, @@ -151,8 +147,6 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { @Inject(PLATFORM_ID) readonly platformId: Object, readonly ngZone: NgZone, @Inject(CSP_NONCE) private readonly nonce: string | null = null, - @Inject(ANIMATIONS_DISABLED) private readonly animationDisabled: boolean, - @Inject(MAX_ANIMATION_TIMEOUT) private readonly maxAnimationTimeout: number, @Inject(TracingService) @Optional() private readonly tracingService: TracingService | null = null, @@ -164,8 +158,6 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { ngZone, this.platformIsServer, this.tracingService, - (this.registry = getAnimationElementRemovalRegistry()), - this.maxAnimationTimeout, ); } @@ -221,9 +213,6 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { ngZone, platformIsServer, tracingService, - this.registry, - this.animationDisabled, - this.maxAnimationTimeout, ); break; case ViewEncapsulation.ShadowDom: @@ -236,8 +225,6 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { this.nonce, platformIsServer, tracingService, - this.registry, - this.maxAnimationTimeout, sharedStylesHost, ); case ViewEncapsulation.IsolatedShadowDom: @@ -250,8 +237,6 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { this.nonce, platformIsServer, tracingService, - this.registry, - this.maxAnimationTimeout, ); default: @@ -264,9 +249,6 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy { ngZone, platformIsServer, tracingService, - this.registry, - this.animationDisabled, - this.maxAnimationTimeout, ); break; } @@ -305,8 +287,6 @@ class DefaultDomRenderer2 implements Renderer2 { protected readonly ngZone: NgZone, private readonly platformIsServer: boolean, private readonly tracingService: TracingService | null, - protected readonly registry: AnimationRemovalRegistry, - protected readonly maxAnimationTimeout: number, ) {} destroy(): void {} @@ -351,11 +331,6 @@ class DefaultDomRenderer2 implements Renderer2 { } removeChild(_parent: any, oldChild: any): void { - const {elements} = this.registry; - if (elements) { - elements.animate(oldChild, () => oldChild.remove(), this.maxAnimationTimeout); - return; - } // child was removed oldChild.remove(); } @@ -544,19 +519,9 @@ class ShadowDomRenderer extends DefaultDomRenderer2 { nonce: string | null, platformIsServer: boolean, tracingService: TracingService | null, - registry: AnimationRemovalRegistry, - maxAnimationTimeout: number, private sharedStylesHost?: SharedStylesHost, ) { - super( - eventManager, - doc, - ngZone, - platformIsServer, - tracingService, - registry, - maxAnimationTimeout, - ); + super(eventManager, doc, ngZone, platformIsServer, tracingService); this.shadowRoot = (hostEl as any).attachShadow({mode: 'open'}); // SharedStylesHost is used to add styles to the shadow root by ShadowDom. @@ -632,7 +597,6 @@ class ShadowDomRenderer extends DefaultDomRenderer2 { class NoneEncapsulationDomRenderer extends DefaultDomRenderer2 { private readonly styles: string[]; private readonly styleUrls?: string[]; - private readonly _animationDisabled: boolean; constructor( eventManager: EventManager, @@ -643,21 +607,9 @@ class NoneEncapsulationDomRenderer extends DefaultDomRenderer2 { ngZone: NgZone, platformIsServer: boolean, tracingService: TracingService | null, - registry: AnimationRemovalRegistry, - animationDisabled: boolean, - maxAnimationTimeout: number, compId?: string, ) { - super( - eventManager, - doc, - ngZone, - platformIsServer, - tracingService, - registry, - maxAnimationTimeout, - ); - this._animationDisabled = animationDisabled; + super(eventManager, doc, ngZone, platformIsServer, tracingService); let styles = component.styles; if (ngDevMode) { // We only do this in development, as for production users should not add CSS sourcemaps to components. @@ -677,32 +629,9 @@ class NoneEncapsulationDomRenderer extends DefaultDomRenderer2 { if (!this.removeStylesOnCompDestroy) { return; } - - // In the case that animate.leave animations are used, depending on - // app structure, a race condition happens with the destroy call and - // the animation being added to the element. Either the DOM node is - // immediately removed or the animate instruction is called right - // as the styles are pruned, causing the animated element to sit - // until the timeout removes it. This delays the pruning of style - // sheets for a few seconds to avoid this problem. - // - // TODO(thePunderWoman): replace this with a more targeted delay on only - // cases where we know there's a leave animation, that the leave animation - // is actually running, and to instead use the longest animation value - // for the timeout duration. - if ( - (typeof ngServerMode === 'undefined' || !ngServerMode) && - !this._animationDisabled && - this.registry.elements - ) { - this.ngZone.runOutsideAngular(() => { - setTimeout(() => { - this.sharedStylesHost.removeStyles(this.styles, this.styleUrls); - }, this.maxAnimationTimeout); - }); - return; + if (allLeavingAnimations.size === 0) { + this.sharedStylesHost.removeStyles(this.styles, this.styleUrls); } - this.sharedStylesHost.removeStyles(this.styles, this.styleUrls); } } @@ -720,9 +649,6 @@ class EmulatedEncapsulationDomRenderer2 extends NoneEncapsulationDomRenderer { ngZone: NgZone, platformIsServer: boolean, tracingService: TracingService | null, - registry: AnimationRemovalRegistry, - animationDisabled: boolean, - maxAnimationTimeout: number, ) { const compId = appId + '-' + component.id; super( @@ -734,9 +660,6 @@ class EmulatedEncapsulationDomRenderer2 extends NoneEncapsulationDomRenderer { ngZone, platformIsServer, tracingService, - registry, - animationDisabled, - maxAnimationTimeout, compId, ); this.contentAttr = shimContentAttribute(compId); diff --git a/packages/platform-server/test/full_app_hydration_spec.ts b/packages/platform-server/test/full_app_hydration_spec.ts index c3ed3132c16..9b27f14e2fe 100644 --- a/packages/platform-server/test/full_app_hydration_spec.ts +++ b/packages/platform-server/test/full_app_hydration_spec.ts @@ -2076,7 +2076,7 @@ describe('platform-server full application hydration integration', () => { const content = clientRootNode.querySelector('app-content'); expect(content.innerHTML).toBe( - 'Start Inner Start Hello World! Inner End Middle Span End', + 'Start Inner Start Hello World! Inner End Middle Span End', ); }); @@ -2130,7 +2130,7 @@ describe('platform-server full application hydration integration', () => { const content = clientRootNode.querySelector('app-content-outer'); expect(content.innerHTML).toBe( - 'Start Outer Start Span Hello World! Outer End Middle End', + 'Start Outer Start Span Hello World! Outer End Middle End', ); }); @@ -2371,7 +2371,7 @@ describe('platform-server full application hydration integration', () => { verifyClientAndSSRContentsMatch(ssrContents, clientRootNode); const div = clientRootNode.querySelector('div'); - expect(div.innerHTML).toMatch(/Some strong<\/strong> content/); + expect(div.innerHTML).toMatch(/Some strong<\/strong> content/); }); it('should support translations that remove elements', async () => {