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', () => {