diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts index 28f48f6ba6..21dc1c2a8a 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts @@ -1134,6 +1134,113 @@ describe('compiler compliance: bindings', () => { expectEmit(result.source, template, 'Incorrect template'); }); + it('should chain multiple host listeners into a single instruction', () => { + const files = { + app: { + 'example.ts': ` + import {Directive, HostListener} from '@angular/core'; + + @Directive({ + selector: '[my-dir]', + host: { + '(mousedown)': 'mousedown()', + '(mouseup)': 'mouseup()', + } + }) + export class MyDirective { + mousedown() {} + mouseup() {} + + @HostListener('click') + click() {} + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) { + if (rf & 1) { + $r3$.ɵɵlistener("mousedown", function MyDirective_mousedown_HostBindingHandler($event) { return ctx.mousedown(); })("mouseup", function MyDirective_mouseup_HostBindingHandler($event) { return ctx.mouseup(); })("click", function MyDirective_click_HostBindingHandler($event) { return ctx.click(); }); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple synthetic host listeners into a single instruction', () => { + const files = { + app: { + 'example.ts': ` + import {Component, HostListener} from '@angular/core'; + + @Component({ + selector: 'my-comp', + template: '', + host: { + '(@animation.done)': 'done()', + } + }) + export class MyComponent { + @HostListener('@animation.start') + start() {} + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { + if (rf & 1) { + $r3$.ɵɵcomponentHostSyntheticListener("@animation.done", function MyComponent_animation_animation_done_HostBindingHandler($event) { return ctx.done(); })("@animation.start", function MyComponent_animation_animation_start_HostBindingHandler($event) { return ctx.start(); }); + } + } + `; + + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple regular and synthetic host listeners into two instructions', () => { + const files = { + app: { + 'example.ts': ` + import {Component, HostListener} from '@angular/core'; + + @Component({ + selector: 'my-comp', + template: '', + host: { + '(mousedown)': 'mousedown()', + '(@animation.done)': 'done()', + '(mouseup)': 'mouseup()', + } + }) + export class MyComponent { + @HostListener('@animation.start') + start() {} + + @HostListener('click') + click() {} + }` + } + }; + + const result = compile(files, angularFiles); + const template = ` + … + hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { + if (rf & 1) { + $r3$.ɵɵcomponentHostSyntheticListener("@animation.done", function MyComponent_animation_animation_done_HostBindingHandler($event) { return ctx.done(); })("@animation.start", function MyComponent_animation_animation_start_HostBindingHandler($event) { return ctx.start(); }); + $r3$.ɵɵlistener("mousedown", function MyComponent_mousedown_HostBindingHandler($event) { return ctx.mousedown(); })("mouseup", function MyComponent_mouseup_HostBindingHandler($event) { return ctx.mouseup(); })("click", function MyComponent_click_HostBindingHandler($event) { return ctx.click(); }); + } + } + `; + expectEmit(result.source, template, 'Incorrect template'); + }); + }); describe('non bindable behavior', () => { diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts index 088b881e03..f0fa2488ed 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_listener_spec.ts @@ -229,4 +229,119 @@ describe('compiler compliance: listen()', () => { expectEmit(source, MyComponentFactory, 'Incorrect MyComponent.ɵfac'); }); + it('should chain multiple listeners on the same element', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`
\` + }) + export class MyComponent { + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + … + consts: [[${AttributeMarker.Bindings}, "click", "change"]], + template: function MyComponent_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵelementStart(0, "div", 0); + $r3$.ɵɵlistener("click", function MyComponent_Template_div_click_0_listener($event) { + return ctx.click(); + })("change", function MyComponent_Template_div_change_0_listener($event) { + return ctx.change(); + }); + $r3$.ɵɵelementEnd(); + } + } + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple listeners across elements', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \` +
+ + \` + }) + export class MyComponent { + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + … + consts: [[${AttributeMarker.Bindings}, "click", "change"], [${AttributeMarker.Bindings}, "update", "delete"]], + template: function MyComponent_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵelementStart(0, "div", 0); + $r3$.ɵɵlistener("click", function MyComponent_Template_div_click_0_listener($event) { return ctx.click(); })("change", function MyComponent_Template_div_change_0_listener($event) { return ctx.change(); }); + $r3$.ɵɵelementEnd(); + $r3$.ɵɵelementStart(1, "some-comp", 1); + $r3$.ɵɵlistener("update", function MyComponent_Template_some_comp_update_1_listener($event) { return ctx.update(); })("delete", function MyComponent_Template_some_comp_delete_1_listener($event) { return ctx.delete(); }); + $r3$.ɵɵelementEnd(); + } + } + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should chain multiple listeners on the same template', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'my-component', + template: \`\` + }) + export class MyComponent { + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + … + consts: [[${AttributeMarker.Bindings}, "click", "change"]], + template: function MyComponent_Template(rf, ctx) { + if (rf & 1) { + $r3$.ɵɵtemplate(0, MyComponent_ng_template_0_Template, 0, 0, "ng-template", 0); + $r3$.ɵɵlistener("click", function MyComponent_Template_ng_template_click_0_listener($event) { return ctx.click(); })("change", function MyComponent_Template_ng_template_change_0_listener($event) { return ctx.change(); }); + } + } + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + + }); diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index 5ae7759b70..a5413bd41f 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -277,8 +277,7 @@ describe('compiler compliance: styling', () => { template: function MyComponent_Template(rf, ctx) { if (rf & 1) { $r3$.ɵɵelementStart(0, "div"); - $r3$.ɵɵlistener("@myAnimation.start", function MyComponent_Template_div_animation_myAnimation_start_0_listener($event) { return ctx.onStart($event); }); - $r3$.ɵɵlistener("@myAnimation.done", function MyComponent_Template_div_animation_myAnimation_done_0_listener($event) { return ctx.onDone($event); }); + $r3$.ɵɵlistener("@myAnimation.start", function MyComponent_Template_div_animation_myAnimation_start_0_listener($event) { return ctx.onStart($event); })("@myAnimation.done", function MyComponent_Template_div_animation_myAnimation_done_0_listener($event) { return ctx.onDone($event); }); $r3$.ɵɵelementEnd(); } if (rf & 2) { $r3$.ɵɵproperty("@myAnimation", ctx.exp); @@ -337,8 +336,7 @@ describe('compiler compliance: styling', () => { hostBindings: function MyAnimDir_HostBindings(rf, ctx, elIndex) { if (rf & 1) { $r3$.ɵɵallocHostVars(1); - $r3$.ɵɵcomponentHostSyntheticListener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); }); - $r3$.ɵɵcomponentHostSyntheticListener("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); }); + $r3$.ɵɵcomponentHostSyntheticListener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); })("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); }); } if (rf & 2) { $r3$.ɵɵupdateSyntheticHostBinding("@myAnim", ctx.myAnimState); } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index a8abb01bd0..4c826443c3 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -2041,9 +2041,7 @@ runInEachFileSystem(os => { const hostBindingsFn = ` hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick(); }); - i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onDocumentClick($event.target); }, false, i0.ɵɵresolveDocument); - i0.ɵɵlistener("scroll", function FooCmp_scroll_HostBindingHandler($event) { return ctx.onWindowScroll(); }, false, i0.ɵɵresolveWindow); + i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick(); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onDocumentClick($event.target); }, false, i0.ɵɵresolveDocument)("scroll", function FooCmp_scroll_HostBindingHandler($event) { return ctx.onWindowScroll(); }, false, i0.ɵɵresolveWindow); } } `; @@ -2136,9 +2134,7 @@ runInEachFileSystem(os => { hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { i0.ɵɵallocHostVars(3); - i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); }); - i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody); - i0.ɵɵlistener("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); }); + i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody)("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); }); } if (rf & 2) { i0.ɵɵhostProperty("prop", ctx.bar); @@ -3144,7 +3140,7 @@ runInEachFileSystem(os => { env.write('test.ts', ` import {Component} from '@angular/core'; - + @Component({ template: '
', }) diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index dc057c5702..b589276cbc 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -733,17 +733,35 @@ function getBindingNameAndInstruction(binding: ParsedProperty): } function createHostListeners(eventBindings: ParsedEvent[], name?: string): o.Statement[] { - return eventBindings.map(binding => { + const listeners: o.Expression[][] = []; + const syntheticListeners: o.Expression[][] = []; + const instructions: o.Statement[] = []; + + eventBindings.forEach(binding => { let bindingName = binding.name && sanitizeIdentifier(binding.name); const bindingFnName = binding.type === ParsedEventType.Animation ? prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) : bindingName; const handlerName = name && bindingName ? `${name}_${bindingFnName}_HostBindingHandler` : null; const params = prepareEventListenerParameters(BoundEvent.fromParsedEvent(binding), handlerName); - const instruction = - binding.type == ParsedEventType.Animation ? R3.componentHostSyntheticListener : R3.listener; - return o.importExpr(instruction).callFn(params).toStmt(); + + if (binding.type == ParsedEventType.Animation) { + syntheticListeners.push(params); + } else { + listeners.push(params); + } }); + + if (syntheticListeners.length > 0) { + instructions.push( + chainedInstruction(R3.componentHostSyntheticListener, syntheticListeners).toStmt()); + } + + if (listeners.length > 0) { + instructions.push(chainedInstruction(R3.listener, listeners).toStmt()); + } + + return instructions; } function metadataAsSummary(meta: R3HostMetadata): CompileDirectiveSummary { diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index de51e3cc42..9eba6ff72f 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -684,11 +684,14 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } // Generate Listeners (outputs) - element.outputs.forEach((outputAst: t.BoundEvent) => { - this.creationInstruction( - outputAst.sourceSpan, R3.listener, - this.prepareListenerParameter(element.name, outputAst, elementIndex)); - }); + if (element.outputs.length > 0) { + const listeners = element.outputs.map( + (outputAst: t.BoundEvent) => ({ + sourceSpan: outputAst.sourceSpan, + params: this.prepareListenerParameter(element.name, outputAst, elementIndex) + })); + this.creationInstructionChain(R3.listener, listeners); + } // Note: it's important to keep i18n/i18nStart instructions after i18nAttributes and // listeners, to make sure i18nAttributes instruction targets current element at runtime. @@ -912,11 +915,14 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Add the input bindings this.templatePropertyBindings(templateIndex, template.inputs); // Generate listeners for directive output - template.outputs.forEach((outputAst: t.BoundEvent) => { - this.creationInstruction( - outputAst.sourceSpan, R3.listener, - this.prepareListenerParameter('ng_template', outputAst, templateIndex)); - }); + if (template.outputs.length > 0) { + const listeners = template.outputs.map( + (outputAst: t.BoundEvent) => ({ + sourceSpan: outputAst.sourceSpan, + params: this.prepareListenerParameter('ng_template', outputAst, templateIndex) + })); + this.creationInstructionChain(R3.listener, listeners); + } } } @@ -1092,6 +1098,16 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver this.instructionFn(this._creationCodeFns, span, reference, paramsOrFn || [], prepend); } + private creationInstructionChain(reference: o.ExternalReference, calls: { + sourceSpan: ParseSourceSpan | null, + params: () => o.Expression[] + }[]) { + const span = calls.length ? calls[0].sourceSpan : null; + this._creationCodeFns.push(() => { + return chainedInstruction(reference, calls.map(call => call.params()), span).toStmt(); + }); + } + private updateInstructionWithAdvance( nodeIndex: number, span: ParseSourceSpan|null, reference: o.ExternalReference, paramsOrFn?: o.Expression[]|(() => o.Expression[])) { diff --git a/packages/core/src/render3/instructions/listener.ts b/packages/core/src/render3/instructions/listener.ts index 9f164d5715..bb0b02b0d1 100644 --- a/packages/core/src/render3/instructions/listener.ts +++ b/packages/core/src/render3/instructions/listener.ts @@ -18,7 +18,7 @@ import {assertNodeOfPossibleTypes} from '../node_assert'; import {getLView, getPreviousOrParentTNode} from '../state'; import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils'; -import {getCleanup, handleError, loadComponentRenderer, markViewDirty} from './shared'; +import {TsickleIssue1009, getCleanup, handleError, loadComponentRenderer, markViewDirty} from './shared'; @@ -38,11 +38,12 @@ import {getCleanup, handleError, loadComponentRenderer, markViewDirty} from './s */ export function ɵɵlistener( eventName: string, listenerFn: (e?: any) => any, useCapture = false, - eventTargetResolver?: GlobalTargetResolver): void { + eventTargetResolver?: GlobalTargetResolver): TsickleIssue1009 { const lView = getLView(); const tNode = getPreviousOrParentTNode(); listenerInternal( lView, lView[RENDERER], tNode, eventName, listenerFn, useCapture, eventTargetResolver); + return ɵɵlistener; } /** @@ -68,11 +69,12 @@ export function ɵɵlistener( */ export function ɵɵcomponentHostSyntheticListener( eventName: string, listenerFn: (e?: any) => any, useCapture = false, - eventTargetResolver?: GlobalTargetResolver): void { + eventTargetResolver?: GlobalTargetResolver): TsickleIssue1009 { const lView = getLView(); const tNode = getPreviousOrParentTNode(); const renderer = loadComponentRenderer(tNode, lView); listenerInternal(lView, renderer, tNode, eventName, listenerFn, useCapture, eventTargetResolver); + return ɵɵcomponentHostSyntheticListener; } /** diff --git a/packages/core/test/render3/listeners_spec.ts b/packages/core/test/render3/listeners_spec.ts index 1537fedbc3..d2af7aa923 100644 --- a/packages/core/test/render3/listeners_spec.ts +++ b/packages/core/test/render3/listeners_spec.ts @@ -107,8 +107,7 @@ describe('event listeners', () => { if (rf & RenderFlags.Create) { ɵɵlistener('custom', function() { return ctx.onDocumentCustomEvent(); - }, false, ɵɵresolveDocument as GlobalTargetResolver); - ɵɵlistener('click', function() { + }, false, ɵɵresolveDocument as GlobalTargetResolver)('click', function() { return ctx.onBodyClick(); }, false, ɵɵresolveBody as GlobalTargetResolver); } diff --git a/packages/core/test/render3/perf/listeners/index.ts b/packages/core/test/render3/perf/listeners/index.ts index 89cb11d660..a5b6075f51 100644 --- a/packages/core/test/render3/perf/listeners/index.ts +++ b/packages/core/test/render3/perf/listeners/index.ts @@ -32,44 +32,34 @@ function testTemplate(rf: RenderFlags, ctx: any) { if (rf & 1) { ɵɵelementStart(0, 'div'); ɵɵelementStart(1, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementStart(2, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementStart(3, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementStart(4, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementStart(5, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementStart(6, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementStart(7, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementStart(8, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementStart(9, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementStart(10, 'button', 0); - ɵɵlistener('click', function clickListener() {}); - ɵɵlistener('input', function inputListener() {}); + ɵɵlistener('click', function clickListener() {})('input', function inputListener() {}); ɵɵelementEnd(); ɵɵelementEnd(); } diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 2de96f649f..90c37659dc 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -732,7 +732,7 @@ export declare type ɵɵComponentDefWithMeta = ComponentDef; -export declare function ɵɵcomponentHostSyntheticListener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): void; +export declare function ɵɵcomponentHostSyntheticListener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): TsickleIssue1009; export declare function ɵɵcontainer(index: number): void; @@ -897,7 +897,7 @@ export declare function ɵɵinjectPipeChangeDetectorRef(flags?: InjectFlags): Ch export declare function ɵɵinvalidFactory(): never; -export declare function ɵɵlistener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): void; +export declare function ɵɵlistener(eventName: string, listenerFn: (e?: any) => any, useCapture?: boolean, eventTargetResolver?: GlobalTargetResolver): TsickleIssue1009; export declare function ɵɵloadQuery(): QueryList;