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