diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts index f237bcc0968..5fe9d31d01e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts @@ -71,6 +71,8 @@ class InterpolatedSignalCheck extends TemplateCheckWithVisitor 0) { return; // See TODO above. } @@ -213,6 +212,7 @@ function createNodeFromHostLiteralProperty( ); } else if (name.text.startsWith('(') && name.text.endsWith(')')) { const events: ParsedEvent[] = []; + parser.parseEvent( name.text.slice(1, -1), initializer.text, @@ -428,7 +428,8 @@ function inferBoundAttribute(name: string): {attrName: string; type: BindingType const attrPrefix = 'attr.'; const classPrefix = 'class.'; const stylePrefix = 'style.'; - const animationPrefix = '@'; + const animationPrefix = 'animate.'; + const legacyAnimationPrefix = '@'; let attrName: string; let type: BindingType; @@ -443,7 +444,10 @@ function inferBoundAttribute(name: string): {attrName: string; type: BindingType attrName = name.slice(stylePrefix.length); type = BindingType.Style; } else if (name.startsWith(animationPrefix)) { - attrName = name.slice(animationPrefix.length); + attrName = name; + type = BindingType.Animation; + } else if (name.startsWith(legacyAnimationPrefix)) { + attrName = name.slice(legacyAnimationPrefix.length); type = BindingType.LegacyAnimation; } else { attrName = name; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index d79dc3bda0c..97894e8fc24 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -1634,6 +1634,14 @@ class TcbUnclaimedOutputsOp extends TcbOp { ? this.tcb.env.referenceExternalType('@angular/animations', 'AnimationEvent') : EventParamType.Any; + const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType); + this.scope.addStatement(ts.factory.createExpressionStatement(handler)); + } else if (output.type === ParsedEventType.Animation) { + const eventType = this.tcb.env.referenceExternalType( + '@angular/core', + 'AnimationCallbackEvent', + ); + const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType); this.scope.addStatement(ts.factory.createExpressionStatement(handler)); } else if (this.tcb.env.config.checkTypeOfDomEvents) { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 92e25eeaab7..40b94763a2e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -21,6 +21,14 @@ describe('type check blocks', () => { expect(tcb('{{hello}} {{world}}')).toContain('"" + (((this).hello)) + (((this).world));'); }); + it('should generate an animation in function call', () => { + const TEMPLATE = '

'; + const results = tcb(TEMPLATE); + expect(results).toContain( + '($event: i1.AnimationCallbackEvent): any => { (this).animateFn($event); };', + ); + }); + it('should generate literal map expressions', () => { const TEMPLATE = '{{ method({foo: a, bar: b}) }}'; expect(tcb(TEMPLATE)).toContain('(this).method({ "foo": ((this).a), "bar": ((this).b) })'); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/GOLDEN_PARTIAL.js index 2cc55be1865..07c03aec1ab 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/GOLDEN_PARTIAL.js @@ -1,83 +1,314 @@ /**************************************************************************************************** - * PARTIAL FILE: static_animation_attribute.js + * PARTIAL FILE: animate_enter_with_string.js ****************************************************************************************************/ -import { Component, NgModule } from '@angular/core'; +import { Component } from '@angular/core'; import * as i0 from "@angular/core"; -export class MyApp { +export class MyComponent { } -MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); -MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: false, selector: "my-app", ngImport: i0, template: '
', isInline: true }); -i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ +MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: ` +
+

Sliding Content

+
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ type: Component, args: [{ - selector: 'my-app', template: '
', - standalone: false + selector: 'my-component', + template: ` +
+

Sliding Content

+
+ `, }] }] }); -export class MyModule { -} -MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); -MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [MyApp] }); -MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule }); -i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{ - type: NgModule, - args: [{ declarations: [MyApp] }] - }] }); /**************************************************************************************************** - * PARTIAL FILE: static_animation_attribute.d.ts + * PARTIAL FILE: animate_enter_with_string.d.ts ****************************************************************************************************/ import * as i0 from "@angular/core"; -export declare class MyApp { - exp: any; - any: any; - static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵcmp: i0.ɵɵComponentDeclaration; -} -export declare class MyModule { - static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵmod: i0.ɵɵNgModuleDeclaration; - static ɵinj: i0.ɵɵInjectorDeclaration; +export declare class MyComponent { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; } /**************************************************************************************************** - * PARTIAL FILE: duplicate_animation_listeners.js + * PARTIAL FILE: animate_enter_with_string_host_bindings.js ****************************************************************************************************/ -import { Component, NgModule } from '@angular/core'; +import { Component } from '@angular/core'; import * as i0 from "@angular/core"; -export class MyApp { +export class ChildComponent { } -MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); -MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: false, selector: "my-app", ngImport: i0, template: '
', isInline: true }); -i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ +ChildComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ChildComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +ChildComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: ChildComponent, isStandalone: true, selector: "child-component", host: { attributes: { "animate.enter": "fade" } }, ngImport: i0, template: `

Sliding Content

`, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ChildComponent, decorators: [{ type: Component, args: [{ - selector: 'my-app', - template: '
', - standalone: false + selector: 'child-component', + host: { 'animate.enter': 'fade' }, + template: `

Sliding Content

`, }] }] }); -export class MyModule { +export class MyComponent { } -MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); -MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [MyApp] }); -MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule }); -i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{ - type: NgModule, - args: [{ declarations: [MyApp] }] +MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: ` + + `, isInline: true, dependencies: [{ kind: "component", type: ChildComponent, selector: "child-component" }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ + type: Component, + args: [{ + selector: 'my-component', + imports: [ChildComponent], + template: ` + + `, + }] }] }); /**************************************************************************************************** - * PARTIAL FILE: duplicate_animation_listeners.d.ts + * PARTIAL FILE: animate_enter_with_string_host_bindings.d.ts ****************************************************************************************************/ import * as i0 from "@angular/core"; -export declare class MyApp { - static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵcmp: i0.ɵɵComponentDeclaration; +export declare class ChildComponent { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; } -export declare class MyModule { - static ɵfac: i0.ɵɵFactoryDeclaration; - static ɵmod: i0.ɵɵNgModuleDeclaration; - static ɵinj: i0.ɵɵInjectorDeclaration; +export declare class MyComponent { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: animate_enter_with_binding.js + ****************************************************************************************************/ +import { Component, signal } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyComponent { + constructor() { + this.enterClass = signal('slide', ...(ngDevMode ? [{ debugName: "enterClass" }] : [])); + } +} +MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: ` +
+

Sliding Content

+
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ + type: Component, + args: [{ + selector: 'my-component', + template: ` +
+

Sliding Content

+
+ `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: animate_enter_with_binding.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyComponent { + enterClass: import("@angular/core").WritableSignal; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: animate_enter_with_event_listener.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyComponent { + slideFn(event) { + event.target.classList.add('slide-in'); + } +} +MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: ` +
+

Sliding Content

+
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ + type: Component, + args: [{ + selector: 'my-component', + template: ` +
+

Sliding Content

+
+ `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: animate_enter_with_event_listener.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyComponent { + slideFn(event: any): void; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: animate_leave_with_string.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyComponent { +} +MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: ` +
+

Fading Content

+
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ + type: Component, + args: [{ + selector: 'my-component', + template: ` +
+

Fading Content

+
+ `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: animate_leave_with_string.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyComponent { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: animate_leave_with_binding.js + ****************************************************************************************************/ +import { Component, signal } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyComponent { + constructor() { + this.leaveClass = signal('fade', ...(ngDevMode ? [{ debugName: "leaveClass" }] : [])); + } +} +MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: ` +
+

Fading Content

+
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ + type: Component, + args: [{ + selector: 'my-component', + template: ` +
+

Fading Content

+
+ `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: animate_leave_with_binding.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyComponent { + leaveClass: import("@angular/core").WritableSignal; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: animate_leave_with_event_listener.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyComponent { + fadeFn(event) { + event.target.classList.add('fade-out'); + } +} +MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: ` +
+

Fading Content

+
+ `, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ + type: Component, + args: [{ + selector: 'my-component', + template: ` +
+

Fading Content

+
+ `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: animate_leave_with_event_listener.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyComponent { + fadeFn(event: any): void; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: animate_leave_with_string_host_bindings.js + ****************************************************************************************************/ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class ChildComponent { +} +ChildComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ChildComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +ChildComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: ChildComponent, isStandalone: true, selector: "child-component", host: { attributes: { "animate.leave": "fade" } }, ngImport: i0, template: `

Fading Content

`, isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ChildComponent, decorators: [{ + type: Component, + args: [{ + selector: 'child-component', + template: `

Fading Content

`, + host: { 'animate.leave': 'fade' }, + }] + }] }); +export class MyComponent { +} +MyComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "my-component", ngImport: i0, template: ` + + `, isInline: true, dependencies: [{ kind: "component", type: ChildComponent, selector: "child-component" }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{ + type: Component, + args: [{ + selector: 'my-component', + imports: [ChildComponent], + template: ` + + `, + }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: animate_leave_with_string_host_bindings.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class ChildComponent { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} +export declare class MyComponent { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/TEST_CASES.json index c8ee5d619c0..ee0fe412692 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/TEST_CASES.json @@ -2,38 +2,140 @@ "$schema": "../../test_case_schema.json", "cases": [ { - "description": "should not register any @attr attributes as static attributes", + "description": "should generate animate enter instructions on element with a simple string", "inputFiles": [ - "static_animation_attribute.ts" + "animate_enter_with_string.ts" ], "expectations": [ { "files": [ { - "expected": "static_animation_attribute_template.js", - "generated": "static_animation_attribute.js" + "expected": "animate_enter_with_string_template.js", + "generated": "animate_enter_with_string.js" } ], - "failureMessage": "Incorrect initialization attributes" + "failureMessage": "Incorrect ɵɵanimateEnter() call" } ] }, { - "description": "should dedup multiple [@event] listeners", + "description": "should generate animate enter instructions with host binding and simple string", "inputFiles": [ - "duplicate_animation_listeners.ts" + "animate_enter_with_string_host_bindings.ts" ], "expectations": [ { "files": [ { - "expected": "duplicate_animation_listeners_template.js", - "generated": "duplicate_animation_listeners.js" + "expected": "animate_enter_with_string_host_bindings_template.js", + "generated": "animate_enter_with_string_host_bindings.js" } ], - "failureMessage": "Incorrect initialization attributes" + "failureMessage": "Incorrect ɵɵanimateEnter() call" + } + ] + }, + { + "description": "should generate animate enter instructions on element with a binding", + "inputFiles": [ + "animate_enter_with_binding.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "animate_enter_with_binding_template.js", + "generated": "animate_enter_with_binding.js" + } + ], + "failureMessage": "Incorrect ɵɵanimateEnter() call" + } + ] + }, + { + "description": "should generate animate enter instructions on element with an event binding", + "inputFiles": [ + "animate_enter_with_event_listener.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "animate_enter_with_event_listener_template.js", + "generated": "animate_enter_with_event_listener.js" + } + ], + "failureMessage": "Incorrect ɵɵanimateEnter() call" + } + ] + }, + { + "description": "should generate animate leave instructions on element with a simple string", + "inputFiles": [ + "animate_leave_with_string.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "animate_leave_with_string_template.js", + "generated": "animate_leave_with_string.js" + } + ], + "failureMessage": "Incorrect ɵɵanimateLeave() call" + } + ] + }, + { + "description": "should generate animate leave instructions on element with a binding", + "inputFiles": [ + "animate_leave_with_binding.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "animate_leave_with_binding_template.js", + "generated": "animate_leave_with_binding.js" + } + ], + "failureMessage": "Incorrect ɵɵanimateLeave() call" + } + ] + }, + { + "description": "should generate animate leave instructions on element with an event binding", + "inputFiles": [ + "animate_leave_with_event_listener.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "animate_leave_with_event_listener_template.js", + "generated": "animate_leave_with_event_listener.js" + } + ], + "failureMessage": "Incorrect ɵɵanimateLeave() call" + } + ] + }, + { + "description": "should generate animate leave instructions with host binding and simple string", + "inputFiles": [ + "animate_leave_with_string_host_bindings.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "animate_leave_with_string_host_bindings_template.js", + "generated": "animate_leave_with_string_host_bindings.js" + } + ], + "failureMessage": "Incorrect ɵɵanimateLeave() call" } ] } ] -} \ No newline at end of file +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_binding.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_binding.ts new file mode 100644 index 00000000000..e61164059b7 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_binding.ts @@ -0,0 +1,13 @@ +import {Component, signal} from '@angular/core'; + +@Component({ + selector: 'my-component', + template: ` +
+

Sliding Content

+
+ `, +}) +export class MyComponent { + enterClass = signal('slide'); +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_binding_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_binding_template.js new file mode 100644 index 00000000000..e5adfd2b10c --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_binding_template.js @@ -0,0 +1,6 @@ +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.ɵɵanimateEnter(function MyComponent_Template_animateenter_cb() { return ctx.enterClass(); }); + i0.ɵɵtext(2, "Sliding Content"); + i0.ɵɵdomElementEnd()(); + } }, encapsulation: 2 }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_event_listener.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_event_listener.ts new file mode 100644 index 00000000000..2ca0af57b87 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_event_listener.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'my-component', + template: ` +
+

Sliding Content

+
+ `, +}) +export class MyComponent { + slideFn(event: any) { + event.target.classList.add('slide-in'); + } +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_event_listener_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_event_listener_template.js new file mode 100644 index 00000000000..14a20d08397 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_event_listener_template.js @@ -0,0 +1,6 @@ +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.ɵɵanimateEnterListener(function MyComponent_Template_p_animateenter_1_listener($event) { return ctx.slideFn($event); }); + i0.ɵɵtext(2, "Sliding Content"); + i0.ɵɵdomElementEnd()(); + } }, encapsulation: 2 }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string.ts new file mode 100644 index 00000000000..7f21029eb86 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string.ts @@ -0,0 +1,12 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'my-component', + template: ` +
+

Sliding Content

+
+ `, +}) +export class MyComponent { +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string_host_bindings.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string_host_bindings.ts new file mode 100644 index 00000000000..c1e82a35ed8 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string_host_bindings.ts @@ -0,0 +1,19 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'child-component', + host: {'animate.enter': 'fade'}, + template: `

Sliding Content

`, +}) +export class ChildComponent { +} + +@Component({ + selector: 'my-component', + imports: [ChildComponent], + template: ` + + `, +}) +export class MyComponent { +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string_host_bindings_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string_host_bindings_template.js new file mode 100644 index 00000000000..2143690ed64 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string_host_bindings_template.js @@ -0,0 +1,13 @@ +ChildComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ChildComponent, selectors: [["child-component"]], hostBindings: function ChildComponent_HostBindings(rf, ctx) { if (rf & 1) { + i0.ɵɵanimateEnter("fade"); + } }, decls: 2, vars: 0, template: function ChildComponent_Template(rf, ctx) { if (rf & 1) { + i0.ɵɵdomElementStart(0, "p"); + i0.ɵɵtext(1, "Sliding Content"); + i0.ɵɵdomElementEnd(); + } }, encapsulation: 2 }); +… +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.ɵɵanimateEnter("slide"); + i0.ɵɵelementEnd(); + } }, dependencies: [ChildComponent], encapsulation: 2 }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string_template.js new file mode 100644 index 00000000000..74d59280a99 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_enter_with_string_template.js @@ -0,0 +1,15 @@ +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.ɵɵanimateEnter("slide"); + i0.ɵɵtext(2, "Sliding Content"); + i0.ɵɵdomElementEnd()(); + } + }, + encapsulation: 2 +}); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_binding.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_binding.ts new file mode 100644 index 00000000000..30541dda313 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_binding.ts @@ -0,0 +1,13 @@ +import {Component, signal} from '@angular/core'; + +@Component({ + selector: 'my-component', + template: ` +
+

Fading Content

+
+ `, +}) +export class MyComponent { + leaveClass = signal('fade'); +} 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 new file mode 100644 index 00000000000..a237e7a244b --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_binding_template.js @@ -0,0 +1,6 @@ +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"); + i0.ɵɵdomElementEnd()(); + } }, encapsulation: 2 }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_listener.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_listener.ts new file mode 100644 index 00000000000..58004a5bab0 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_listener.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'my-component', + template: ` +
+

Fading Content

+
+ `, +}) +export class MyComponent { + fadeFn(event: any) { + event.target.classList.add('fade-out'); + } +} 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 new file mode 100644 index 00000000000..1f4678afa82 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_event_listener_template.js @@ -0,0 +1,6 @@ +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"); + i0.ɵɵdomElementEnd()(); + } }, encapsulation: 2 }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string.ts new file mode 100644 index 00000000000..8d16ad4b99d --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string.ts @@ -0,0 +1,12 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'my-component', + template: ` +
+

Fading Content

+
+ `, +}) +export class MyComponent { +} diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_host_bindings.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_host_bindings.ts new file mode 100644 index 00000000000..e9ae003426b --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_host_bindings.ts @@ -0,0 +1,19 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'child-component', + template: `

Fading Content

`, + host: {'animate.leave': 'fade'}, +}) +export class ChildComponent { +} + +@Component({ + selector: 'my-component', + imports: [ChildComponent], + template: ` + + `, +}) +export class MyComponent { +} 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 new file mode 100644 index 00000000000..d424451b912 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_host_bindings_template.js @@ -0,0 +1,13 @@ +ChildComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ChildComponent, selectors: [["child-component"]], hostBindings: function ChildComponent_HostBindings(rf, ctx) { if (rf & 1) { + i0.ɵɵanimateLeave("fade"); + } }, 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"]], decls: 1, vars: 0, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { + i0.ɵɵelementStart(0, "child-component"); + i0.ɵɵanimateLeave("slide"); + i0.ɵɵelementEnd(); + } }, dependencies: [ChildComponent], encapsulation: 2 }); 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 new file mode 100644 index 00000000000..7a810bc80f4 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/animate_leave_with_string_template.js @@ -0,0 +1,6 @@ +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"); + i0.ɵɵdomElementEnd()(); + } }, encapsulation: 2 }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/test.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/test.js new file mode 100644 index 00000000000..52500744630 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/test.js @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyComponent { + fadeFn(event) { + event.target.classList.add('fade-out'); + } +} +MyComponent.ɵfac = function MyComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MyComponent)(); }; +MyComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComponent, selectors: [["my-component"]], decls: 3, vars: 2, template: function MyComponent_Template(rf, ctx) { if (rf & 1) { + i0.ɵɵdomElementStart(0, "div")(1, "p"); + i0.ɵɵtext(2, "Fading Content"); + i0.ɵɵdomElementEnd()(); + } if (rf & 2) { + i0.ɵɵadvance(); + i0.ɵɵanimateLeave(ctx.fadeFn); + } }, encapsulation: 2 }); +(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MyComponent, [{ + type: Component, + args: [{ + selector: 'my-component', + template: ` +
+

Fading Content

+
+ `, + }] + }], null, null); })(); +(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MyComponent, { className: "MyComponent", filePath: "animate_out_with_event_listener.ts", lineNumber: 11 }); })(); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/GOLDEN_PARTIAL.js new file mode 100644 index 00000000000..2cc55be1865 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/GOLDEN_PARTIAL.js @@ -0,0 +1,83 @@ +/**************************************************************************************************** + * PARTIAL FILE: static_animation_attribute.js + ****************************************************************************************************/ +import { Component, NgModule } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: false, selector: "my-app", ngImport: i0, template: '
', isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + selector: 'my-app', template: '
', + standalone: false + }] + }] }); +export class MyModule { +} +MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [MyApp] }); +MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{ + type: NgModule, + args: [{ declarations: [MyApp] }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: static_animation_attribute.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + exp: any; + any: any; + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} +export declare class MyModule { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵinj: i0.ɵɵInjectorDeclaration; +} + +/**************************************************************************************************** + * PARTIAL FILE: duplicate_animation_listeners.js + ****************************************************************************************************/ +import { Component, NgModule } from '@angular/core'; +import * as i0 from "@angular/core"; +export class MyApp { +} +MyApp.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component }); +MyApp.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: false, selector: "my-app", ngImport: i0, template: '
', isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{ + type: Component, + args: [{ + selector: 'my-app', + template: '
', + standalone: false + }] + }] }); +export class MyModule { +} +MyModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +MyModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, declarations: [MyApp] }); +MyModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyModule, decorators: [{ + type: NgModule, + args: [{ declarations: [MyApp] }] + }] }); + +/**************************************************************************************************** + * PARTIAL FILE: duplicate_animation_listeners.d.ts + ****************************************************************************************************/ +import * as i0 from "@angular/core"; +export declare class MyApp { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; +} +export declare class MyModule { + static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵinj: i0.ɵɵInjectorDeclaration; +} + diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/TEST_CASES.json new file mode 100644 index 00000000000..c8ee5d619c0 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/TEST_CASES.json @@ -0,0 +1,39 @@ +{ + "$schema": "../../test_case_schema.json", + "cases": [ + { + "description": "should not register any @attr attributes as static attributes", + "inputFiles": [ + "static_animation_attribute.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "static_animation_attribute_template.js", + "generated": "static_animation_attribute.js" + } + ], + "failureMessage": "Incorrect initialization attributes" + } + ] + }, + { + "description": "should dedup multiple [@event] listeners", + "inputFiles": [ + "duplicate_animation_listeners.ts" + ], + "expectations": [ + { + "files": [ + { + "expected": "duplicate_animation_listeners_template.js", + "generated": "duplicate_animation_listeners.js" + } + ], + "failureMessage": "Incorrect initialization attributes" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/duplicate_animation_listeners.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/duplicate_animation_listeners.ts similarity index 100% rename from packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/duplicate_animation_listeners.ts rename to packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/duplicate_animation_listeners.ts diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/duplicate_animation_listeners_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/duplicate_animation_listeners_template.js similarity index 100% rename from packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/duplicate_animation_listeners_template.js rename to packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/duplicate_animation_listeners_template.js diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/static_animation_attribute.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/static_animation_attribute.ts similarity index 100% rename from packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/static_animation_attribute.ts rename to packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/static_animation_attribute.ts diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/static_animation_attribute_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/static_animation_attribute_template.js similarity index 100% rename from packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/animations/static_animation_attribute_template.js rename to packages/compiler-cli/test/compliance/test_cases/r3_view_compiler/legacy_animations/static_animation_attribute_template.js diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 64fe4fc42f5..9a24a043527 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -187,6 +187,258 @@ runInEachFileSystem((os: string) => { expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); }); + describe('animate.enter', () => { + it('should compile animate.enter event bindings with a function call', () => { + env.write( + 'test.ts', + ` + import {Component, signal, ViewChild, ElementRef} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: + '
@if (show()) {

I should slide in

}
', + }) + class TestComponent { + show = signal(false); + animateFn = (event: any) => { + event.target.classList.add('slide-in'); + }; + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'i0.ɵɵanimateEnterListener(function TestComponent_Conditional_1_Template_p_animateenter_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.animateFn($event)); });', + ); + const instances = jsContents.match(/ɵɵanimateEnter/g); + expect(instances?.length).toBe(1); + }); + + it('should compile animate.enter bindings with a class string', () => { + env.write( + 'test.ts', + ` + import {Component, signal, ViewChild, ElementRef} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: + '
@if (show()) {

I should slide in

}
', + }) + class TestComponent { + show = signal(false); + fade = signal('fadein'); + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'i0.ɵɵanimateEnter(function TestComponent_Conditional_1_Template_animateenter_cb() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.fade()); });', + ); + const instances = jsContents.match(/ɵɵanimateEnter/g); + expect(instances?.length).toBe(1); + }); + + it('should compile animate.enter bindings with a string array', () => { + env.write( + 'test.ts', + ` + import {Component, signal, ViewChild, ElementRef} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: + '
@if (show()) {

I should slide in

}
', + }) + class TestComponent { + show = signal(false); + classList = ['fadein', 'stuff']; + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'i0.ɵɵanimateEnter(function TestComponent_Conditional_1_Template_animateenter_cb() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.classList); });', + ); + const instances = jsContents.match(/ɵɵanimateEnter/g); + expect(instances?.length).toBe(1); + }); + + it('should compile animate.enter with a string', () => { + env.write( + 'test.ts', + ` + import {Component, signal, ViewChild, ElementRef} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: + '
@if (show()) {

I should slide in

}
', + }) + class TestComponent { + show = signal(false); + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵanimateEnter("fade");'); + const updateInstances = jsContents.match(/ɵɵanimateEnter\(/g); + expect(updateInstances?.length).toBe(1); + }); + + it('should compile animate.enter with a host binding string', () => { + env.write( + 'test.ts', + ` + import {Component, signal, ViewChild, ElementRef} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + host: {'animate.enter': 'fade'}, + template: + '

I should slide in

', + }) + class TestComponent { + show = signal(false); + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵanimateEnter("fade");'); + const updateInstances = jsContents.match(/ɵɵanimateEnter\(/g); + expect(updateInstances?.length).toBe(1); + }); + }); + + describe('animate.leave', () => { + it('should compile animate.leave event bindings with a function call', () => { + env.write( + 'test.ts', + ` + import {Component, signal, ViewChild, ElementRef} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: + '
@if (show()) {

I should slide out

}
', + }) + class TestComponent { + show = signal(true); + animateFn = (event: any) => { + event.target.classList.add('slide-in'); + }; + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'i0.ɵɵanimateLeaveListener(function TestComponent_Conditional_1_Template_p_animateleave_0_listener($event) { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.animateFn($event)); });', + ); + const instances = jsContents.match(/ɵɵanimateLeave/g); + expect(instances?.length).toBe(1); + }); + + it('should compile animate.leave bindings with a class string', () => { + env.write( + 'test.ts', + ` + import {Component, signal, ViewChild, ElementRef} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: + '
@if (show()) {

I should slide out

}
', + }) + class TestComponent { + show = signal(true); + fade = signal('fadeout'); + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'i0.ɵɵanimateLeave(function TestComponent_Conditional_1_Template_animateleave_cb() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.fade()); });', + ); + const instances = jsContents.match(/ɵɵanimateLeave/g); + expect(instances?.length).toBe(1); + }); + + it('should compile animate.leave bindings with a string array', () => { + env.write( + 'test.ts', + ` + import {Component, signal, ViewChild, ElementRef} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: + '
@if (show()) {

I should slide out

}
', + }) + class TestComponent { + show = signal(true); + classList = ['fadeout', 'stuff']; + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'i0.ɵɵanimateLeave(function TestComponent_Conditional_1_Template_animateleave_cb() { i0.ɵɵrestoreView(_r1); const ctx_r1 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r1.classList); });', + ); + const instances = jsContents.match(/ɵɵanimateLeave/g); + expect(instances?.length).toBe(1); + }); + + it('should compile animate.leave with a string', () => { + env.write( + 'test.ts', + ` + import {Component, signal, ViewChild, ElementRef} from '@angular/core'; + + @Component({ + selector: 'test-cmp', + template: + '
@if (show()) {

I should slide out

}
', + }) + class TestComponent { + show = signal(true); + } + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('i0.ɵɵanimateLeave("fade");'); + const updateInstances = jsContents.match(/ɵɵanimateLeave\(/g); + expect(updateInstances?.length).toBe(1); + }); + }); + it('should compile Injectables with providedIn and factory with deps without errors', () => { env.write( 'test.ts', diff --git a/packages/compiler/src/expression_parser/ast.ts b/packages/compiler/src/expression_parser/ast.ts index e202500642f..ba63feb9c23 100644 --- a/packages/compiler/src/expression_parser/ast.ts +++ b/packages/compiler/src/expression_parser/ast.ts @@ -732,6 +732,7 @@ export class RecursiveAstVisitor implements AstVisitor { export class ParsedProperty { public readonly isLiteral: boolean; public readonly isLegacyAnimation: boolean; + public readonly isAnimation: boolean; constructor( public name: string, @@ -743,6 +744,7 @@ export class ParsedProperty { ) { this.isLiteral = this.type === ParsedPropertyType.LITERAL_ATTR; this.isLegacyAnimation = this.type === ParsedPropertyType.LEGACY_ANIMATION; + this.isAnimation = this.type === ParsedPropertyType.ANIMATION; } } @@ -751,6 +753,7 @@ export enum ParsedPropertyType { LITERAL_ATTR, LEGACY_ANIMATION, TWO_WAY, + ANIMATION, } export enum ParsedEventType { @@ -760,6 +763,8 @@ export enum ParsedEventType { LegacyAnimation, // Event side of a two-way binding (e.g. `[(property)]="expression"`). TwoWay, + // Animation specific event + Animation, } export class ParsedEvent { @@ -822,6 +827,8 @@ export enum BindingType { LegacyAnimation, // Property side of a two-way binding (e.g. `[(property)]="expression"`). TwoWay, + // A binding to an animation CSS class or function (e.g. `[animate.leave]="expression"`). + Animation, } export class BoundElementProperty { diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 687323133ab..563d5e39c1b 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -254,6 +254,17 @@ export class Identifiers { static property: o.ExternalReference = {name: 'ɵɵproperty', moduleName: CORE}; + static animationEnterListener: o.ExternalReference = { + name: 'ɵɵanimateEnterListener', + moduleName: CORE, + }; + static animationLeaveListener: o.ExternalReference = { + name: 'ɵɵanimateLeaveListener', + moduleName: CORE, + }; + static animationEnter: o.ExternalReference = {name: 'ɵɵanimateEnter', moduleName: CORE}; + static animationLeave: o.ExternalReference = {name: 'ɵɵanimateLeave', moduleName: CORE}; + static i18n: o.ExternalReference = {name: 'ɵɵi18n', moduleName: CORE}; static i18nAttributes: o.ExternalReference = {name: 'ɵɵi18nAttributes', moduleName: CORE}; static i18nExp: o.ExternalReference = {name: 'ɵɵi18nExp', moduleName: CORE}; diff --git a/packages/compiler/src/render3/r3_template_transform.ts b/packages/compiler/src/render3/r3_template_transform.ts index efce71c5358..8991e9743b6 100644 --- a/packages/compiler/src/render3/r3_template_transform.ts +++ b/packages/compiler/src/render3/r3_template_transform.ts @@ -777,6 +777,7 @@ class HtmlAstToIvyAst implements html.Visitor { matchableAttributes, boundEvents, keySpan, + absoluteOffset, ); } else if (bindParts[KW_AT_IDX]) { const keySpan = createKeySpan(srcSpan, '', name); @@ -836,6 +837,7 @@ class HtmlAstToIvyAst implements html.Visitor { matchableAttributes, boundEvents, keySpan, + absoluteOffset, ); } else if (delims.start === BINDING_DELIMS.PROPERTY.start) { this.bindingParser.parsePropertyBinding( @@ -1080,6 +1082,7 @@ class HtmlAstToIvyAst implements html.Visitor { targetMatchableAttrs: string[][], boundEvents: t.BoundEvent[], keySpan: ParseSourceSpan, + absoluteOffset: number, ) { const events: ParsedEvent[] = []; this.bindingParser.parseEvent( diff --git a/packages/compiler/src/template/pipeline/ir/src/enums.ts b/packages/compiler/src/template/pipeline/ir/src/enums.ts index e9358854d0f..08a7ea01fe2 100644 --- a/packages/compiler/src/template/pipeline/ir/src/enums.ts +++ b/packages/compiler/src/template/pipeline/ir/src/enums.ts @@ -279,6 +279,26 @@ export enum OpKind { * Creation op that attaches the location at which an element was defined in a template to it. */ SourceLocation, + + /** + * An operation to bind animation css classes to an element. + */ + Animation, + + /** + * An operation to bind animation css classes to an element. + */ + AnimationString, + + /** + * An operation to bind animation css classes to an element. + */ + AnimationBinding, + + /** + * An operation to bind animation events to an element. + */ + AnimationListener, } /** @@ -504,6 +524,11 @@ export enum BindingKind { * Property side of a two-way binding. */ TwoWayProperty, + + /** + * Property side of an animation binding. + */ + Animation, } /** @@ -608,6 +633,22 @@ export enum TemplateKind { Block, } +/** + * Kinds of animations + */ +export const enum AnimationKind { + ENTER = 'enter', + LEAVE = 'leave', +} + +/** + * Kinds of animations + */ +export const enum AnimationBindingKind { + STRING, + VALUE, +} + /** * Kinds of modifiers for a defer block. */ diff --git a/packages/compiler/src/template/pipeline/ir/src/expression.ts b/packages/compiler/src/template/pipeline/ir/src/expression.ts index 39026dd3a22..81f0512d877 100644 --- a/packages/compiler/src/template/pipeline/ir/src/expression.ts +++ b/packages/compiler/src/template/pipeline/ir/src/expression.ts @@ -1091,6 +1091,8 @@ export function transformExpressionsInOp( case OpKind.StyleMap: case OpKind.ClassProp: case OpKind.ClassMap: + case OpKind.AnimationString: + case OpKind.AnimationBinding: case OpKind.Binding: if (op.expression instanceof Interpolation) { transformExpressionsInInterpolation(op.expression, transform, flags); @@ -1141,6 +1143,8 @@ export function transformExpressionsInOp( op.contextValue = transformExpressionsInExpression(op.contextValue, transform, flags); } break; + case OpKind.Animation: + case OpKind.AnimationListener: case OpKind.Listener: case OpKind.TwoWayListener: for (const innerOp of op.handlerOps) { diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts index 68d1219d643..04ce95021db 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts @@ -11,6 +11,7 @@ import * as i18n from '../../../../../i18n/i18n_ast'; import * as o from '../../../../../output/output_ast'; import {ParseSourceSpan} from '../../../../../parse_util'; import { + AnimationKind, BindingKind, DeferOpModifierKind, DeferTriggerKind, @@ -32,7 +33,7 @@ import { import {ListEndOp, NEW_OP, StatementOp, VariableOp} from './shared'; -import type {UpdateOp} from './update'; +import type {Interpolation, UpdateOp} from './update'; /** * An operation usable on the creation side of the IR. @@ -73,6 +74,9 @@ export type CreateOp = | I18nContextOp | I18nAttributesOp | DeclareLetOp + | AnimationListenerOp + | AnimationStringOp + | AnimationOp | SourceLocationOp; /** @@ -703,6 +707,140 @@ export function createTextOp( }; } +/** + * A logical operation representing binding to an animation in the create IR. + */ +export interface AnimationStringOp extends Op { + kind: OpKind.AnimationString; + + target: XrefId; + + /** + * The name of the extracted attribute. + */ + name: string; + + /** + * Kind of animation (enter or leave). + */ + animationKind: AnimationKind; + + /** + * Expression which is bound to the property. + */ + expression: o.Expression | Interpolation; + + i18nMessage: XrefId | null; + + /** + * The security context of the binding. + */ + securityContext: SecurityContext | SecurityContext[]; + + /** + * The sanitizer for this property. + */ + sanitizer: o.Expression | null; + + sourceSpan: ParseSourceSpan; +} + +/** + * Create an `AnimationOp`. + */ +export function createAnimationStringOp( + name: string, + target: XrefId, + animationKind: AnimationKind, + expression: o.Expression | Interpolation, + securityContext: SecurityContext | SecurityContext[], + sourceSpan: ParseSourceSpan, +): AnimationStringOp { + return { + kind: OpKind.AnimationString, + name, + target, + animationKind, + expression, + i18nMessage: null, + securityContext, + sanitizer: null, + sourceSpan, + ...NEW_OP, + }; +} + +/** + * A logical operation representing binding to an animation in the create IR. + */ +export interface AnimationOp extends Op { + kind: OpKind.Animation; + + target: XrefId; + + /** + * The name of the extracted attribute. + */ + name: string; + + /** + * Kind of animation (enter or leave). + */ + animationKind: AnimationKind; + + /** + * A list of `UpdateOp`s representing the body of the callback function. + */ + handlerOps: OpList; + + /** + * Name of the function + */ + handlerFnName: string | null; + + i18nMessage: XrefId | null; + + /** + * The security context of the binding. + */ + securityContext: SecurityContext | SecurityContext[]; + + /** + * The sanitizer for this property. + */ + sanitizer: o.Expression | null; + + sourceSpan: ParseSourceSpan; +} + +/** + * Create an `AnimationOp`. + */ +export function createAnimationOp( + name: string, + target: XrefId, + animationKind: AnimationKind, + callbackOps: Array, + securityContext: SecurityContext | SecurityContext[], + sourceSpan: ParseSourceSpan, +): AnimationOp { + const handlerOps = new OpList(); + handlerOps.push(callbackOps); + return { + kind: OpKind.Animation, + name, + target, + animationKind, + handlerOps, + handlerFnName: null, + i18nMessage: null, + securityContext, + sanitizer: null, + sourceSpan, + ...NEW_OP, + }; +} + /** * Logical operation representing an event listener on an element in the creation IR. */ @@ -795,6 +933,89 @@ export function createListenerOp( }; } +export interface AnimationListenerOp extends Op { + kind: OpKind.AnimationListener; + + target: XrefId; + targetSlot: SlotHandle; + + /** + * Whether this listener is from a host binding. + */ + hostListener: boolean; + + /** + * Name of the event which is being listened to. + */ + name: string; + + /** + * Whether the event is on enter or leave + */ + animationKind: AnimationKind; + + /** + * Tag name of the element on which this listener is placed. Might be null, if this listener + * belongs to a host binding. + */ + tag: string | null; + + /** + * A list of `UpdateOp`s representing the body of the event listener. + */ + handlerOps: OpList; + + /** + * Name of the function + */ + handlerFnName: string | null; + + /** + * Whether this listener is known to consume `$event` in its body. + */ + consumesDollarEvent: boolean; + + /** + * Some event listeners can have a target, e.g. in `document:dragover`. + */ + eventTarget: string | null; + + sourceSpan: ParseSourceSpan; +} + +/** + * Create a `ListenerOp`. Host bindings reuse all the listener logic. + */ +export function createAnimationListenerOp( + target: XrefId, + targetSlot: SlotHandle, + name: string, + tag: string | null, + handlerOps: Array, + animationKind: AnimationKind, + eventTarget: string | null, + hostListener: boolean, + sourceSpan: ParseSourceSpan, +): AnimationListenerOp { + const handlerList = new OpList(); + handlerList.push(handlerOps); + return { + kind: OpKind.AnimationListener, + target, + targetSlot, + tag, + hostListener, + name, + animationKind, + handlerOps: handlerList, + handlerFnName: null, + consumesDollarEvent: false, + eventTarget, + sourceSpan, + ...NEW_OP, + }; +} + /** * Logical operation representing the event side of a two-way binding on an element * in the creation IR. diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/host.ts b/packages/compiler/src/template/pipeline/ir/src/ops/host.ts index d2ea38bb6b6..7371bf2a119 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/host.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/host.ts @@ -9,7 +9,7 @@ import * as o from '../../../../../../src/output/output_ast'; import {ParseSourceSpan} from '../../../../../../src/parse_util'; import {SecurityContext} from '../../../../../core'; -import {OpKind} from '../enums'; +import {BindingKind, OpKind} from '../enums'; import {Op, XrefId} from '../operations'; import {ConsumesVarsTrait, TRAIT_CONSUMES_VARS} from '../traits'; @@ -24,7 +24,7 @@ export interface DomPropertyOp extends Op, ConsumesVarsTrait { kind: OpKind.DomProperty; name: string; expression: o.Expression | Interpolation; - isLegacyAnimationTrigger: boolean; + bindingKind: BindingKind; i18nContext: XrefId | null; @@ -38,7 +38,7 @@ export interface DomPropertyOp extends Op, ConsumesVarsTrait { export function createDomPropertyOp( name: string, expression: o.Expression | Interpolation, - isLegacyAnimationTrigger: boolean, + bindingKind: BindingKind, i18nContext: XrefId | null, securityContext: SecurityContext | SecurityContext[], sourceSpan: ParseSourceSpan, @@ -47,7 +47,7 @@ export function createDomPropertyOp( kind: OpKind.DomProperty, name, expression, - isLegacyAnimationTrigger, + bindingKind, i18nContext, securityContext, sanitizer: null, diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts index 5660c6c7228..070c94ad008 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/update.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/update.ts @@ -11,6 +11,8 @@ import * as i18n from '../../../../../i18n/i18n_ast'; import * as o from '../../../../../output/output_ast'; import {ParseSourceSpan} from '../../../../../parse_util'; import { + AnimationBindingKind, + AnimationKind, BindingKind, DeferOpModifierKind, I18nExpressionFor, @@ -53,6 +55,7 @@ export type UpdateOp = | I18nApplyOp | RepeaterOp | DeferWhenOp + | AnimationBindingOp | StoreLetOp; /** @@ -227,7 +230,7 @@ export interface PropertyOp extends Op, ConsumesVarsTrait, DependsOnSl /** * Whether this property is an animation trigger. */ - isLegacyAnimationTrigger: boolean; + bindingKind: BindingKind; /** * The security context of the binding. @@ -260,7 +263,7 @@ export function createPropertyOp( target: XrefId, name: string, expression: o.Expression | Interpolation, - isLegacyAnimationTrigger: boolean, + bindingKind: BindingKind, securityContext: SecurityContext | SecurityContext[], isStructuralTemplateAttribute: boolean, templateKind: TemplateKind | null, @@ -273,7 +276,7 @@ export function createPropertyOp( target, name, expression, - isLegacyAnimationTrigger, + bindingKind, securityContext, sanitizer: null, isStructuralTemplateAttribute, @@ -760,6 +763,76 @@ export function createRepeaterOp( }; } +/** + * A logical operation representing binding to an animation in the update IR. + */ +export interface AnimationBindingOp extends Op { + kind: OpKind.AnimationBinding; + + /** + * The name of the extracted attribute. + */ + name: string; + + /** + * Reference to the element on which the property is bound. + */ + target: XrefId; + + /** + * Name of the bound property. + */ + animationKind: AnimationKind; + + /** + * Expression which is bound to the property. + */ + expression: o.Expression | Interpolation; + + i18nMessage: XrefId | null; + + /** + * The security context of the binding. + */ + securityContext: SecurityContext | SecurityContext[]; + + /** + * The sanitizer for this property. + */ + sanitizer: o.Expression | null; + + sourceSpan: ParseSourceSpan; + + animationBindingKind: AnimationBindingKind; +} + +/** + * Create an `AnimationBindingOp`. + */ +export function createAnimationBindingOp( + name: string, + target: XrefId, + animationKind: AnimationKind, + expression: o.Expression | Interpolation, + securityContext: SecurityContext | SecurityContext[], + sourceSpan: ParseSourceSpan, + animationBindingKind: AnimationBindingKind, +): AnimationBindingOp { + return { + kind: OpKind.AnimationBinding, + name, + target, + animationKind, + expression, + i18nMessage: null, + securityContext, + sanitizer: null, + sourceSpan, + animationBindingKind, + ...NEW_OP, + }; +} + export interface DeferWhenOp extends Op, DependsOnSlotContextOpTrait, ConsumesVarsTrait { kind: OpKind.DeferWhen; diff --git a/packages/compiler/src/template/pipeline/src/compilation.ts b/packages/compiler/src/template/pipeline/src/compilation.ts index a39ca66c10f..51076e6545b 100644 --- a/packages/compiler/src/template/pipeline/src/compilation.ts +++ b/packages/compiler/src/template/pipeline/src/compilation.ts @@ -197,7 +197,12 @@ export abstract class CompilationUnit { *ops(): Generator { for (const op of this.create) { yield op; - if (op.kind === ir.OpKind.Listener || op.kind === ir.OpKind.TwoWayListener) { + if ( + op.kind === ir.OpKind.Listener || + op.kind === ir.OpKind.Animation || + op.kind === ir.OpKind.AnimationListener || + op.kind === ir.OpKind.TwoWayListener + ) { for (const listenerOp of op.handlerOps) { yield listenerOp; } diff --git a/packages/compiler/src/template/pipeline/src/emit.ts b/packages/compiler/src/template/pipeline/src/emit.ts index 2916262f9c6..db4d882fe57 100644 --- a/packages/compiler/src/template/pipeline/src/emit.ts +++ b/packages/compiler/src/template/pipeline/src/emit.ts @@ -29,6 +29,7 @@ import {chain} from './phases/chaining'; import {collapseSingletonInterpolations} from './phases/collapse_singleton_interpolations'; import {generateConditionalExpressions} from './phases/conditionals'; import {collectElementConsts} from './phases/const_collection'; +import {convertAnimations} from './phases/convert_animations'; import {convertI18nBindings} from './phases/convert_i18n_bindings'; import {createI18nContexts} from './phases/create_i18n_contexts'; import {deduplicateTextBindings} from './phases/deduplicate_text_bindings'; @@ -107,6 +108,7 @@ const phases: Phase[] = [ {kind: Kind.Both, fn: deduplicateTextBindings}, {kind: Kind.Both, fn: specializeStyleBindings}, {kind: Kind.Both, fn: specializeBindings}, + {kind: Kind.Both, fn: convertAnimations}, {kind: Kind.Both, fn: extractAttributes}, {kind: Kind.Tmpl, fn: createI18nContexts}, {kind: Kind.Both, fn: parseExtractedStyles}, diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index 0c00c9b8785..b3941f0a933 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -38,6 +38,9 @@ const domSchema = new DomElementSchemaRegistry(); // Tag name of the `ng-template` element. const NG_TEMPLATE_TAG_NAME = 'ng-template'; +// prefix for any animation binding +const ANIMATE_PREFIX = 'animate.'; + export function isI18nRootNode(meta?: i18n.I18nMeta): meta is i18n.Message { return meta instanceof i18n.Message; } @@ -112,6 +115,9 @@ export function ingestHostBinding( if (property.isLegacyAnimation) { bindingKind = ir.BindingKind.LegacyAnimation; } + if (property.isAnimation) { + bindingKind = ir.BindingKind.Animation; + } const securityContexts = bindingParser .calcPossibleSecurityContexts( input.componentSelector, @@ -1226,6 +1232,7 @@ const BINDING_KINDS = new Map([ [e.BindingType.Class, ir.BindingKind.ClassName], [e.BindingType.Style, ir.BindingKind.StyleProperty], [e.BindingType.LegacyAnimation, ir.BindingKind.LegacyAnimation], + [e.BindingType.Animation, ir.BindingKind.Animation], ]); /** @@ -1272,7 +1279,6 @@ function ingestElementBindings( element: t.Element, ): void { let bindings = new Array(); - let i18nAttributeBindingNames = new Set(); for (const attr of element.attributes) { @@ -1343,6 +1349,20 @@ function ingestElementBindings( output.sourceSpan, ), ); + } else if (output.type === e.ParsedEventType.Animation) { + unit.create.push( + ir.createAnimationListenerOp( + op.xref, + op.handle, + output.name, + op.tag, + makeListenerHandlerOps(unit, output.handler, output.handlerSpan), + output.name.endsWith('enter') ? ir.AnimationKind.ENTER : ir.AnimationKind.LEAVE, + output.target, + false, + output.sourceSpan, + ), + ); } else { unit.create.push( ir.createListenerOp( @@ -1380,7 +1400,6 @@ function ingestTemplateBindings( templateKind: ir.TemplateKind | null, ): void { let bindings = new Array(); - for (const attr of template.templateAttrs) { if (attr instanceof t.TextAttribute) { const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true); @@ -1604,7 +1623,9 @@ function createTemplateBinding( if ( !isTextBinding && - (type === e.BindingType.Attribute || type === e.BindingType.LegacyAnimation) + (type === e.BindingType.Attribute || + type === e.BindingType.LegacyAnimation || + type === e.BindingType.Animation) ) { // Again, this binding doesn't really target the ng-template; it actually targets the element // inside the structural template. In the case of non-text attribute or animation bindings, @@ -1811,29 +1832,35 @@ function ingestControlFlowInsertionPoint( if (root !== null) { // Collect the static attributes for content projection purposes. for (const attr of root.attributes) { - const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true); - unit.update.push( - ir.createBindingOp( - xref, - ir.BindingKind.Attribute, - attr.name, - o.literal(attr.value), - null, - securityContext, - true, - false, - null, - asMessage(attr.i18n), - attr.sourceSpan, - ), - ); + if (!attr.name.startsWith(ANIMATE_PREFIX)) { + const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true); + unit.update.push( + ir.createBindingOp( + xref, + ir.BindingKind.Attribute, + attr.name, + o.literal(attr.value), + null, + securityContext, + true, + false, + null, + asMessage(attr.i18n), + attr.sourceSpan, + ), + ); + } } // Also collect the inputs since they participate in content projection as well. // Note that TDB used to collect the outputs as well, but it wasn't passing them into // the template instruction. Here we just don't collect them. for (const attr of root.inputs) { - if (attr.type !== e.BindingType.LegacyAnimation && attr.type !== e.BindingType.Attribute) { + if ( + attr.type !== e.BindingType.LegacyAnimation && + attr.type !== e.BindingType.Animation && + attr.type !== e.BindingType.Attribute + ) { const securityContext = domSchema.securityContext(NG_TEMPLATE_TAG_NAME, attr.name, true); unit.create.push( ir.createExtractedAttributeOp( diff --git a/packages/compiler/src/template/pipeline/src/instruction.ts b/packages/compiler/src/template/pipeline/src/instruction.ts index e60cfef9b4c..670fe8d1c8b 100644 --- a/packages/compiler/src/template/pipeline/src/instruction.ts +++ b/packages/compiler/src/template/pipeline/src/instruction.ts @@ -834,6 +834,62 @@ export function domProperty( return propertyBase(Identifiers.domProperty, name, expression, sanitizer, sourceSpan); } +export function animation( + animationKind: ir.AnimationKind, + handlerFn: o.Expression, + sanitizer: o.Expression | null, + sourceSpan: ParseSourceSpan, +): ir.CreateOp { + const args = [handlerFn]; + if (sanitizer !== null) { + args.push(sanitizer); + } + const identifier = + animationKind === ir.AnimationKind.ENTER + ? Identifiers.animationEnter + : Identifiers.animationLeave; + return call(identifier, args, sourceSpan); +} + +export function animationString( + animationKind: ir.AnimationKind, + expression: o.Expression | ir.Interpolation, + sanitizer: o.Expression | null, + sourceSpan: ParseSourceSpan, +): ir.CreateOp { + const value = + expression instanceof ir.Interpolation + ? interpolationToExpression(expression, sourceSpan) + : expression; + const args = [value]; + if (sanitizer !== null) { + args.push(sanitizer); + } + const identifier = + animationKind === ir.AnimationKind.ENTER + ? Identifiers.animationEnter + : Identifiers.animationLeave; + return call(identifier, args, sourceSpan); +} + +export function animationListener( + animationKind: ir.AnimationKind, + handlerFn: o.Expression, + eventTargetResolver: o.ExternalReference | null, + sourceSpan: ParseSourceSpan, +): ir.CreateOp { + const args = [handlerFn]; + if (eventTargetResolver !== null) { + args.push(o.importExpr(eventTargetResolver)); + } + const identifier = + animationKind === ir.AnimationKind.ENTER + ? Identifiers.animationEnterListener + : Identifiers.animationLeaveListener; + + return call(identifier, args, sourceSpan); +} + export function syntheticHostProperty( name: string, expression: o.Expression, diff --git a/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts b/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts index 1bb169cdf65..671a09becf9 100644 --- a/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts +++ b/packages/compiler/src/template/pipeline/src/phases/attribute_extraction.ts @@ -24,7 +24,10 @@ export function extractAttributes(job: CompilationJob): void { extractAttributeOp(unit, op, elements); break; case ir.OpKind.Property: - if (!op.isLegacyAnimationTrigger) { + if ( + op.bindingKind !== ir.BindingKind.LegacyAnimation && + op.bindingKind !== ir.BindingKind.Animation + ) { let bindingKind: ir.BindingKind; if (op.i18nMessage !== null && op.templateKind === null) { // If the binding has an i18n context, it is an i18n attribute, and should have that diff --git a/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts b/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts index f77d8f89edd..974cc92c499 100644 --- a/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts +++ b/packages/compiler/src/template/pipeline/src/phases/binding_specialization.ts @@ -47,6 +47,19 @@ export function specializeBindings(job: CompilationJob): void { ir.OpList.remove(op); const target = lookupElement(elements, op.target); target.nonBindable = true; + } else if (op.name.startsWith('animate.')) { + ir.OpList.replace( + op, + ir.createAnimationBindingOp( + op.name, + op.target, + op.name === 'animate.enter' ? ir.AnimationKind.ENTER : ir.AnimationKind.LEAVE, + op.expression, + op.securityContext, + op.sourceSpan, + ir.AnimationBindingKind.STRING, + ), + ); } else { const [namespace, name] = splitNsName(op.name); ir.OpList.replace( @@ -66,6 +79,20 @@ export function specializeBindings(job: CompilationJob): void { ); } break; + case ir.BindingKind.Animation: + ir.OpList.replace( + op, + ir.createAnimationBindingOp( + op.name, + op.target, + op.name === 'animate.enter' ? ir.AnimationKind.ENTER : ir.AnimationKind.LEAVE, + op.expression, + op.securityContext, + op.sourceSpan, + ir.AnimationBindingKind.VALUE, + ), + ); + break; case ir.BindingKind.Property: case ir.BindingKind.LegacyAnimation: if (job.kind === CompilationJobKind.Host) { @@ -74,7 +101,7 @@ export function specializeBindings(job: CompilationJob): void { ir.createDomPropertyOp( op.name, op.expression, - op.bindingKind === ir.BindingKind.LegacyAnimation, + op.bindingKind, op.i18nContext, op.securityContext, op.sourceSpan, @@ -87,7 +114,7 @@ export function specializeBindings(job: CompilationJob): void { op.target, op.name, op.expression, - op.bindingKind === ir.BindingKind.LegacyAnimation, + op.bindingKind, op.securityContext, op.isStructuralTemplateAttribute, op.templateKind, diff --git a/packages/compiler/src/template/pipeline/src/phases/chaining.ts b/packages/compiler/src/template/pipeline/src/phases/chaining.ts index d24a712f9e4..c123956a258 100644 --- a/packages/compiler/src/template/pipeline/src/phases/chaining.ts +++ b/packages/compiler/src/template/pipeline/src/phases/chaining.ts @@ -42,6 +42,10 @@ const CHAIN_COMPATIBILITY = new Map([ [R3.domElementContainerEnd, R3.domElementContainerEnd], [R3.domListener, R3.domListener], [R3.domTemplate, R3.domTemplate], + [R3.animationEnter, R3.animationEnter], + [R3.animationLeave, R3.animationLeave], + [R3.animationEnterListener, R3.animationEnterListener], + [R3.animationLeaveListener, R3.animationLeaveListener], ]); /** diff --git a/packages/compiler/src/template/pipeline/src/phases/convert_animations.ts b/packages/compiler/src/template/pipeline/src/phases/convert_animations.ts new file mode 100644 index 00000000000..810d3b9a88a --- /dev/null +++ b/packages/compiler/src/template/pipeline/src/phases/convert_animations.ts @@ -0,0 +1,75 @@ +/** + * @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 * as ir from '../../ir'; +import * as o from '../../../../output/output_ast'; +import {CompilationJob, CompilationJobKind} from '../compilation'; + +/** + * Looks up an element in the given map by xref ID. + */ +function lookupElement( + elements: Map, + xref: ir.XrefId, +): ir.ElementOrContainerOps { + const el = elements.get(xref); + if (el === undefined) { + throw new Error('All attributes should have an element-like target.'); + } + return el; +} + +export function convertAnimations(job: CompilationJob): void { + const elements = new Map(); + for (const unit of job.units) { + for (const op of unit.create) { + if (!ir.isElementOrContainerOp(op)) { + continue; + } + elements.set(op.xref, op); + } + } + + for (const unit of job.units) { + for (const op of unit.ops()) { + if (op.kind === ir.OpKind.AnimationBinding) { + const createAnimationOp = getAnimationOp(op); + if (job.kind === CompilationJobKind.Host) { + unit.create.push(createAnimationOp); + } else { + ir.OpList.insertAfter(createAnimationOp, lookupElement(elements, op.target)); + } + ir.OpList.remove(op); + } + } + } +} + +function getAnimationOp(op: ir.AnimationBindingOp): ir.AnimationOp | ir.AnimationStringOp { + if (op.animationBindingKind === ir.AnimationBindingKind.STRING) { + // this is a simple string case + return ir.createAnimationStringOp( + op.name, + op.target, + op.name === 'animate.enter' ? ir.AnimationKind.ENTER : ir.AnimationKind.LEAVE, + op.expression, + op.securityContext, + op.sourceSpan, + ); + } else { + const expression = op.expression as ir.Expression; + return ir.createAnimationOp( + op.name, + op.target, + op.name === 'animate.enter' ? ir.AnimationKind.ENTER : ir.AnimationKind.LEAVE, + [ir.createStatementOp(new o.ReturnStatement(expression, expression.sourceSpan))], + op.securityContext, + op.sourceSpan, + ); + } +} diff --git a/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts b/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts index 9b508022243..f77e885b4c5 100644 --- a/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts +++ b/packages/compiler/src/template/pipeline/src/phases/generate_variables.ts @@ -63,6 +63,8 @@ function recursivelyProcessView(view: ViewCompilationUnit, parentScope: Scope | op.trackByOps.prepend(generateVariablesInScopeForView(view, scope, false)); } break; + case ir.OpKind.Animation: + case ir.OpKind.AnimationListener: case ir.OpKind.Listener: case ir.OpKind.TwoWayListener: // Prepend variables to listener handler functions. @@ -230,7 +232,7 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope | null): Scope function generateVariablesInScopeForView( view: ViewCompilationUnit, scope: Scope, - isListener: boolean, + isCallback: boolean, ): ir.VariableOp[] { const newOps: ir.VariableOp[] = []; @@ -288,7 +290,7 @@ function generateVariablesInScopeForView( ); } - if (scope.view !== view.xref || isListener) { + if (scope.view !== view.xref || isCallback) { for (const decl of scope.letDeclarations) { newOps.push( ir.createVariableOp( diff --git a/packages/compiler/src/template/pipeline/src/phases/naming.ts b/packages/compiler/src/template/pipeline/src/phases/naming.ts index 307d2c86634..46d010d857d 100644 --- a/packages/compiler/src/template/pipeline/src/phases/naming.ts +++ b/packages/compiler/src/template/pipeline/src/phases/naming.ts @@ -52,10 +52,34 @@ function addNamesToView( switch (op.kind) { case ir.OpKind.Property: case ir.OpKind.DomProperty: - if (op.isLegacyAnimationTrigger) { + if (op.bindingKind === ir.BindingKind.LegacyAnimation) { op.name = '@' + op.name; } break; + case ir.OpKind.Animation: + if (op.handlerFnName === null) { + const animationKind = op.name.replace('.', ''); + op.handlerFnName = `${unit.fnName}_${animationKind}_cb`; + op.handlerFnName = sanitizeIdentifier(op.handlerFnName); + } + break; + case ir.OpKind.AnimationListener: + if (op.handlerFnName !== null) { + break; + } + if (!op.hostListener && op.targetSlot.slot === null) { + throw new Error(`Expected a slot to be assigned`); + } + const animationKind = op.name.replace('.', ''); + if (op.hostListener) { + op.handlerFnName = `${baseName}_${animationKind}_HostBindingHandler`; + } else { + op.handlerFnName = `${unit.fnName}_${op.tag!.replace('-', '_')}_${animationKind}_${ + op.targetSlot.slot + }_listener`; + } + op.handlerFnName = sanitizeIdentifier(op.handlerFnName); + break; case ir.OpKind.Listener: if (op.handlerFnName !== null) { break; diff --git a/packages/compiler/src/template/pipeline/src/phases/next_context_merging.ts b/packages/compiler/src/template/pipeline/src/phases/next_context_merging.ts index 4e312469b39..7f598125c9f 100644 --- a/packages/compiler/src/template/pipeline/src/phases/next_context_merging.ts +++ b/packages/compiler/src/template/pipeline/src/phases/next_context_merging.ts @@ -26,7 +26,12 @@ import type {CompilationJob} from '../compilation'; export function mergeNextContextExpressions(job: CompilationJob): void { for (const unit of job.units) { for (const op of unit.create) { - if (op.kind === ir.OpKind.Listener || op.kind === ir.OpKind.TwoWayListener) { + if ( + op.kind === ir.OpKind.Listener || + op.kind === ir.OpKind.Animation || + op.kind === ir.OpKind.AnimationListener || + op.kind === ir.OpKind.TwoWayListener + ) { mergeNextContextsInOps(op.handlerOps); } } diff --git a/packages/compiler/src/template/pipeline/src/phases/ordering.ts b/packages/compiler/src/template/pipeline/src/phases/ordering.ts index 50d0563cf9d..5a16d20c19c 100644 --- a/packages/compiler/src/template/pipeline/src/phases/ordering.ts +++ b/packages/compiler/src/template/pipeline/src/phases/ordering.ts @@ -25,7 +25,9 @@ function kindWithInterpolationTest( function basicListenerKindTest(op: ir.CreateOp): boolean { return ( (op.kind === ir.OpKind.Listener && !(op.hostListener && op.isLegacyAnimationListener)) || - op.kind === ir.OpKind.TwoWayListener + op.kind === ir.OpKind.TwoWayListener || + op.kind === ir.OpKind.Animation || + op.kind === ir.OpKind.AnimationListener ); } @@ -85,6 +87,7 @@ const UPDATE_HOST_ORDERING: Array> = [ const handledOpKinds = new Set([ ir.OpKind.Listener, ir.OpKind.TwoWayListener, + ir.OpKind.AnimationListener, ir.OpKind.StyleMap, ir.OpKind.ClassMap, ir.OpKind.StyleProp, @@ -93,6 +96,7 @@ const handledOpKinds = new Set([ ir.OpKind.TwoWayProperty, ir.OpKind.DomProperty, ir.OpKind.Attribute, + ir.OpKind.Animation, ]); /** diff --git a/packages/compiler/src/template/pipeline/src/phases/reify.ts b/packages/compiler/src/template/pipeline/src/phases/reify.ts index f0f1b68b829..aaa6c6b90a4 100644 --- a/packages/compiler/src/template/pipeline/src/phases/reify.ts +++ b/packages/compiler/src/template/pipeline/src/phases/reify.ts @@ -258,6 +258,37 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList | ir.OpList): void { for (const op of ops) { - if (op.kind === ir.OpKind.Listener || op.kind === ir.OpKind.TwoWayListener) { + if ( + op.kind === ir.OpKind.Listener || + op.kind === ir.OpKind.TwoWayListener || + op.kind === ir.OpKind.AnimationListener + ) { ir.transformExpressionsInOp( op, (expr) => { if (expr instanceof ir.LexicalReadExpr && expr.name === '$event') { // Two-way listeners always consume `$event` so they omit this field. - if (op.kind === ir.OpKind.Listener) { + if (op.kind === ir.OpKind.Listener || op.kind === ir.OpKind.AnimationListener) { op.consumesDollarEvent = true; } return new o.ReadVarExpr(expr.name); diff --git a/packages/compiler/src/template/pipeline/src/phases/resolve_names.ts b/packages/compiler/src/template/pipeline/src/phases/resolve_names.ts index 07a33b31aaf..a8c3e40192d 100644 --- a/packages/compiler/src/template/pipeline/src/phases/resolve_names.ts +++ b/packages/compiler/src/template/pipeline/src/phases/resolve_names.ts @@ -74,6 +74,8 @@ function processLexicalScope( break; } break; + case ir.OpKind.Animation: + case ir.OpKind.AnimationListener: case ir.OpKind.Listener: case ir.OpKind.TwoWayListener: // Listener functions have separate variable declarations, so process them as a separate @@ -92,7 +94,12 @@ function processLexicalScope( // scope. Also, look for `ir.RestoreViewExpr`s and match them with the snapshotted view context // variable. for (const op of ops) { - if (op.kind == ir.OpKind.Listener || op.kind === ir.OpKind.TwoWayListener) { + if ( + op.kind == ir.OpKind.Listener || + op.kind === ir.OpKind.TwoWayListener || + op.kind === ir.OpKind.Animation || + op.kind === ir.OpKind.AnimationListener + ) { // Listeners were already processed above with their own scopes. continue; } diff --git a/packages/compiler/src/template/pipeline/src/phases/save_restore_view.ts b/packages/compiler/src/template/pipeline/src/phases/save_restore_view.ts index a42ac686b13..8c2b074a32d 100644 --- a/packages/compiler/src/template/pipeline/src/phases/save_restore_view.ts +++ b/packages/compiler/src/template/pipeline/src/phases/save_restore_view.ts @@ -31,7 +31,12 @@ export function saveAndRestoreView(job: ComponentCompilationJob): void { ]); for (const op of unit.create) { - if (op.kind !== ir.OpKind.Listener && op.kind !== ir.OpKind.TwoWayListener) { + if ( + op.kind !== ir.OpKind.Listener && + op.kind !== ir.OpKind.TwoWayListener && + op.kind !== ir.OpKind.Animation && + op.kind !== ir.OpKind.AnimationListener + ) { continue; } @@ -58,7 +63,7 @@ export function saveAndRestoreView(job: ComponentCompilationJob): void { function addSaveRestoreViewOperationToListener( unit: ViewCompilationUnit, - op: ir.ListenerOp | ir.TwoWayListenerOp, + op: ir.ListenerOp | ir.TwoWayListenerOp | ir.AnimationOp | ir.AnimationListenerOp, ) { op.handlerOps.prepend([ ir.createVariableOp( diff --git a/packages/compiler/src/template/pipeline/src/phases/temporary_variables.ts b/packages/compiler/src/template/pipeline/src/phases/temporary_variables.ts index 168b20cc0a1..fe4018726bc 100644 --- a/packages/compiler/src/template/pipeline/src/phases/temporary_variables.ts +++ b/packages/compiler/src/template/pipeline/src/phases/temporary_variables.ts @@ -81,7 +81,12 @@ function generateTemporaries( ); opCount++; - if (op.kind === ir.OpKind.Listener || op.kind === ir.OpKind.TwoWayListener) { + if ( + op.kind === ir.OpKind.Listener || + op.kind === ir.OpKind.Animation || + op.kind === ir.OpKind.AnimationListener || + op.kind === ir.OpKind.TwoWayListener + ) { op.handlerOps.prepend(generateTemporaries(op.handlerOps) as ir.UpdateOp[]); } else if (op.kind === ir.OpKind.RepeaterCreate && op.trackByOps !== null) { op.trackByOps.prepend(generateTemporaries(op.trackByOps) as ir.UpdateOp[]); diff --git a/packages/compiler/src/template/pipeline/src/phases/variable_optimization.ts b/packages/compiler/src/template/pipeline/src/phases/variable_optimization.ts index f3afc57d285..92fe0297f13 100644 --- a/packages/compiler/src/template/pipeline/src/phases/variable_optimization.ts +++ b/packages/compiler/src/template/pipeline/src/phases/variable_optimization.ts @@ -34,7 +34,12 @@ export function optimizeVariables(job: CompilationJob): void { inlineAlwaysInlineVariables(unit.update); for (const op of unit.create) { - if (op.kind === ir.OpKind.Listener || op.kind === ir.OpKind.TwoWayListener) { + if ( + op.kind === ir.OpKind.Listener || + op.kind === ir.OpKind.Animation || + op.kind === ir.OpKind.AnimationListener || + op.kind === ir.OpKind.TwoWayListener + ) { inlineAlwaysInlineVariables(op.handlerOps); } else if (op.kind === ir.OpKind.RepeaterCreate && op.trackByOps !== null) { inlineAlwaysInlineVariables(op.trackByOps); @@ -45,7 +50,12 @@ export function optimizeVariables(job: CompilationJob): void { optimizeVariablesInOpList(unit.update, job.compatibility); for (const op of unit.create) { - if (op.kind === ir.OpKind.Listener || op.kind === ir.OpKind.TwoWayListener) { + if ( + op.kind === ir.OpKind.Listener || + op.kind === ir.OpKind.Animation || + op.kind === ir.OpKind.AnimationListener || + op.kind === ir.OpKind.TwoWayListener + ) { optimizeVariablesInOpList(op.handlerOps, job.compatibility); } else if (op.kind === ir.OpKind.RepeaterCreate && op.trackByOps !== null) { optimizeVariablesInOpList(op.trackByOps, job.compatibility); diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts index f250b0b9596..6c4e6ad914a 100644 --- a/packages/compiler/src/template_parser/binding_parser.ts +++ b/packages/compiler/src/template_parser/binding_parser.ts @@ -39,6 +39,7 @@ import {splitAtColon, splitAtPeriod} from '../util'; const PROPERTY_PARTS_SEPARATOR = '.'; const ATTRIBUTE_PREFIX = 'attr'; +const ANIMATE_PREFIX = 'animate'; const CLASS_PREFIX = 'class'; const STYLE_PREFIX = 'style'; const TEMPLATE_ATTR_PREFIX = '*'; @@ -399,6 +400,16 @@ export class BindingParser { targetMatchableAttrs, targetProps, ); + } else if (name.startsWith(ANIMATE_PREFIX)) { + this._parseAnimation( + name, + this.parseBinding(expression, isHost, valueSpan || sourceSpan, absoluteOffset), + sourceSpan, + keySpan, + valueSpan, + targetMatchableAttrs, + targetProps, + ); } else { this._parsePropertyAst( name, @@ -463,6 +474,21 @@ export class BindingParser { ); } + private _parseAnimation( + name: string, + ast: ASTWithSource, + sourceSpan: ParseSourceSpan, + keySpan: ParseSourceSpan, + valueSpan: ParseSourceSpan | undefined, + targetMatchableAttrs: string[][], + targetProps: ParsedProperty[], + ) { + targetMatchableAttrs.push([name, ast.source!]); + targetProps.push( + new ParsedProperty(name, ast, ParsedPropertyType.ANIMATION, sourceSpan, keySpan, valueSpan), + ); + } + private _parseLegacyAnimation( name: string, expression: string | null, @@ -585,6 +611,10 @@ export class BindingParser { boundPropertyName = parts[1]; bindingType = BindingType.Style; securityContexts = [SecurityContext.STYLE]; + } else if (parts[0] == ANIMATE_PREFIX) { + boundPropertyName = boundProp.name; + bindingType = BindingType.Animation; + securityContexts = [SecurityContext.NONE]; } } @@ -617,7 +647,6 @@ export class BindingParser { ); } - // TODO: keySpan should be required but was made optional to avoid changing VE parser. parseEvent( name: string, expression: string, @@ -744,16 +773,16 @@ export class BindingParser { this._reportError('Unsupported expression in a two-way binding', sourceSpan); } + let eventType = ParsedEventType.Regular; + if (isAssignmentEvent) { + eventType = ParsedEventType.TwoWay; + } + if (name.startsWith(ANIMATE_PREFIX)) { + eventType = ParsedEventType.Animation; + } + targetEvents.push( - new ParsedEvent( - eventName, - target, - isAssignmentEvent ? ParsedEventType.TwoWay : ParsedEventType.Regular, - ast, - sourceSpan, - handlerSpan, - keySpan, - ), + new ParsedEvent(eventName, target, eventType, ast, sourceSpan, handlerSpan, keySpan), ); // Don't detect directives for event names for now, // so don't add the event name to the matchableAttrs diff --git a/packages/compiler/test/ml_parser/html_parser_spec.ts b/packages/compiler/test/ml_parser/html_parser_spec.ts index a893ae141f5..8e5fe221cb7 100644 --- a/packages/compiler/test/ml_parser/html_parser_spec.ts +++ b/packages/compiler/test/ml_parser/html_parser_spec.ts @@ -301,10 +301,8 @@ describe('HtmlParser', () => { expect( humanizeDom( parser.parse( - ``, + `` + + ``, 'TestComp', ), ), @@ -313,13 +311,9 @@ describe('HtmlParser', () => { [ html.Attribute, '[attr]', - `[ - {text: 'some text',url:'//www.google.com'}, - {text:'other text',url:'//www.google.com'}]`, + `[\n {text: 'some text',url:'//www.google.com'},\n {text:'other text',url:'//www.google.com'}]`, [ - `[ - {text: 'some text',url:'//www.google.com'}, - {text:'other text',url:'//www.google.com'}]`, + `[\n {text: 'some text',url:'//www.google.com'},\n {text:'other text',url:'//www.google.com'}]`, ], ], ]); @@ -343,10 +337,8 @@ describe('HtmlParser', () => { expect( humanizeDom( parser.parse( - ``, + `` + + ``, 'TestComp', ), ), @@ -355,13 +347,9 @@ describe('HtmlParser', () => { [ html.Attribute, '[attr]', - `[ - {text: 'some text',url:'//www.google.com'}, - {text:'other text',url:'//www.google.com'}]`, + `[\n {text: 'some text',url:'//www.google.com'},\n {text:'other text',url:'//www.google.com'}]`, [ - `[ - {text: 'some text',url:'//www.google.com'}, - {text:'other text',url:'//www.google.com'}]`, + `[\n {text: 'some text',url:'//www.google.com'},\n {text:'other text',url:'//www.google.com'}]`, ], ], ]); @@ -431,6 +419,89 @@ describe('HtmlParser', () => { ]); expect(humanizeErrors(errors)).toEqual([]); }); + + describe('animate instructions', () => { + it('should parse animate.enter as a static attribute', () => { + expect(humanizeDom(parser.parse(`
`, 'TestComp'))).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, 'animate.enter', 'foo', ['foo']], + ]); + }); + + it('should parse animate.leave as a static attribute', () => { + expect(humanizeDom(parser.parse(`
`, 'TestComp'))).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, 'animate.leave', 'bar', ['bar']], + ]); + }); + + it('should parse both animate.enter and animate.leave as static attributes', () => { + expect( + humanizeDom( + parser.parse(`
`, 'TestComp'), + ), + ).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, 'animate.enter', 'foo', ['foo']], + [html.Attribute, 'animate.leave', 'bar', ['bar']], + ]); + }); + + it('should parse animate.enter as a property binding', () => { + expect( + humanizeDom(parser.parse(`
`, 'TestComp')), + ).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, '[animate.enter]', `'foo'`, [`'foo'`]], + ]); + }); + + it('should parse animate.leave as a property binding with a string array', () => { + expect( + humanizeDom(parser.parse(`
`, 'TestComp')), + ).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, '[animate.leave]', `['bar', 'baz']`, [`['bar', 'baz']`]], + ]); + }); + + it('should parse animate.enter as an event binding', () => { + expect( + humanizeDom( + parser.parse(`
`, 'TestComp'), + ), + ).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, '(animate.enter)', 'onAnimation($event)', ['onAnimation($event)']], + ]); + }); + + it('should parse animate.leave as an event binding', () => { + expect( + humanizeDom( + parser.parse(`
`, 'TestComp'), + ), + ).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, '(animate.leave)', 'onAnimation($event)', ['onAnimation($event)']], + ]); + }); + + it('should parse a combination of animate property and event bindings', () => { + expect( + humanizeDom( + parser.parse( + `
`, + 'TestComp', + ), + ), + ).toEqual([ + [html.Element, 'div', 0], + [html.Attribute, '[animate.enter]', `'foo'`, [`'foo'`]], + [html.Attribute, '(animate.leave)', 'onAnimation($event)', ['onAnimation($event)']], + ]); + }); + }); }); describe('comments', () => { diff --git a/packages/compiler/test/render3/r3_template_transform_spec.ts b/packages/compiler/test/render3/r3_template_transform_spec.ts index f4857b4e75b..f7ffbe28776 100644 --- a/packages/compiler/test/render3/r3_template_transform_spec.ts +++ b/packages/compiler/test/render3/r3_template_transform_spec.ts @@ -377,6 +377,42 @@ describe('R3 template transform', () => { }); }); + describe('animation bindings', () => { + it('should support animate.enter', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['TextAttribute', 'animate.enter', 'foo'], + ]); + + expectFromHtml(`
`).toEqual([ + ['Element', 'div'], + ['BoundAttribute', 6, 'animate.enter', '["foo", "bar"]'], + ]); + + expectFromHtml(`
`).toEqual([ + ['Element', 'div'], + ['BoundEvent', 3, 'animate.enter', null, 'animateFn($event)'], + ]); + }); + + it('should support animate.leave', () => { + expectFromHtml('
').toEqual([ + ['Element', 'div'], + ['TextAttribute', 'animate.leave', 'foo'], + ]); + + expectFromHtml(`
`).toEqual([ + ['Element', 'div'], + ['BoundAttribute', 6, 'animate.leave', '["foo", "bar"]'], + ]); + + expectFromHtml(`
`).toEqual([ + ['Element', 'div'], + ['BoundEvent', 3, 'animate.leave', null, 'animateFn($event)'], + ]); + }); + }); + describe('templates', () => { it('should support * directives', () => { expectFromHtml('
').toEqual([ diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index bbfcb64e33f..b1baf2d6fe8 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -232,6 +232,10 @@ export { ɵɵreplaceMetadata, ɵɵgetReplaceMetadataURL, ɵɵattachSourceLocations, + ɵɵanimateEnter, + ɵɵanimateEnterListener, + ɵɵanimateLeave, + ɵɵanimateLeaveListener, } from './render3/index'; export {CONTAINER_HEADER_OFFSET as ɵCONTAINER_HEADER_OFFSET} from './render3/interfaces/container'; export {LContext as ɵLContext} from './render3/interfaces/context'; diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 3c24ae146df..8689f44fecf 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -159,6 +159,10 @@ export { ɵɵstoreLet, ɵɵreadContextLet, ɵɵattachSourceLocations, + ɵɵanimateEnter, + ɵɵanimateEnterListener, + ɵɵanimateLeave, + ɵɵanimateLeaveListener, } from './instructions/all'; export { ɵɵdeferEnableTimerScheduling, diff --git a/packages/core/src/render3/instructions/all.ts b/packages/core/src/render3/instructions/all.ts index e5f99bb5600..6b80a409711 100644 --- a/packages/core/src/render3/instructions/all.ts +++ b/packages/core/src/render3/instructions/all.ts @@ -28,6 +28,7 @@ export * from '../../defer/instructions'; export * from './advance'; export * from './attribute'; +export * from './animation'; export * from './change_detection'; export * from './component_instance'; export * from './control_flow'; diff --git a/packages/core/src/render3/instructions/animation.ts b/packages/core/src/render3/instructions/animation.ts new file mode 100644 index 00000000000..d39cd8cdfcd --- /dev/null +++ b/packages/core/src/render3/instructions/animation.ts @@ -0,0 +1,23 @@ +/** + * @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 + */ + +export function ɵɵanimateLeave(): typeof ɵɵanimateLeave { + throw new Error('Not Implemented'); +} + +export function ɵɵanimateLeaveListener(): typeof ɵɵanimateLeaveListener { + throw new Error('Not Implemented'); +} + +export function ɵɵanimateEnter(): typeof ɵɵanimateEnter { + throw new Error('Not Implemented'); +} + +export function ɵɵanimateEnterListener(): typeof ɵɵanimateEnterListener { + throw new Error('Not Implemented'); +} diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 8508d0d039f..aca12b3b7c8 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -20,6 +20,10 @@ import * as r3 from '../index'; * This should be kept up to date with the public exports of @angular/core. */ export const angularCoreEnv: {[name: string]: unknown} = (() => ({ + 'ɵɵanimateEnter': r3.ɵɵanimateEnter, + 'ɵɵanimateEnterListener': r3.ɵɵanimateEnterListener, + 'ɵɵanimateLeave': r3.ɵɵanimateLeave, + 'ɵɵanimateLeaveListener': r3.ɵɵanimateLeaveListener, 'ɵɵattribute': r3.ɵɵattribute, 'ɵɵdefineComponent': r3.ɵɵdefineComponent, 'ɵɵdefineDirective': r3.ɵɵdefineDirective,