fix(platform-browser): run BLACK_LISTED_EVENTS outside of ngZone (#18993)
PR Close #18993
This commit is contained in:
parent
ed1175f27e
commit
d52f42688a
|
@ -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},
|
||||
|
|
|
@ -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 (!listenerRegistered) {
|
||||
}
|
||||
if (!callbackRegistered) {
|
||||
taskDatas.push({zone: zone, handler: callback});
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalListenerRegistered) {
|
||||
element[ADD_EVENT_LISTENER](eventName, globalListener, false);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -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('<div><div></div></div>');
|
||||
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('<div><div></div></div>');
|
||||
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('<div><div></div></div>');
|
||||
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('<div><div></div></div>');
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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'];
|
Loading…
Reference in New Issue