diff --git a/karma-js.conf.js b/karma-js.conf.js
index c93da152a5..0a8dde14ee 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 77a3d68401..906fb52b2a 100644
--- a/packages/platform-browser/src/dom/events/dom_events.ts
+++ b/packages/platform-browser/src/dom/events/dom_events.ts
@@ -32,6 +32,20 @@ const ANGULAR = 'ANGULAR';
const NATIVE_ADD_LISTENER = 'addEventListener';
const NATIVE_REMOVE_LISTENER = 'removeEventListener';
+const blackListedEvents: string[] = Zone && Zone[__symbol__('BLACK_LISTED_EVENTS')];
+let blackListedMap: {[eventName: string]: string};
+if (blackListedEvents) {
+ blackListedMap = {};
+ blackListedEvents.forEach(eventName => { blackListedMap[eventName] = eventName; });
+}
+
+const isBlackListedEvent = function(eventName: string) {
+ if (!blackListedMap) {
+ return false;
+ }
+ return blackListedMap.hasOwnProperty(eventName);
+};
+
interface TaskData {
zone: any;
handler: Function;
@@ -49,14 +63,29 @@ const globalListener = function(event: Event) {
return;
}
const args: any = [event];
- taskDatas.forEach(taskData => {
+ if (taskDatas.length === 1) {
+ // if taskDatas only have one element, just invoke it
+ const taskData = taskDatas[0];
if (taskData.zone !== Zone.current) {
// only use Zone.run when Zone.current not equals to stored zone
return taskData.zone.run(taskData.handler, this, args);
} else {
return taskData.handler.apply(this, args);
}
- });
+ } else {
+ // copy tasks as a snapshot to avoid event handlers remove
+ // itself or others
+ const copiedTasks = taskDatas.slice();
+ for (let i = 0; i < copiedTasks.length; i++) {
+ const taskData = copiedTasks[i];
+ if (taskData.zone !== Zone.current) {
+ // only use Zone.run when Zone.current not equals to stored zone
+ taskData.zone.run(taskData.handler, this, args);
+ } else {
+ taskData.handler.apply(this, args);
+ }
+ }
+ }
};
@Injectable()
@@ -86,20 +115,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 81ca4705ff..480de63bd1 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,132 @@ 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 handle event correctly when one handler remove itself ', () => {
+ 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[] = [];
+ let remover1: any = null;
+ let remover2: any = null;
+ const handler1 = (e: any /** TODO #9100 */) => {
+ receivedEvents.push(e);
+ receivedZones.push(Zone.current.name);
+ remover1 && remover1();
+ };
+ const handler2 = (e: any /** TODO #9100 */) => {
+ receivedEvents.push(e);
+ receivedZones.push(Zone.current.name);
+ };
+ const manager = new EventManager([domEventPlugin], new FakeNgZone());
+
+ 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 0000000000..e32e0a537e
--- /dev/null
+++ b/test-events.js
@@ -0,0 +1,9 @@
+/**
+ * @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'];
\ No newline at end of file