mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Kirill Cherkashin
parent
e25e6342f2
commit
fc8247de95
+2
@@ -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) })');
|
||||
|
||||
+283
-52
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
+113
-11
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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');
|
||||
}
|
||||
+6
@@ -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 });
|
||||
+15
@@ -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');
|
||||
}
|
||||
}
|
||||
+6
@@ -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 });
|
||||
+12
@@ -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 {
|
||||
}
|
||||
+19
@@ -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 {
|
||||
}
|
||||
+13
@@ -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 });
|
||||
+15
@@ -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
|
||||
});
|
||||
+13
@@ -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');
|
||||
}
|
||||
+6
@@ -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 });
|
||||
+15
@@ -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');
|
||||
}
|
||||
}
|
||||
+6
@@ -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 });
|
||||
+12
@@ -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 {
|
||||
}
|
||||
+19
@@ -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 {
|
||||
}
|
||||
+13
@@ -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 });
|
||||
+6
@@ -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 }); })();
|
||||
+83
@@ -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>;
|
||||
}
|
||||
|
||||
+39
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user