refactor(core): add compiler support for animation instructions (#62528)

this adds the compiler code to support the animate instructions.

PR Close #62528
This commit is contained in:
Jessica Janiuk
2025-06-30 17:24:23 +02:00
committed by Kirill Cherkashin
parent e25e6342f2
commit fc8247de95
65 changed files with 1925 additions and 149 deletions
@@ -71,6 +71,8 @@ class InterpolatedSignalCheck extends TemplateCheckWithVisitor<ErrorCode.INTERPO
node.type === BindingType.Style ||
// or an attribute binding like `[attr.role]="mySignal"`
node.type === BindingType.Attribute ||
// or an animation binding like `[animate.enter]="mySignal"`
node.type === BindingType.Animation ||
// or an animation binding like `[@myAnimation]="mySignal"`
node.type === BindingType.LegacyAnimation) &&
nodeAst
@@ -192,7 +192,6 @@ function createNodeFromHostLiteralProperty(
const {attrName, type} = inferBoundAttribute(name.text.slice(1, -1));
const valueSpan = createStaticExpressionSpan(initializer);
const ast = parser.parseBinding(initializer.text, true, valueSpan, valueSpan.start.offset);
if (ast.errors.length > 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;
@@ -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) {
@@ -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 = '<p (animate.enter)="animateFn($event)"></p>';
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) })');
@@ -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: '<div @attr [@binding]="exp"></div>', 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: `
<div>
<p animate.enter="slide">Sliding Content</p>
</div>
`, 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: '<div @attr [@binding]="exp"></div>',
standalone: false
selector: 'my-component',
template: `
<div>
<p animate.enter="slide">Sliding Content</p>
</div>
`,
}]
}] });
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<MyApp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never, false, never>;
}
export declare class MyModule {
static ɵfac: i0.ɵɵFactoryDeclaration<MyModule, never>;
static ɵmod: i0.ɵɵNgModuleDeclaration<MyModule, [typeof MyApp], never, never>;
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
export declare class MyComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* 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: '<div (@mySelector.start)="false" (@mySelector.done)="false" [@mySelector]="0"></div>', 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: `<p>Sliding Content</p>`, 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: '<div (@mySelector.start)="false" (@mySelector.done)="false" [@mySelector]="0"></div>',
standalone: false
selector: 'child-component',
host: { 'animate.enter': 'fade' },
template: `<p>Sliding Content</p>`,
}]
}] });
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: `
<child-component animate.enter="slide"></child-component>
`, 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: `
<child-component animate.enter="slide"></child-component>
`,
}]
}] });
/****************************************************************************************************
* 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<MyApp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never, false, never>;
export declare class ChildComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<ChildComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<ChildComponent, "child-component", never, {}, {}, never, never, true, never>;
}
export declare class MyModule {
static ɵfac: i0.ɵɵFactoryDeclaration<MyModule, never>;
static ɵmod: i0.ɵɵNgModuleDeclaration<MyModule, [typeof MyApp], never, never>;
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
export declare class MyComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* 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: `
<div>
<p [animate.enter]="enterClass()">Sliding Content</p>
</div>
`, 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: `
<div>
<p [animate.enter]="enterClass()">Sliding Content</p>
</div>
`,
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: animate_enter_with_binding.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyComponent {
enterClass: import("@angular/core").WritableSignal<string>;
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* 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: `
<div>
<p (animate.enter)="slideFn($event)">Sliding Content</p>
</div>
`, 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: `
<div>
<p (animate.enter)="slideFn($event)">Sliding Content</p>
</div>
`,
}]
}] });
/****************************************************************************************************
* 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<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* 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: `
<div>
<p animate.leave="fade">Fading Content</p>
</div>
`, 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: `
<div>
<p animate.leave="fade">Fading Content</p>
</div>
`,
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: animate_leave_with_string.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* 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: `
<div>
<p [animate.leave]="leaveClass()">Fading Content</p>
</div>
`, 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: `
<div>
<p [animate.leave]="leaveClass()">Fading Content</p>
</div>
`,
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: animate_leave_with_binding.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class MyComponent {
leaveClass: import("@angular/core").WritableSignal<string>;
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* 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: `
<div>
<p (animate.leave)="fadeFn($event)">Fading Content</p>
</div>
`, 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: `
<div>
<p (animate.leave)="fadeFn($event)">Fading Content</p>
</div>
`,
}]
}] });
/****************************************************************************************************
* 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<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* 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: `<p>Fading Content</p>`, 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: `<p>Fading Content</p>`,
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: `
<child-component animate.leave="slide"></child-component>
`, 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: `
<child-component animate.leave="slide"></child-component>
`,
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: animate_leave_with_string_host_bindings.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class ChildComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<ChildComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<ChildComponent, "child-component", never, {}, {}, never, never, true, never>;
}
export declare class MyComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<MyComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyComponent, "my-component", never, {}, {}, never, never, true, never>;
}
@@ -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"
}
]
}
]
}
}
@@ -0,0 +1,13 @@
import {Component, signal} from '@angular/core';
@Component({
selector: 'my-component',
template: `
<div>
<p [animate.enter]="enterClass()">Sliding Content</p>
</div>
`,
})
export class MyComponent {
enterClass = signal('slide');
}
@@ -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 });
@@ -0,0 +1,15 @@
import {Component} from '@angular/core';
@Component({
selector: 'my-component',
template: `
<div>
<p (animate.enter)="slideFn($event)">Sliding Content</p>
</div>
`,
})
export class MyComponent {
slideFn(event: any) {
event.target.classList.add('slide-in');
}
}
@@ -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 });
@@ -0,0 +1,12 @@
import {Component} from '@angular/core';
@Component({
selector: 'my-component',
template: `
<div>
<p animate.enter="slide">Sliding Content</p>
</div>
`,
})
export class MyComponent {
}
@@ -0,0 +1,19 @@
import {Component} from '@angular/core';
@Component({
selector: 'child-component',
host: {'animate.enter': 'fade'},
template: `<p>Sliding Content</p>`,
})
export class ChildComponent {
}
@Component({
selector: 'my-component',
imports: [ChildComponent],
template: `
<child-component animate.enter="slide"></child-component>
`,
})
export class MyComponent {
}
@@ -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 });
@@ -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
});
@@ -0,0 +1,13 @@
import {Component, signal} from '@angular/core';
@Component({
selector: 'my-component',
template: `
<div>
<p [animate.leave]="leaveClass()">Fading Content</p>
</div>
`,
})
export class MyComponent {
leaveClass = signal('fade');
}
@@ -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 });
@@ -0,0 +1,15 @@
import {Component} from '@angular/core';
@Component({
selector: 'my-component',
template: `
<div>
<p (animate.leave)="fadeFn($event)">Fading Content</p>
</div>
`,
})
export class MyComponent {
fadeFn(event: any) {
event.target.classList.add('fade-out');
}
}
@@ -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 });
@@ -0,0 +1,12 @@
import {Component} from '@angular/core';
@Component({
selector: 'my-component',
template: `
<div>
<p animate.leave="fade">Fading Content</p>
</div>
`,
})
export class MyComponent {
}
@@ -0,0 +1,19 @@
import {Component} from '@angular/core';
@Component({
selector: 'child-component',
template: `<p>Fading Content</p>`,
host: {'animate.leave': 'fade'},
})
export class ChildComponent {
}
@Component({
selector: 'my-component',
imports: [ChildComponent],
template: `
<child-component animate.leave="slide"></child-component>
`,
})
export class MyComponent {
}
@@ -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 });
@@ -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 });
@@ -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: `
<div>
<p (animate.leave)="fadeFn($event)">Fading Content</p>
</div>
`,
}]
}], null, null); })();
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MyComponent, { className: "MyComponent", filePath: "animate_out_with_event_listener.ts", lineNumber: 11 }); })();
@@ -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: '<div @attr [@binding]="exp"></div>', 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: '<div @attr [@binding]="exp"></div>',
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<MyApp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never, false, never>;
}
export declare class MyModule {
static ɵfac: i0.ɵɵFactoryDeclaration<MyModule, never>;
static ɵmod: i0.ɵɵNgModuleDeclaration<MyModule, [typeof MyApp], never, never>;
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
}
/****************************************************************************************************
* 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: '<div (@mySelector.start)="false" (@mySelector.done)="false" [@mySelector]="0"></div>', 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: '<div (@mySelector.start)="false" (@mySelector.done)="false" [@mySelector]="0"></div>',
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<MyApp, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "my-app", never, {}, {}, never, never, false, never>;
}
export declare class MyModule {
static ɵfac: i0.ɵɵFactoryDeclaration<MyModule, never>;
static ɵmod: i0.ɵɵNgModuleDeclaration<MyModule, [typeof MyApp], never, never>;
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
}
@@ -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"
}
]
}
]
}
@@ -187,6 +187,258 @@ runInEachFileSystem((os: string) => {
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration<Service, never>;');
});
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:
'<div>@if (show()) {<p (animate.enter)="animateFn($event)">I should slide in</p>}</div>',
})
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:
'<div>@if (show()) {<p [animate.enter]="fade()">I should slide in</p>}</div>',
})
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:
'<div>@if (show()) {<p [animate.enter]="classList">I should slide in</p>}</div>',
})
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:
'<div>@if (show()) {<p animate.enter="fade">I should slide in</p>}</div>',
})
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:
'<div><p>I should slide in</p></div>',
})
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:
'<div>@if (show()) {<p (animate.leave)="animateFn($event)">I should slide out</p>}</div>',
})
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:
'<div>@if (show()) {<p [animate.leave]="fade()">I should slide out</p>}</div>',
})
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:
'<div>@if (show()) {<p [animate.leave]="classList">I should slide out</p>}</div>',
})
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:
'<div>@if (show()) {<p animate.leave="fade">I should slide out</p>}</div>',
})
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',
@@ -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 {
@@ -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};
@@ -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(
@@ -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.
*/
@@ -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) {
@@ -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<CreateOp> {
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<CreateOp> {
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<UpdateOp>;
/**
* 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<UpdateOp>,
securityContext: SecurityContext | SecurityContext[],
sourceSpan: ParseSourceSpan,
): AnimationOp {
const handlerOps = new OpList<UpdateOp>();
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<CreateOp> {
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<UpdateOp>;
/**
* 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<UpdateOp>,
animationKind: AnimationKind,
eventTarget: string | null,
hostListener: boolean,
sourceSpan: ParseSourceSpan,
): AnimationListenerOp {
const handlerList = new OpList<UpdateOp>();
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.
@@ -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<UpdateOp>, 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<UpdateOp>, 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,
@@ -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<UpdateOp>, 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<UpdateOp> {
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<UpdateOp>, DependsOnSlotContextOpTrait, ConsumesVarsTrait {
kind: OpKind.DeferWhen;
@@ -197,7 +197,12 @@ export abstract class CompilationUnit {
*ops(): Generator<ir.CreateOp | ir.UpdateOp> {
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;
}
@@ -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},
@@ -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, ir.BindingKind>([
[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<ir.BindingOp | ir.ExtractedAttributeOp | null>();
let i18nAttributeBindingNames = new Set<string>();
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<ir.BindingOp | ir.ExtractedAttributeOp | null>();
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(
@@ -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,
@@ -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
@@ -47,6 +47,19 @@ export function specializeBindings(job: CompilationJob): void {
ir.OpList.remove<ir.UpdateOp>(op);
const target = lookupElement(elements, op.target);
target.nonBindable = true;
} else if (op.name.startsWith('animate.')) {
ir.OpList.replace<ir.UpdateOp>(
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<ir.UpdateOp>(
@@ -66,6 +79,20 @@ export function specializeBindings(job: CompilationJob): void {
);
}
break;
case ir.BindingKind.Animation:
ir.OpList.replace<ir.UpdateOp>(
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,
@@ -42,6 +42,10 @@ const CHAIN_COMPATIBILITY = new Map<o.ExternalReference, o.ExternalReference>([
[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],
]);
/**
@@ -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<ir.XrefId, ir.ElementOrContainerOps>,
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<ir.XrefId, ir.ElementOrContainerOps>();
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<ir.CreateOp>(createAnimationOp, lookupElement(elements, op.target));
}
ir.OpList.remove<ir.UpdateOp>(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,
);
}
}
@@ -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<ir.UpdateOp>[] {
const newOps: ir.VariableOp<ir.UpdateOp>[] = [];
@@ -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<ir.UpdateOp>(
@@ -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;
@@ -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);
}
}
@@ -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<Rule<ir.UpdateOp>> = [
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,
]);
/**
@@ -258,6 +258,37 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList<ir.CreateOp
case ir.OpKind.DeclareLet:
ir.OpList.replace(op, ng.declareLet(op.handle.slot!, op.sourceSpan));
break;
case ir.OpKind.AnimationString:
ir.OpList.replace(
op,
ng.animationString(op.animationKind, op.expression, op.sanitizer, op.sourceSpan),
);
break;
case ir.OpKind.Animation:
const animationCallbackFn = reifyListenerHandler(
unit,
op.handlerFnName!,
op.handlerOps,
/* consumesDollarEvent */ false,
);
ir.OpList.replace(
op,
ng.animation(op.animationKind, animationCallbackFn, op.sanitizer, op.sourceSpan),
);
break;
case ir.OpKind.AnimationListener:
const animationListenerFn = reifyListenerHandler(
unit,
op.handlerFnName!,
op.handlerOps,
op.consumesDollarEvent,
);
ir.OpList.replace(
op,
ng.animationListener(op.animationKind, animationListenerFn, null, op.sourceSpan),
);
break;
case ir.OpKind.Listener:
const listenerFn = reifyListenerHandler(
unit,
@@ -561,7 +592,9 @@ function reifyUpdateOperations(unit: CompilationUnit, ops: ir.OpList<ir.UpdateOp
case ir.OpKind.Property:
ir.OpList.replace(
op,
unit.job.mode === TemplateCompilationMode.DomOnly && !op.isLegacyAnimationTrigger
unit.job.mode === TemplateCompilationMode.DomOnly &&
op.bindingKind !== ir.BindingKind.LegacyAnimation &&
op.bindingKind !== ir.BindingKind.Animation
? ng.domProperty(
DOM_PROPERTY_REMAPPING.get(op.name) ?? op.name,
op.expression,
@@ -611,7 +644,10 @@ function reifyUpdateOperations(unit: CompilationUnit, ops: ir.OpList<ir.UpdateOp
if (op.expression instanceof ir.Interpolation) {
throw new Error('not yet handled');
} else {
if (op.isLegacyAnimationTrigger) {
if (
op.bindingKind === ir.BindingKind.LegacyAnimation ||
op.bindingKind === ir.BindingKind.Animation
) {
ir.OpList.replace(op, ng.syntheticHostProperty(op.name, op.expression, op.sourceSpan));
} else {
ir.OpList.replace(
@@ -8,12 +8,7 @@
import * as o from '../../../../output/output_ast';
import * as ir from '../../ir';
import {
CompilationJob,
CompilationUnit,
ComponentCompilationJob,
ViewCompilationUnit,
} from '../compilation';
import {CompilationJob, CompilationUnit} from '../compilation';
/**
* Resolves `ir.ContextExpr` expressions (which represent embedded view or component contexts) to
@@ -47,6 +42,8 @@ function processLexicalScope(
break;
}
break;
case ir.OpKind.Animation:
case ir.OpKind.AnimationListener:
case ir.OpKind.Listener:
case ir.OpKind.TwoWayListener:
processLexicalScope(view, op.handlerOps);
@@ -23,13 +23,17 @@ export function resolveDollarEvent(job: CompilationJob): void {
function transformDollarEvent(ops: ir.OpList<ir.CreateOp> | ir.OpList<ir.UpdateOp>): 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);
@@ -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;
}
@@ -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<ir.UpdateOp>(
@@ -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[]);
@@ -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);
@@ -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
@@ -301,10 +301,8 @@ describe('HtmlParser', () => {
expect(
humanizeDom(
parser.parse(
`<app-component
[attr]="[
{text: 'some text',url:'//www.google.com'},
{text:'other text',url:'//www.google.com'}]"></app-component>`,
`<app-component\n [attr]="[\n {text: 'some text',url:'//www.google.com'},\n {text:'other text',url:'//www.google.com'}]">` +
`</app-component>`,
'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(
`<app-component
[attr]="[
{text: 'some text',url:'//www.google.com'},
{text:'other text',url:'//www.google.com'}]"></app-component>`,
`<app-component\n [attr]="[\n {text: 'some text',url:'//www.google.com'},\n {text:'other text',url:'//www.google.com'}]">` +
`</app-component>`,
'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(`<div animate.enter="foo"></div>`, '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(`<div animate.leave="bar"></div>`, '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(`<div animate.enter="foo" animate.leave="bar"></div>`, '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(`<div [animate.enter]="'foo'"></div>`, '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(`<div [animate.leave]="['bar', 'baz']"></div>`, '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(`<div (animate.enter)="onAnimation($event)"></div>`, '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(`<div (animate.leave)="onAnimation($event)"></div>`, '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(
`<div [animate.enter]="'foo'" (animate.leave)="onAnimation($event)"></div>`,
'TestComp',
),
),
).toEqual([
[html.Element, 'div', 0],
[html.Attribute, '[animate.enter]', `'foo'`, [`'foo'`]],
[html.Attribute, '(animate.leave)', 'onAnimation($event)', ['onAnimation($event)']],
]);
});
});
});
describe('comments', () => {
@@ -377,6 +377,42 @@ describe('R3 template transform', () => {
});
});
describe('animation bindings', () => {
it('should support animate.enter', () => {
expectFromHtml('<div animate.enter="foo"></div>').toEqual([
['Element', 'div'],
['TextAttribute', 'animate.enter', 'foo'],
]);
expectFromHtml(`<div [animate.enter]="['foo', 'bar']"></div>`).toEqual([
['Element', 'div'],
['BoundAttribute', 6, 'animate.enter', '["foo", "bar"]'],
]);
expectFromHtml(`<div (animate.enter)="animateFn($event)"></div>`).toEqual([
['Element', 'div'],
['BoundEvent', 3, 'animate.enter', null, 'animateFn($event)'],
]);
});
it('should support animate.leave', () => {
expectFromHtml('<div animate.leave="foo"></div>').toEqual([
['Element', 'div'],
['TextAttribute', 'animate.leave', 'foo'],
]);
expectFromHtml(`<div [animate.leave]="['foo', 'bar']"></div>`).toEqual([
['Element', 'div'],
['BoundAttribute', 6, 'animate.leave', '["foo", "bar"]'],
]);
expectFromHtml(`<div (animate.leave)="animateFn($event)"></div>`).toEqual([
['Element', 'div'],
['BoundEvent', 3, 'animate.leave', null, 'animateFn($event)'],
]);
});
});
describe('templates', () => {
it('should support * directives', () => {
expectFromHtml('<div *ngIf></div>').toEqual([
@@ -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';
+4
View File
@@ -159,6 +159,10 @@ export {
ɵɵstoreLet,
ɵɵreadContextLet,
ɵɵattachSourceLocations,
ɵɵanimateEnter,
ɵɵanimateEnterListener,
ɵɵanimateLeave,
ɵɵanimateLeaveListener,
} from './instructions/all';
export {
ɵɵdeferEnableTimerScheduling,
@@ -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';
@@ -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');
}
@@ -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,