From 4924108630dec9f30e8cf6bf69a19e9345be26c4 Mon Sep 17 00:00:00 2001 From: Jessica Janiuk Date: Thu, 28 Aug 2025 15:01:13 +0200 Subject: [PATCH] refactor(core): dispatch enter and leave animations at the right times (#63450) This updates the enter and leave logic to use the stored LView data to dispatch the enter and leave animations at the right points in the lifecycle. This should fix issues with signals not being available yet, parallel animations, and also eliminate the need for the element registry. fixes: #63391 fixes: #63388 fixes: #63369 PR Close #63450 --- .../cli-hello-world-ivy-i18n/tsconfig.json | 3 +- .../cli-hello-world-lazy/tsconfig.json | 3 +- integration/cli-hello-world/tsconfig.json | 2 +- .../cli-signal-inputs/e2e/tsconfig.json | 3 +- integration/cli-signal-inputs/tsconfig.json | 2 +- integration/defer/tsconfig.json | 3 +- .../legacy-animations-async/tsconfig.json | 3 +- integration/legacy-animations/tsconfig.json | 3 +- integration/ng-add-localize/tsconfig.json | 3 +- integration/nodenext_resolution/tsconfig.json | 8 +- .../platform-server-hydration/tsconfig.json | 3 +- .../platform-server-zoneless/tsconfig.json | 3 +- integration/platform-server/e2e/tsconfig.json | 3 +- integration/platform-server/tsconfig.json | 3 +- .../standalone-bootstrap/tsconfig.json | 3 +- integration/trusted-types/tsconfig.json | 3 +- integration/typings_test_rxjs7/tsconfig.json | 8 +- integration/typings_test_ts59/tsconfig.json | 6 +- .../animate_leave_with_binding_template.js | 2 +- ...leave_with_event_host_bindings_template.js | 4 +- ...mate_leave_with_event_listener_template.js | 2 +- ...eave_with_string_host_bindings_template.js | 4 +- .../animate_leave_with_string_template.js | 2 +- .../compiler/src/render3/r3_identifiers.ts | 5 - .../compiler/src/render3/view/compiler.ts | 7 - .../src/template_parser/animation_analyzer.ts | 54 ---- .../src/animation/element_removal_registry.ts | 133 -------- packages/core/src/animation/interfaces.ts | 19 +- .../core/src/animation/longest_animation.ts | 9 +- packages/core/src/core_private_export.ts | 5 +- .../core/src/core_render3_private_export.ts | 2 - .../render3/features/animations_feature.ts | 22 -- packages/core/src/render3/index.ts | 2 - .../src/render3/instructions/animation.ts | 287 +++++++++--------- .../render3/instructions/change_detection.ts | 11 +- packages/core/src/render3/interfaces/view.ts | 12 +- packages/core/src/render3/jit/environment.ts | 1 - .../core/src/render3/node_manipulation.ts | 70 ++++- packages/core/src/render3/state.ts | 18 -- packages/core/test/acceptance/BUILD.bazel | 1 + .../core/test/acceptance/animation_spec.ts | 78 ++--- .../test/acceptance/authoring/BUILD.bazel | 2 + .../authoring/signal_inputs_spec.ts | 76 ++++- .../test/acceptance/renderer_factory_spec.ts | 3 - .../core/test/animation_utils/BUILD.bazel | 14 + .../animation_utils/tick_animation_frames.ts | 14 + .../bundle.golden_symbols.json | 10 +- .../bundling/defer/bundle.golden_symbols.json | 10 +- .../forms_reactive/bundle.golden_symbols.json | 10 +- .../bundle.golden_symbols.json | 10 +- .../hydration/bundle.golden_symbols.json | 10 +- .../router/bundle.golden_symbols.json | 10 +- .../bundle.golden_symbols.json | 10 +- .../core/test/render3/imported_renderer2.ts | 3 - .../platform-browser/src/dom/dom_renderer.ts | 87 +----- .../test/full_app_hydration_spec.ts | 6 +- 56 files changed, 452 insertions(+), 638 deletions(-) delete mode 100644 packages/compiler/src/template_parser/animation_analyzer.ts delete mode 100644 packages/core/src/animation/element_removal_registry.ts delete mode 100644 packages/core/src/render3/features/animations_feature.ts create mode 100644 packages/core/test/animation_utils/BUILD.bazel create mode 100644 packages/core/test/animation_utils/tick_animation_frames.ts 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 () => {