diff --git a/packages/core/testing/src/ng_zone_mock.ts b/packages/core/testing/src/ng_zone_mock.ts index a3356d8e6b219..cad1508c9bf5a 100644 --- a/packages/core/testing/src/ng_zone_mock.ts +++ b/packages/core/testing/src/ng_zone_mock.ts @@ -16,7 +16,7 @@ import {EventEmitter, Injectable, NgZone} from '@angular/core'; export class MockNgZone extends NgZone { onStable: EventEmitter = new EventEmitter(false); - constructor() { super({enableLongStackTrace: false}); } + constructor() { super({enableLongStackTrace: false, shouldCoalesceEventChangeDetection: false}); } run(fn: Function): any { return fn(); } diff --git a/packages/platform-browser/src/dom/dom_renderer.ts b/packages/platform-browser/src/dom/dom_renderer.ts index d6c850b132356..ae04ba7aeec78 100644 --- a/packages/platform-browser/src/dom/dom_renderer.ts +++ b/packages/platform-browser/src/dom/dom_renderer.ts @@ -49,12 +49,6 @@ export function flattenStyles( function decoratePreventDefault(eventHandler: Function): Function { return (event: any) => { - // check if Zone exists, if exists, check whether current Zone - // holds the delayChangeDetection function reference. - if (typeof Zone !== 'undefined') { - const maybeDelayChangeDetection: () => void = Zone.current.get('maybeDelayChangeDetection'); - maybeDelayChangeDetection && maybeDelayChangeDetection(); - } const allowDefaultBehavior = eventHandler(event); if (allowDefaultBehavior === false) { // TODO(tbosch): move preventDefault into event plugins... diff --git a/packages/platform-browser/src/dom/events/dom_events.ts b/packages/platform-browser/src/dom/events/dom_events.ts index 2d3045eb15b68..252584e3ce616 100644 --- a/packages/platform-browser/src/dom/events/dom_events.ts +++ b/packages/platform-browser/src/dom/events/dom_events.ts @@ -11,128 +11,12 @@ import {Inject, Injectable, NgZone, Optional, PLATFORM_ID} from '@angular/core'; import {EventManagerPlugin} from './event_manager'; -/** - * Detect if Zone is present. If it is then use simple zone aware 'addEventListener' - * since Angular can do much more - * efficient bookkeeping than Zone can, because we have additional information. This speeds up - * addEventListener by 3x. - */ -const __symbol__ = - (() => (typeof Zone !== 'undefined') && (Zone as any)['__symbol__'] || - function(v: string): string { return '__zone_symbol__' + v; })(); -const ADD_EVENT_LISTENER: 'addEventListener' = __symbol__('addEventListener'); -const REMOVE_EVENT_LISTENER: 'removeEventListener' = __symbol__('removeEventListener'); - -const symbolNames: {[key: string]: string} = {}; - -const FALSE = 'FALSE'; -const ANGULAR = 'ANGULAR'; -const NATIVE_ADD_LISTENER = 'addEventListener'; -const NATIVE_REMOVE_LISTENER = 'removeEventListener'; - -// use the same symbol string which is used in zone.js -const stopSymbol = '__zone_symbol__propagationStopped'; -const stopMethodSymbol = '__zone_symbol__stopImmediatePropagation'; - - -const blackListedMap = (() => { - const blackListedEvents: string[] = - (typeof Zone !== 'undefined') && (Zone as any)[__symbol__('BLACK_LISTED_EVENTS')]; - if (blackListedEvents) { - const res: {[eventName: string]: string} = {}; - blackListedEvents.forEach(eventName => { res[eventName] = eventName; }); - return res; - } - return undefined; -})(); - - -const isBlackListedEvent = function(eventName: string) { - if (!blackListedMap) { - return false; - } - return blackListedMap.hasOwnProperty(eventName); -}; - -interface TaskData { - zone: any; - handler: Function; -} - -// a global listener to handle all dom event, -// so we do not need to create a closure every time -const globalListener = function(event: Event) { - const symbolName = symbolNames[event.type]; - if (!symbolName) { - return; - } - const taskDatas: TaskData[] = this[symbolName]; - if (!taskDatas) { - return; - } - const args: any = [event]; - 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++) { - // if other listener call event.stopImmediatePropagation - // just break - if ((event as any)[stopSymbol] === true) { - break; - } - 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() export class DomEventsPlugin extends EventManagerPlugin { constructor( @Inject(DOCUMENT) doc: any, private ngZone: NgZone, @Optional() @Inject(PLATFORM_ID) platformId: {}|null) { super(doc); - - if (!platformId || !isPlatformServer(platformId)) { - this.patchEvent(); - } - } - - private patchEvent() { - if (typeof Event === 'undefined' || !Event || !Event.prototype) { - return; - } - if ((Event.prototype as any)[stopMethodSymbol]) { - // already patched by zone.js - return; - } - const delegate = (Event.prototype as any)[stopMethodSymbol] = - Event.prototype.stopImmediatePropagation; - Event.prototype.stopImmediatePropagation = function() { - if (this) { - this[stopSymbol] = true; - } - - // should call native delegate in case - // in some environment part of the application - // will not use the patched Event - delegate && delegate.apply(this, arguments); - }; } // This plugin should come last in the list of plugins, because it accepts all @@ -153,80 +37,21 @@ export class DomEventsPlugin extends EventManagerPlugin { * NOTE: it is possible that the element is from different iframe, and so we * have to check before we execute the method. */ - const self = this; - const zoneJsLoaded = element[ADD_EVENT_LISTENER]; - 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() || isBlackListedEvent(eventName))) { - let symbolName = symbolNames[eventName]; - if (!symbolName) { - symbolName = symbolNames[eventName] = __symbol__(ANGULAR + eventName + FALSE); - } - let taskDatas: TaskData[] = (element as any)[symbolName]; - const globalListenerRegistered = taskDatas && taskDatas.length > 0; - if (!taskDatas) { - taskDatas = (element as any)[symbolName] = []; - } - - 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}); - } + let callback: EventListener = (function(evt: Event) { + // check if Zone exists, if exists, check whether current Zone + // holds the delayChangeDetection function reference. + if (typeof Zone !== 'undefined') { + const maybeDelayChangeDetection: () => void = Zone.current.get('maybeDelayChangeDetection'); + maybeDelayChangeDetection && maybeDelayChangeDetection(); } + return handler && handler.call(this, evt); + }) as EventListener; + element.addEventListener(eventName, callback); - if (!globalListenerRegistered) { - element[ADD_EVENT_LISTENER](eventName, globalListener, false); - } - } else { - element[NATIVE_ADD_LISTENER](eventName, callback, false); - } return () => this.removeEventListener(element, eventName, callback); } removeEventListener(target: any, eventName: string, callback: Function): void { - let underlyingRemove = target[REMOVE_EVENT_LISTENER]; - // zone.js not loaded, use native removeEventListener - if (!underlyingRemove) { - return target[NATIVE_REMOVE_LISTENER].apply(target, [eventName, callback, false]); - } - let symbolName = symbolNames[eventName]; - let taskDatas: TaskData[] = symbolName && target[symbolName]; - if (!taskDatas) { - // addEventListener not using patched version - // just call native removeEventListener - return target[NATIVE_REMOVE_LISTENER].apply(target, [eventName, callback, false]); - } - // fix issue 20532, should be able to remove - // listener which was added inside of ngZone - let found = false; - for (let i = 0; i < taskDatas.length; i++) { - // remove listener from taskDatas if the callback equals - if (taskDatas[i].handler === callback) { - found = true; - taskDatas.splice(i, 1); - break; - } - } - if (found) { - if (taskDatas.length === 0) { - // all listeners are removed, we can remove the globalListener from target - underlyingRemove.apply(target, [eventName, globalListener, false]); - } - } else { - // not found in taskDatas, the callback may be added inside of ngZone - // use native remove listener to remove the callback - target[NATIVE_REMOVE_LISTENER].apply(target, [eventName, callback, false]); - } + target.removeEventListener(eventName, callback); } } 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 46f5c1ad13afd..66e9a78803691 100644 --- a/packages/platform-browser/test/dom/events/event_manager_spec.ts +++ b/packages/platform-browser/test/dom/events/event_manager_spec.ts @@ -320,7 +320,10 @@ import {el} from '../../../testing/src/browser_util'; expect(receivedEvent).toBe(null); }); - it('should only trigger one Change detection when bubbling', () => { + it('should only trigger one Change detection when bubbling', (done: DoneFn) => { + doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument(); + zone = new NgZone({shouldCoalesceEventChangeDetection: true}); + domEventPlugin = new DomEventsPlugin(doc, zone, null); const element = el('
'); const child = el('
'); getDOM().appendChild(element, child); @@ -329,18 +332,24 @@ import {el} from '../../../testing/src/browser_util'; let receivedEvents: any = []; let stables: any = []; const handler = (e: any) => { receivedEvents.push(e); }; - const zone = new FakeNgZone(); const manager = new EventManager([domEventPlugin], zone); + let removerChild: any; + let removerParent: any; - const removerChild = manager.addEventListener(child, 'click', handler); - const removerParent = manager.addEventListener(element, 'click', handler); + zone.run(() => { + removerChild = manager.addEventListener(child, 'click', handler); + removerParent = manager.addEventListener(element, 'click', handler); + }); zone.onStable.subscribe((isStable: any) => { stables.push(isStable); }); getDOM().dispatchEvent(child, dispatchedEvent); - expect(receivedEvents.length).toBe(2); - expect(stables.length).toBe(1); + requestAnimationFrame(() => { + expect(receivedEvents.length).toBe(2); + expect(stables.length).toBe(1); - removerChild && removerChild(); - removerParent && removerParent(); + removerChild && removerChild(); + removerParent && removerParent(); + done(); + }); }); }); })(); diff --git a/packages/platform-browser/testing/src/browser_util.ts b/packages/platform-browser/testing/src/browser_util.ts index e260633f7aa34..7420eb3a6ce86 100644 --- a/packages/platform-browser/testing/src/browser_util.ts +++ b/packages/platform-browser/testing/src/browser_util.ts @@ -157,5 +157,5 @@ export function stringifyElement(el: any /** TODO #9100 */): string { } export function createNgZone(): NgZone { - return new NgZone({enableLongStackTrace: true}); + return new NgZone({enableLongStackTrace: true, shouldCoalesceEventChangeDetection: false}); }