diff --git a/packages/core/src/render3/instructions/listener.ts b/packages/core/src/render3/instructions/listener.ts index 265765498e..ee61c22680 100644 --- a/packages/core/src/render3/instructions/listener.ts +++ b/packages/core/src/render3/instructions/listener.ts @@ -110,6 +110,8 @@ function listenerInternal( ngDevMode && assertNodeOfPossibleTypes( tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer); + let processOutputs = true; + // add native event listener - applicable to elements only if (tNode.type === TNodeType.Element) { const native = getNativeByTNode(tNode, lView) as RElement; @@ -149,6 +151,7 @@ function listenerInternal( // Attach a new listener at the head of the coalesced listeners list. (listenerFn).__ngNextListenerFn__ = (existingListener).__ngNextListenerFn__; (existingListener).__ngNextListenerFn__ = listenerFn; + processOutputs = false; } else { // The first argument of `listen` function in Procedural Renderer is: // - either a target name (as a string) in case of global target (window, document, body) @@ -180,7 +183,7 @@ function listenerInternal( const outputs = tNode.outputs; let props: PropertyAliasValue|undefined; - if (outputs && (props = outputs[eventName])) { + if (processOutputs && outputs && (props = outputs[eventName])) { const propsLength = props.length; if (propsLength) { const lCleanup = getCleanup(lView); diff --git a/packages/core/test/acceptance/listener_spec.ts b/packages/core/test/acceptance/listener_spec.ts index 08eec4c573..044520b060 100644 --- a/packages/core/test/acceptance/listener_spec.ts +++ b/packages/core/test/acceptance/listener_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, ErrorHandler, HostListener, QueryList, ViewChildren} from '@angular/core'; +import {Component, Directive, ErrorHandler, EventEmitter, HostListener, Input, Output, QueryList, ViewChild, ViewChildren} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {onlyInIvy} from '@angular/private/testing'; @@ -203,5 +203,40 @@ describe('event listeners', () => { expect(returnsFalseDir.event.preventDefault).toHaveBeenCalled(); }); + it('should not subscribe twice to the output when there are 2 coalesced listeners', () => { + @Directive({selector: '[foo]'}) + class FooDirective { + @Input('foo') model: any; + @Output('fooChange') update = new EventEmitter(); + + updateValue(value: any) { this.update.emit(value); } + } + + @Component({ + selector: 'test-component', + template: `
` + }) + class TestComponent { + count = 0; + someValue = -1; + + @ViewChild(FooDirective) fooDirective: FooDirective|null = null; + + fooChange() { this.count++; } + + triggerUpdate(value: any) { this.fooDirective !.updateValue(value); } + } + + TestBed.configureTestingModule({declarations: [TestComponent, FooDirective]}); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const componentInstance = fixture.componentInstance; + componentInstance.triggerUpdate(42); + fixture.detectChanges(); + + expect(componentInstance.count).toEqual(1); + expect(componentInstance.someValue).toEqual(42); + }); }); });