fix(ivy): generate proper event listener names for animation events (FW-800) (#27525)
Prior to this change, animation event names were treated as a regular event names, stripping `@` symbol and event phase. As a result, event listeners were not invoked during animations. Now animation event name is formatted as needed and the necessary callbacks are invoked. PR Close #27525
This commit is contained in:
parent
6316051967
commit
dcb44e6dea
|
@ -239,6 +239,64 @@ describe('compiler compliance: styling', () => {
|
||||||
const result = compile(files, angularFiles);
|
const result = compile(files, angularFiles);
|
||||||
expectEmit(result.source, template, 'Incorrect template');
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should generate animation listeners', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-cmp',
|
||||||
|
template: \`
|
||||||
|
<div [@myAnimation]="exp"
|
||||||
|
(@myAnimation.start)="onStart($event)"
|
||||||
|
(@myAnimation.done)="onDone($event)"></div>
|
||||||
|
\`,
|
||||||
|
animations: [trigger(
|
||||||
|
'myAnimation',
|
||||||
|
[transition(
|
||||||
|
'* => state',
|
||||||
|
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||||
|
})
|
||||||
|
class MyComponent {
|
||||||
|
exp: any;
|
||||||
|
startEvent: any;
|
||||||
|
doneEvent: any;
|
||||||
|
onStart(event: any) { this.startEvent = event; }
|
||||||
|
onDone(event: any) { this.doneEvent = event; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComponent]})
|
||||||
|
export class MyModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
…
|
||||||
|
MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
|
||||||
|
…
|
||||||
|
consts: 1,
|
||||||
|
vars: 1,
|
||||||
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelementStart(0, "div", _c0);
|
||||||
|
$r3$.ɵlistener("@myAnimation.start", function MyComponent_Template_div__myAnimation_start_listener($event) { return ctx.onStart($event); });
|
||||||
|
$r3$.ɵlistener("@myAnimation.done", function MyComponent_Template_div__myAnimation_done_listener($event) { return ctx.onDone($event); });
|
||||||
|
$r3$.ɵelementEnd();
|
||||||
|
} if (rf & 2) {
|
||||||
|
$r3$.ɵelementProperty(0, "@myAnimation", $r3$.ɵbind(ctx.exp));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
encapsulation: 2,
|
||||||
|
…
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('[style] and [style.prop]', () => {
|
describe('[style] and [style.prop]', () => {
|
||||||
|
|
|
@ -49,14 +49,14 @@ export class BoundAttribute implements Node {
|
||||||
|
|
||||||
export class BoundEvent implements Node {
|
export class BoundEvent implements Node {
|
||||||
constructor(
|
constructor(
|
||||||
public name: string, public handler: AST, public target: string|null,
|
public name: string, public type: ParsedEventType, public handler: AST,
|
||||||
public phase: string|null, public sourceSpan: ParseSourceSpan) {}
|
public target: string|null, public phase: string|null, public sourceSpan: ParseSourceSpan) {}
|
||||||
|
|
||||||
static fromParsedEvent(event: ParsedEvent) {
|
static fromParsedEvent(event: ParsedEvent) {
|
||||||
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
|
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
|
||||||
const phase: string|null =
|
const phase: string|null =
|
||||||
event.type === ParsedEventType.Animation ? event.targetOrPhase : null;
|
event.type === ParsedEventType.Animation ? event.targetOrPhase : null;
|
||||||
return new BoundEvent(event.name, event.handler, target, phase, event.sourceSpan);
|
return new BoundEvent(event.name, event.type, event.handler, target, phase, event.sourceSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundEvent(this); }
|
visit<Result>(visitor: Visitor<Result>): Result { return visitor.visitBoundEvent(this); }
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {flatten, sanitizeIdentifier} from '../../compile_metadata';
|
||||||
import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
|
import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
|
||||||
import {ConstantPool} from '../../constant_pool';
|
import {ConstantPool} from '../../constant_pool';
|
||||||
import * as core from '../../core';
|
import * as core from '../../core';
|
||||||
import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, PropertyRead} from '../../expression_parser/ast';
|
import {AST, AstMemoryEfficientTransformer, BindingPipe, BindingType, FunctionCall, ImplicitReceiver, Interpolation, LiteralArray, LiteralMap, LiteralPrimitive, ParsedEventType, PropertyRead} from '../../expression_parser/ast';
|
||||||
import {Lexer} from '../../expression_parser/lexer';
|
import {Lexer} from '../../expression_parser/lexer';
|
||||||
import {Parser} from '../../expression_parser/parser';
|
import {Parser} from '../../expression_parser/parser';
|
||||||
import * as i18n from '../../i18n/i18n_ast';
|
import * as i18n from '../../i18n/i18n_ast';
|
||||||
|
@ -987,7 +987,11 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
private prepareListenerParameter(tagName: string, outputAst: t.BoundEvent): () => o.Expression[] {
|
private prepareListenerParameter(tagName: string, outputAst: t.BoundEvent): () => o.Expression[] {
|
||||||
const evNameSanitized = sanitizeIdentifier(outputAst.name);
|
let eventName: string = outputAst.name;
|
||||||
|
if (outputAst.type === ParsedEventType.Animation) {
|
||||||
|
eventName = prepareSyntheticAttributeName(`${outputAst.name}.${outputAst.phase}`);
|
||||||
|
}
|
||||||
|
const evNameSanitized = sanitizeIdentifier(eventName);
|
||||||
const tagNameSanitized = sanitizeIdentifier(tagName);
|
const tagNameSanitized = sanitizeIdentifier(tagName);
|
||||||
const functionName = `${this.templateName}_${tagNameSanitized}_${evNameSanitized}_listener`;
|
const functionName = `${this.templateName}_${tagNameSanitized}_${evNameSanitized}_listener`;
|
||||||
|
|
||||||
|
@ -1007,8 +1011,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||||
const handler = o.fn(
|
const handler = o.fn(
|
||||||
[new o.FnParam('$event', o.DYNAMIC_TYPE)], statements, o.INFERRED_TYPE, null,
|
[new o.FnParam('$event', o.DYNAMIC_TYPE)], statements, o.INFERRED_TYPE, null,
|
||||||
functionName);
|
functionName);
|
||||||
|
return [o.literal(eventName), handler];
|
||||||
return [o.literal(outputAst.name), handler];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,43 +120,42 @@ import {el} from '../../testing/src/browser_util';
|
||||||
// these tests are only mean't to be run within the DOM
|
// these tests are only mean't to be run within the DOM
|
||||||
if (isNode) return;
|
if (isNode) return;
|
||||||
|
|
||||||
fixmeIvy(`FW-800: Animation listeners are not invoked`)
|
it('should flush and fire callbacks when the zone becomes stable', (async) => {
|
||||||
.it('should flush and fire callbacks when the zone becomes stable', (async) => {
|
@Component({
|
||||||
@Component({
|
selector: 'my-cmp',
|
||||||
selector: 'my-cmp',
|
template: '<div [@myAnimation]="exp" (@myAnimation.start)="onStart($event)"></div>',
|
||||||
template: '<div [@myAnimation]="exp" (@myAnimation.start)="onStart($event)"></div>',
|
animations: [trigger(
|
||||||
animations: [trigger(
|
'myAnimation',
|
||||||
'myAnimation',
|
[transition(
|
||||||
[transition(
|
'* => state',
|
||||||
'* => state',
|
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||||
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
})
|
||||||
})
|
class Cmp {
|
||||||
class Cmp {
|
exp: any;
|
||||||
exp: any;
|
event: any;
|
||||||
event: any;
|
onStart(event: any) { this.event = event; }
|
||||||
onStart(event: any) { this.event = event; }
|
}
|
||||||
}
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
|
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
|
||||||
declarations: [Cmp]
|
declarations: [Cmp]
|
||||||
});
|
});
|
||||||
|
|
||||||
const engine = TestBed.get(AnimationEngine);
|
const engine = TestBed.get(AnimationEngine);
|
||||||
const fixture = TestBed.createComponent(Cmp);
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
const cmp = fixture.componentInstance;
|
const cmp = fixture.componentInstance;
|
||||||
cmp.exp = 'state';
|
cmp.exp = 'state';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
expect(cmp.event.triggerName).toEqual('myAnimation');
|
expect(cmp.event.triggerName).toEqual('myAnimation');
|
||||||
expect(cmp.event.phaseName).toEqual('start');
|
expect(cmp.event.phaseName).toEqual('start');
|
||||||
cmp.event = null;
|
cmp.event = null;
|
||||||
|
|
||||||
engine.flush();
|
engine.flush();
|
||||||
expect(cmp.event).toBeFalsy();
|
expect(cmp.event).toBeFalsy();
|
||||||
async();
|
async();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly insert/remove nodes through the animation renderer that do not contain animations',
|
it('should properly insert/remove nodes through the animation renderer that do not contain animations',
|
||||||
(async) => {
|
(async) => {
|
||||||
|
|
|
@ -10,94 +10,86 @@ import {ɵAnimationEngine} from '@angular/animations/browser';
|
||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import {fixmeIvy} from '@angular/private/testing';
|
|
||||||
|
|
||||||
{
|
{
|
||||||
describe('NoopAnimationsModule', () => {
|
describe('NoopAnimationsModule', () => {
|
||||||
beforeEach(() => { TestBed.configureTestingModule({imports: [NoopAnimationsModule]}); });
|
beforeEach(() => { TestBed.configureTestingModule({imports: [NoopAnimationsModule]}); });
|
||||||
|
|
||||||
it('should be removed once FW-800 is fixed', () => { expect(true).toBeTruthy(); });
|
it('should flush and fire callbacks when the zone becomes stable', (async) => {
|
||||||
|
@Component({
|
||||||
|
selector: 'my-cmp',
|
||||||
|
template:
|
||||||
|
'<div [@myAnimation]="exp" (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>',
|
||||||
|
animations: [trigger(
|
||||||
|
'myAnimation',
|
||||||
|
[transition(
|
||||||
|
'* => state', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||||
|
})
|
||||||
|
class Cmp {
|
||||||
|
exp: any;
|
||||||
|
startEvent: any;
|
||||||
|
doneEvent: any;
|
||||||
|
onStart(event: any) { this.startEvent = event; }
|
||||||
|
onDone(event: any) { this.doneEvent = event; }
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: remove the dummy test above ^ once the bug FW-800 has been fixed
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
fixmeIvy(`FW-800: Animation listeners are not invoked`)
|
|
||||||
.it('should flush and fire callbacks when the zone becomes stable', (async) => {
|
|
||||||
@Component({
|
|
||||||
selector: 'my-cmp',
|
|
||||||
template:
|
|
||||||
'<div [@myAnimation]="exp" (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>',
|
|
||||||
animations: [trigger(
|
|
||||||
'myAnimation',
|
|
||||||
[transition(
|
|
||||||
'* => state',
|
|
||||||
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
|
||||||
})
|
|
||||||
class Cmp {
|
|
||||||
exp: any;
|
|
||||||
startEvent: any;
|
|
||||||
doneEvent: any;
|
|
||||||
onStart(event: any) { this.startEvent = event; }
|
|
||||||
onDone(event: any) { this.doneEvent = event; }
|
|
||||||
}
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
|
const cmp = fixture.componentInstance;
|
||||||
|
cmp.exp = 'state';
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(cmp.startEvent.triggerName).toEqual('myAnimation');
|
||||||
|
expect(cmp.startEvent.phaseName).toEqual('start');
|
||||||
|
expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
|
||||||
|
expect(cmp.doneEvent.phaseName).toEqual('done');
|
||||||
|
async();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const fixture = TestBed.createComponent(Cmp);
|
it('should handle leave animation callbacks even if the element is destroyed in the process',
|
||||||
const cmp = fixture.componentInstance;
|
(async) => {
|
||||||
cmp.exp = 'state';
|
@Component({
|
||||||
fixture.detectChanges();
|
selector: 'my-cmp',
|
||||||
fixture.whenStable().then(() => {
|
template:
|
||||||
expect(cmp.startEvent.triggerName).toEqual('myAnimation');
|
'<div *ngIf="exp" @myAnimation (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>',
|
||||||
expect(cmp.startEvent.phaseName).toEqual('start');
|
animations: [trigger(
|
||||||
expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
|
'myAnimation',
|
||||||
expect(cmp.doneEvent.phaseName).toEqual('done');
|
[transition(
|
||||||
async();
|
':leave', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||||
});
|
})
|
||||||
});
|
class Cmp {
|
||||||
|
exp: any;
|
||||||
|
startEvent: any;
|
||||||
|
doneEvent: any;
|
||||||
|
onStart(event: any) { this.startEvent = event; }
|
||||||
|
onDone(event: any) { this.doneEvent = event; }
|
||||||
|
}
|
||||||
|
|
||||||
fixmeIvy(`FW-800: Animation listeners are not invoked`)
|
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||||
.it('should handle leave animation callbacks even if the element is destroyed in the process',
|
const engine = TestBed.get(ɵAnimationEngine);
|
||||||
(async) => {
|
const fixture = TestBed.createComponent(Cmp);
|
||||||
@Component({
|
const cmp = fixture.componentInstance;
|
||||||
selector: 'my-cmp',
|
|
||||||
template:
|
|
||||||
'<div *ngIf="exp" @myAnimation (@myAnimation.start)="onStart($event)" (@myAnimation.done)="onDone($event)"></div>',
|
|
||||||
animations: [trigger(
|
|
||||||
'myAnimation',
|
|
||||||
[transition(
|
|
||||||
':leave',
|
|
||||||
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
|
||||||
})
|
|
||||||
class Cmp {
|
|
||||||
exp: any;
|
|
||||||
startEvent: any;
|
|
||||||
doneEvent: any;
|
|
||||||
onStart(event: any) { this.startEvent = event; }
|
|
||||||
onDone(event: any) { this.doneEvent = event; }
|
|
||||||
}
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
cmp.exp = true;
|
||||||
const engine = TestBed.get(ɵAnimationEngine);
|
fixture.detectChanges();
|
||||||
const fixture = TestBed.createComponent(Cmp);
|
fixture.whenStable().then(() => {
|
||||||
const cmp = fixture.componentInstance;
|
cmp.startEvent = null;
|
||||||
|
cmp.doneEvent = null;
|
||||||
|
|
||||||
cmp.exp = true;
|
cmp.exp = false;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
cmp.startEvent = null;
|
expect(cmp.startEvent.triggerName).toEqual('myAnimation');
|
||||||
cmp.doneEvent = null;
|
expect(cmp.startEvent.phaseName).toEqual('start');
|
||||||
|
expect(cmp.startEvent.toState).toEqual('void');
|
||||||
cmp.exp = false;
|
expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
|
||||||
fixture.detectChanges();
|
expect(cmp.doneEvent.phaseName).toEqual('done');
|
||||||
fixture.whenStable().then(() => {
|
expect(cmp.doneEvent.toState).toEqual('void');
|
||||||
expect(cmp.startEvent.triggerName).toEqual('myAnimation');
|
async();
|
||||||
expect(cmp.startEvent.phaseName).toEqual('start');
|
});
|
||||||
expect(cmp.startEvent.toState).toEqual('void');
|
});
|
||||||
expect(cmp.doneEvent.triggerName).toEqual('myAnimation');
|
});
|
||||||
expect(cmp.doneEvent.phaseName).toEqual('done');
|
|
||||||
expect(cmp.doneEvent.toState).toEqual('void');
|
|
||||||
async();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue