diff --git a/aio/content/guide/user-input.md b/aio/content/guide/user-input.md index c7b275afac425..42f42bcda6004 100644 --- a/aio/content/guide/user-input.md +++ b/aio/content/guide/user-input.md @@ -298,7 +298,23 @@ Following is all the code discussed in this page. +Angular also supports passive event listeners. For example, you can use the following steps to make the scroll event passive. +1. Create a file `zone-flags.ts` under `src` directory. +2. Add the following line into this file. + +``` +(window as any)['__zone_symbol__PASSIVE_EVENTS'] = ['scroll']; +``` + +3. In the `src/polyfills.ts` file, before importing zone.js, import the newly created `zone-flags`. + +``` +import './zone-flags'; +import 'zone.js/dist/zone'; // Included with Angular CLI. +``` + +After those steps, if you add event listeners for the `scroll` event, the listeners will be `passive`. ## Summary diff --git a/packages/zone.js/lib/common/events.ts b/packages/zone.js/lib/common/events.ts index 85818b4d1207e..81530d650fa0c 100644 --- a/packages/zone.js/lib/common/events.ts +++ b/packages/zone.js/lib/common/events.ts @@ -25,7 +25,6 @@ if (typeof window !== 'undefined') { try { const options = Object.defineProperty({}, 'passive', {get: function() { passiveSupported = true; }}); - window.addEventListener('test', options, options); window.removeEventListener('test', options, options); } catch (err) { @@ -245,16 +244,30 @@ export function patchEventTarget( proto[patchOptions.prepend]; } - function checkIsPassive(task: Task) { - if (!passiveSupported && typeof taskData.options !== 'boolean' && - typeof taskData.options !== 'undefined' && taskData.options !== null) { - // options is a non-null non-undefined object - // passive is not supported - // don't pass options as object - // just pass capture as a boolean - (task as any).options = !!taskData.options.capture; - taskData.options = (task as any).options; + /** + * This util function will build an option object with passive option + * to handle all possible input from the user. + */ + function buildEventListenerOptions(options: any, passive: boolean) { + if (!passiveSupported && typeof options === 'object' && options) { + // doesn't support passive but user want to pass an object as options. + // this will not work on some old browser, so we just pass a boolean + // as useCapture parameter + return !!options.capture; + } + if (!passiveSupported || !passive) { + return options; + } + if (typeof options === 'boolean') { + return {capture: options, passive: true}; } + if (!options) { + return {passive: true}; + } + if (typeof options === 'object' && options.passive !== false) { + return {...options, passive: true}; + } + return options; } const customScheduleGlobal = function(task: Task) { @@ -263,7 +276,6 @@ export function patchEventTarget( if (taskData.isExisting) { return; } - checkIsPassive(task); return nativeAddEventListener.call( taskData.target, taskData.eventName, taskData.capture ? globalZoneAwareCaptureCallback : globalZoneAwareCallback, @@ -311,7 +323,6 @@ export function patchEventTarget( }; const customScheduleNonGlobal = function(task: Task) { - checkIsPassive(task); return nativeAddEventListener.call( taskData.target, taskData.eventName, task.invoke, taskData.options); }; @@ -338,6 +349,7 @@ export function patchEventTarget( (patchOptions && patchOptions.diff) ? patchOptions.diff : compareTaskCallbackVsDelegate; const blackListedEvents: string[] = (Zone as any)[zoneSymbol('BLACK_LISTED_EVENTS')]; + const passiveEvents: string[] = _global[zoneSymbol('PASSIVE_EVENTS')]; const makeAddListener = function( nativeListener: any, addSource: string, customScheduleFn: any, customCancelFn: any, @@ -372,29 +384,25 @@ export function patchEventTarget( return; } - const options = arguments[2]; + const passive = + passiveSupported && !!passiveEvents && passiveEvents.indexOf(eventName) !== -1; + const options = buildEventListenerOptions(arguments[2], passive); if (blackListedEvents) { // check black list for (let i = 0; i < blackListedEvents.length; i++) { if (eventName === blackListedEvents[i]) { - return nativeListener.apply(this, arguments); + if (passive) { + return nativeListener.call(target, eventName, delegate, options); + } else { + return nativeListener.apply(this, arguments); + } } } } - let capture; - let once = false; - if (options === undefined) { - capture = false; - } else if (options === true) { - capture = true; - } else if (options === false) { - capture = false; - } else { - capture = options ? !!options.capture : false; - once = options ? !!options.once : false; - } + const capture = !options ? false : typeof options === 'boolean' ? true : options.capture; + const once = options && typeof options === 'object' ? options.once : false; const zone = Zone.current; let symbolEventNames = zoneSymbolEventNames[eventName]; @@ -508,17 +516,7 @@ export function patchEventTarget( } const options = arguments[2]; - let capture; - if (options === undefined) { - capture = false; - } else if (options === true) { - capture = true; - } else if (options === false) { - capture = false; - } else { - capture = options ? !!options.capture : false; - } - + const capture = !options ? false : typeof options === 'boolean' ? true : options.capture; const delegate = arguments[1]; if (!delegate) { return nativeRemoveEventListener.apply(this, arguments); diff --git a/packages/zone.js/test/browser/browser.spec.ts b/packages/zone.js/test/browser/browser.spec.ts index 71263adbbd112..6c2b77a6c90c1 100644 --- a/packages/zone.js/test/browser/browser.spec.ts +++ b/packages/zone.js/test/browser/browser.spec.ts @@ -222,6 +222,7 @@ describe('Zone', function() { }); zone.run(() => { document.dispatchEvent(scrollEvent); }); + (document as any).removeAllListeners('scroll'); }); it('should be able to clear on handler added before load zone.js', function() { @@ -799,6 +800,7 @@ describe('Zone', function() { button.dispatchEvent(clickEvent); expect(logs).toEqual([]); + (document as any).removeAllListeners('click'); }); })); @@ -1035,6 +1037,42 @@ describe('Zone', function() { button.removeEventListener('click', listener); })); + describe('passiveEvents by global settings', () => { + let logs: string[] = []; + const listener = (e: Event) => { + logs.push(e.defaultPrevented ? 'defaultPrevented' : 'default will run'); + e.preventDefault(); + logs.push(e.defaultPrevented ? 'defaultPrevented' : 'default will run'); + }; + const testPassive = function(eventName: string, expectedPassiveLog: string, options: any) { + (button as any).addEventListener(eventName, listener, options); + const evt = document.createEvent('Event'); + evt.initEvent(eventName, true, true); + button.dispatchEvent(evt); + expect(logs).toEqual(['default will run', expectedPassiveLog]); + (button as any).removeAllListeners(eventName); + }; + beforeEach(() => { logs = []; }); + it('should be passive with global variable defined', + () => { testPassive('touchstart', 'default will run', {passive: true}); }); + it('should not be passive without global variable defined', + () => { testPassive('touchend', 'defaultPrevented', undefined); }); + it('should be passive with global variable defined even without passive options', + () => { testPassive('touchstart', 'default will run', undefined); }); + it('should be passive with global variable defined even without passive options and with capture', + () => { testPassive('touchstart', 'default will run', {capture: true}); }); + it('should be passive with global variable defined with capture option', + () => { testPassive('touchstart', 'default will run', true); }); + it('should not be passive with global variable defined with passive false option', + () => { testPassive('touchstart', 'defaultPrevented', {passive: false}); }); + it('should be passive with global variable defined and also blacklisted', () => { + (document as any).removeAllListeners('scroll'); + testPassive('scroll', 'default will run', undefined); + }); + it('should not be passive without global variable defined and also blacklisted', + () => { testPassive('wheel', 'defaultPrevented', undefined); }); + }); + it('should support Event.stopImmediatePropagation', ifEnvSupports(supportEventListenerOptions, function() { const hookSpy = jasmine.createSpy('hook'); diff --git a/packages/zone.js/test/test_fake_polyfill.ts b/packages/zone.js/test/test_fake_polyfill.ts index a0787692bd2ce..3d5a712044346 100644 --- a/packages/zone.js/test/test_fake_polyfill.ts +++ b/packages/zone.js/test/test_fake_polyfill.ts @@ -78,5 +78,8 @@ global['__Zone_ignore_on_properties'] = [{target: TestTarget.prototype, ignoreProperties: ['prop1']}]; global[zoneSymbolPrefix + 'FakeAsyncTestMacroTask'] = [{source: 'TestClass.myTimeout'}]; - global[zoneSymbolPrefix + 'UNPATCHED_EVENTS'] = ['scroll']; + // will not monkey patch scroll and wheel event. + global[zoneSymbolPrefix + 'UNPATCHED_EVENTS'] = ['scroll', 'wheel']; + // touchstart and scroll will be passive by default. + global[zoneSymbolPrefix + 'PASSIVE_EVENTS'] = ['touchstart', 'scroll']; })(typeof window === 'object' && window || typeof self === 'object' && self || global);