diff --git a/packages/core/src/render3/instructions/listener.ts b/packages/core/src/render3/instructions/listener.ts
index fb02629b04..265765498e 100644
--- a/packages/core/src/render3/instructions/listener.ts
+++ b/packages/core/src/render3/instructions/listener.ts
@@ -75,7 +75,8 @@ function findExistingListener(
const tCleanup = tView.cleanup;
if (tCleanup != null) {
for (let i = 0; i < tCleanup.length - 1; i += 2) {
- if (tCleanup[i] === eventName && tCleanup[i + 1] === tNodeIdx) {
+ const cleanupEventName = tCleanup[i];
+ if (cleanupEventName === eventName && tCleanup[i + 1] === tNodeIdx) {
// We have found a matching event name on the same node but it might not have been
// registered yet, so we must explicitly verify entries in the LView cleanup data
// structures.
@@ -88,7 +89,9 @@ function findExistingListener(
// blocks of 4 or 2 items in the tView.cleanup and this is why we iterate over 2 elements
// first and jump another 2 elements if we detect listeners cleanup (4 elements). Also check
// documentation of TView.cleanup for more details of this data structure layout.
- i += 2;
+ if (typeof cleanupEventName === 'string') {
+ i += 2;
+ }
}
}
return null;
diff --git a/packages/core/test/acceptance/listener_spec.ts b/packages/core/test/acceptance/listener_spec.ts
index b241a26383..08eec4c573 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} from '@angular/core';
+import {Component, Directive, ErrorHandler, HostListener, QueryList, ViewChildren} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {onlyInIvy} from '@angular/private/testing';
@@ -105,6 +105,35 @@ describe('event listeners', () => {
expect(buttonDebugEls[1].injector.get(MdButton).counter).toBe(1);
});
+ onlyInIvy('ngDevMode.rendererAddEventListener counters are only available in ivy')
+ .it('should coalesce multiple event listeners in presence of queries', () => {
+
+ @Component({
+ selector: 'test-cmpt',
+ template: ``
+ })
+ class TestCmpt {
+ counter = 0;
+
+ @ViewChildren('nothing') nothing !: QueryList;
+ }
+
+ TestBed.configureTestingModule({declarations: [TestCmpt, LikesClicks]});
+ const noOfEventListenersRegisteredSoFar = getNoOfNativeListeners();
+ const fixture = TestBed.createComponent(TestCmpt);
+ fixture.detectChanges();
+ const buttonDebugEl = fixture.debugElement.query(By.css('button'));
+
+ // We want to assert that only one native event handler was registered but still all
+ // directives are notified when an event fires. This assertion can only be verified in
+ // the ngDevMode (but the coalescing always happens!).
+ ngDevMode && expect(getNoOfNativeListeners()).toBe(noOfEventListenersRegisteredSoFar + 1);
+
+ buttonDebugEl.nativeElement.click();
+ expect(buttonDebugEl.injector.get(LikesClicks).counter).toBe(1);
+ expect(fixture.componentInstance.counter).toBe(1);
+ });
+
it('should try to execute remaining coalesced listeners if one of the listeners throws', () => {