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);
|
||||
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]', () => {
|
||||
|
|
|
@ -49,14 +49,14 @@ export class BoundAttribute implements Node {
|
|||
|
||||
export class BoundEvent implements Node {
|
||||
constructor(
|
||||
public name: string, public handler: AST, public target: string|null,
|
||||
public phase: string|null, public sourceSpan: ParseSourceSpan) {}
|
||||
public name: string, public type: ParsedEventType, public handler: AST,
|
||||
public target: string|null, public phase: string|null, public sourceSpan: ParseSourceSpan) {}
|
||||
|
||||
static fromParsedEvent(event: ParsedEvent) {
|
||||
const target: string|null = event.type === ParsedEventType.Regular ? event.targetOrPhase : null;
|
||||
const phase: string|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); }
|
||||
|
|
|
@ -10,7 +10,7 @@ import {flatten, sanitizeIdentifier} from '../../compile_metadata';
|
|||
import {BindingForm, BuiltinFunctionCall, LocalResolver, convertActionBinding, convertPropertyBinding} from '../../compiler_util/expression_converter';
|
||||
import {ConstantPool} from '../../constant_pool';
|
||||
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 {Parser} from '../../expression_parser/parser';
|
||||
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[] {
|
||||
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 functionName = `${this.templateName}_${tagNameSanitized}_${evNameSanitized}_listener`;
|
||||
|
||||
|
@ -1007,8 +1011,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||
const handler = o.fn(
|
||||
[new o.FnParam('$event', o.DYNAMIC_TYPE)], statements, o.INFERRED_TYPE, null,
|
||||
functionName);
|
||||
|
||||
return [o.literal(outputAst.name), handler];
|
||||
return [o.literal(eventName), handler];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,43 +120,42 @@ import {el} from '../../testing/src/browser_util';
|
|||
// these tests are only mean't to be run within the DOM
|
||||
if (isNode) return;
|
||||
|
||||
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)"></div>',
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[transition(
|
||||
'* => state',
|
||||
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
exp: any;
|
||||
event: any;
|
||||
onStart(event: any) { this.event = event; }
|
||||
}
|
||||
it('should flush and fire callbacks when the zone becomes stable', (async) => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: '<div [@myAnimation]="exp" (@myAnimation.start)="onStart($event)"></div>',
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[transition(
|
||||
'* => state',
|
||||
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
exp: any;
|
||||
event: any;
|
||||
onStart(event: any) { this.event = event; }
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
declarations: [Cmp]
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: AnimationEngine, useClass: InjectableAnimationEngine}],
|
||||
declarations: [Cmp]
|
||||
});
|
||||
|
||||
const engine = TestBed.get(AnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'state';
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(cmp.event.triggerName).toEqual('myAnimation');
|
||||
expect(cmp.event.phaseName).toEqual('start');
|
||||
cmp.event = null;
|
||||
const engine = TestBed.get(AnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'state';
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(cmp.event.triggerName).toEqual('myAnimation');
|
||||
expect(cmp.event.phaseName).toEqual('start');
|
||||
cmp.event = null;
|
||||
|
||||
engine.flush();
|
||||
expect(cmp.event).toBeFalsy();
|
||||
async();
|
||||
});
|
||||
});
|
||||
engine.flush();
|
||||
expect(cmp.event).toBeFalsy();
|
||||
async();
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly insert/remove nodes through the animation renderer that do not contain animations',
|
||||
(async) => {
|
||||
|
|
|
@ -10,94 +10,86 @@ import {ɵAnimationEngine} from '@angular/animations/browser';
|
|||
import {Component} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {fixmeIvy} from '@angular/private/testing';
|
||||
|
||||
{
|
||||
describe('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
|
||||
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]});
|
||||
|
||||
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);
|
||||
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();
|
||||
});
|
||||
});
|
||||
it('should handle leave animation callbacks even if the element is destroyed in the process',
|
||||
(async) => {
|
||||
@Component({
|
||||
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; }
|
||||
}
|
||||
|
||||
fixmeIvy(`FW-800: Animation listeners are not invoked`)
|
||||
.it('should handle leave animation callbacks even if the element is destroyed in the process',
|
||||
(async) => {
|
||||
@Component({
|
||||
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]});
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
cmp.startEvent = null;
|
||||
cmp.doneEvent = null;
|
||||
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
cmp.startEvent = null;
|
||||
cmp.doneEvent = null;
|
||||
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(cmp.startEvent.triggerName).toEqual('myAnimation');
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(cmp.startEvent.triggerName).toEqual('myAnimation');
|
||||
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