diff --git a/karma-js.conf.js b/karma-js.conf.js index c93da152a55440..0a8dde14ee6316 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -35,6 +35,7 @@ module.exports = function(config) { 'node_modules/zone.js/dist/fake-async-test.js', // Including systemjs because it defines `__eval`, which produces correct stack traces. + 'test-events.js', 'shims_for_IE.js', 'node_modules/systemjs/dist/system.src.js', {pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true}, diff --git a/packages/platform-browser/src/dom/events/dom_events.ts b/packages/platform-browser/src/dom/events/dom_events.ts index 77a3d684019caf..1168c61302a568 100644 --- a/packages/platform-browser/src/dom/events/dom_events.ts +++ b/packages/platform-browser/src/dom/events/dom_events.ts @@ -32,6 +32,15 @@ const ANGULAR = 'ANGULAR'; const NATIVE_ADD_LISTENER = 'addEventListener'; const NATIVE_REMOVE_LISTENER = 'removeEventListener'; +const blackListedMap: {[key: string]: string} = Zone && Zone[__symbol__('BLACK_LISTED_EVENTS')]; + +const isBlackListedEvent = function(eventName: string) { + if (!blackListedMap) { + return false; + } + return blackListedMap.hasOwnProperty(eventName); +}; + interface TaskData { zone: any; handler: Function; @@ -86,20 +95,34 @@ export class DomEventsPlugin extends EventManagerPlugin { let callback: EventListener = handler as EventListener; // if zonejs is loaded and current zone is not ngZone // we keep Zone.current on target for later restoration. - if (zoneJsLoaded && !NgZone.isInAngularZone()) { + if (zoneJsLoaded && (!NgZone.isInAngularZone() || isBlackListedEvent(eventName))) { let symbolName = symbolNames[eventName]; if (!symbolName) { symbolName = symbolNames[eventName] = __symbol__(ANGULAR + eventName + FALSE); } let taskDatas: TaskData[] = (element as any)[symbolName]; - const listenerRegistered = taskDatas && taskDatas.length > 0; + const globalListenerRegistered = taskDatas && taskDatas.length > 0; if (!taskDatas) { taskDatas = (element as any)[symbolName] = []; } - if (taskDatas.filter(taskData => taskData.handler === callback).length === 0) { - taskDatas.push({zone: Zone.current, handler: callback}); + + const zone = isBlackListedEvent(eventName) ? Zone.root : Zone.current; + if (taskDatas.length === 0) { + taskDatas.push({zone: zone, handler: callback}); + } else { + let callbackRegistered = false; + for (let i = 0; i < taskDatas.length; i++) { + if (taskDatas[i].handler === callback) { + callbackRegistered = true; + break; + } + } + if (!callbackRegistered) { + taskDatas.push({zone: zone, handler: callback}); + } } - if (!listenerRegistered) { + + if (!globalListenerRegistered) { element[ADD_EVENT_LISTENER](eventName, globalListener, false); } } else { diff --git a/packages/platform-browser/test/dom/events/event_manager_spec.ts b/packages/platform-browser/test/dom/events/event_manager_spec.ts index 81ca4705ffa7dc..8b130afd83f595 100644 --- a/packages/platform-browser/test/dom/events/event_manager_spec.ts +++ b/packages/platform-browser/test/dom/events/event_manager_spec.ts @@ -116,6 +116,96 @@ export function main() { getDOM().dispatchEvent(element, dispatchedEvent); expect(receivedEvent).toBe(null); }); + + it('should keep zone when addEventListener multiple times', () => { + const Zone = (window as any)['Zone']; + + const element = el('
'); + getDOM().appendChild(doc.body, element); + const dispatchedEvent = getDOM().createMouseEvent('click'); + let receivedEvents: any[] /** TODO #9100 */ = []; + let receivedZones: any[] = []; + const handler1 = (e: any /** TODO #9100 */) => { + receivedEvents.push(e); + receivedZones.push(Zone.current.name); + }; + const handler2 = (e: any /** TODO #9100 */) => { + receivedEvents.push(e); + receivedZones.push(Zone.current.name); + }; + const manager = new EventManager([domEventPlugin], new FakeNgZone()); + + let remover1 = null; + let remover2 = null; + Zone.root.run(() => { remover1 = manager.addEventListener(element, 'click', handler1); }); + Zone.root.fork({name: 'test'}).run(() => { + remover2 = manager.addEventListener(element, 'click', handler2); + }); + getDOM().dispatchEvent(element, dispatchedEvent); + expect(receivedEvents).toEqual([dispatchedEvent, dispatchedEvent]); + expect(receivedZones).toEqual([Zone.root.name, 'test']); + + receivedEvents = []; + remover1 && remover1(); + remover2 && remover2(); + getDOM().dispatchEvent(element, dispatchedEvent); + expect(receivedEvents).toEqual([]); + }); + + it('should only add same callback once when addEventListener', () => { + const Zone = (window as any)['Zone']; + + const element = el('
'); + getDOM().appendChild(doc.body, element); + const dispatchedEvent = getDOM().createMouseEvent('click'); + let receivedEvents: any[] /** TODO #9100 */ = []; + let receivedZones: any[] = []; + const handler = (e: any /** TODO #9100 */) => { + receivedEvents.push(e); + receivedZones.push(Zone.current.name); + }; + const manager = new EventManager([domEventPlugin], new FakeNgZone()); + + let remover1 = null; + let remover2 = null; + Zone.root.run(() => { remover1 = manager.addEventListener(element, 'click', handler); }); + Zone.root.fork({name: 'test'}).run(() => { + remover2 = manager.addEventListener(element, 'click', handler); + }); + getDOM().dispatchEvent(element, dispatchedEvent); + expect(receivedEvents).toEqual([dispatchedEvent]); + expect(receivedZones).toEqual([Zone.root.name]); + + receivedEvents = []; + remover1 && remover1(); + remover2 && remover2(); + getDOM().dispatchEvent(element, dispatchedEvent); + expect(receivedEvents).toEqual([]); + }); + + it('should run blackListedEvents handler outside of ngZone', () => { + const Zone = (window as any)['Zone']; + const element = el('
'); + getDOM().appendChild(doc.body, element); + const dispatchedEvent = getDOM().createMouseEvent('scroll'); + let receivedEvent: any /** TODO #9100 */ = null; + let receivedZone: any = null; + const handler = (e: any /** TODO #9100 */) => { + receivedEvent = e; + receivedZone = Zone.current; + }; + const manager = new EventManager([domEventPlugin], new FakeNgZone()); + + let remover = manager.addEventListener(element, 'scroll', handler); + getDOM().dispatchEvent(element, dispatchedEvent); + expect(receivedEvent).toBe(dispatchedEvent); + expect(receivedZone.name).toBe(Zone.root.name); + + receivedEvent = null; + remover && remover(); + getDOM().dispatchEvent(element, dispatchedEvent); + expect(receivedEvent).toBe(null); + }); }); } diff --git a/test-events.js b/test-events.js new file mode 100644 index 00000000000000..e9f20c474fd8ed --- /dev/null +++ b/test-events.js @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +Zone[Zone.__symbol__('BLACK_LISTED_EVENTS')] = { + scroll: 'scroll' +}; \ No newline at end of file