Skip to content

Commit f9d483e

Browse files
JiaLiPassionmhevery
authored andcommitted
feat: add a temp solution to support passive event listeners. (#34503)
Now Angular doesn't support add event listeners as passive very easily. User needs to use `elem.addEventListener('scroll', listener, {passive: true});` or implements their own EventManagerPlugin to do that. Angular may finally support new template syntax to support passive event, for now, this commit introduces a temp solution to allow user to define the passive event names in zone.js configurations. User can define a global varibale like this. ``` (window as any)['__zone_symbol__PASSIVE_EVENTS'] = ['scroll']; ``` to let all `scroll` event listeners passive. PR Close #34503
1 parent af76651 commit f9d483e

4 files changed

Lines changed: 93 additions & 38 deletions

File tree

aio/content/guide/user-input.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,23 @@ Following is all the code discussed in this page.
298298
</code-tabs>
299299

300300

301+
Angular also supports passive event listeners. For example, you can use the following steps to make the scroll event passive.
301302

303+
1. Create a file `zone-flags.ts` under `src` directory.
304+
2. Add the following line into this file.
305+
306+
```
307+
(window as any)['__zone_symbol__PASSIVE_EVENTS'] = ['scroll'];
308+
```
309+
310+
3. In the `src/polyfills.ts` file, before importing zone.js, import the newly created `zone-flags`.
311+
312+
```
313+
import './zone-flags';
314+
import 'zone.js/dist/zone'; // Included with Angular CLI.
315+
```
316+
317+
After those steps, if you add event listeners for the `scroll` event, the listeners will be `passive`.
302318

303319
## Summary
304320

packages/zone.js/lib/common/events.ts

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ if (typeof window !== 'undefined') {
2525
try {
2626
const options =
2727
Object.defineProperty({}, 'passive', {get: function() { passiveSupported = true; }});
28-
2928
window.addEventListener('test', options, options);
3029
window.removeEventListener('test', options, options);
3130
} catch (err) {
@@ -245,16 +244,30 @@ export function patchEventTarget(
245244
proto[patchOptions.prepend];
246245
}
247246

248-
function checkIsPassive(task: Task) {
249-
if (!passiveSupported && typeof taskData.options !== 'boolean' &&
250-
typeof taskData.options !== 'undefined' && taskData.options !== null) {
251-
// options is a non-null non-undefined object
252-
// passive is not supported
253-
// don't pass options as object
254-
// just pass capture as a boolean
255-
(task as any).options = !!taskData.options.capture;
256-
taskData.options = (task as any).options;
247+
/**
248+
* This util function will build an option object with passive option
249+
* to handle all possible input from the user.
250+
*/
251+
function buildEventListenerOptions(options: any, passive: boolean) {
252+
if (!passiveSupported && typeof options === 'object' && options) {
253+
// doesn't support passive but user want to pass an object as options.
254+
// this will not work on some old browser, so we just pass a boolean
255+
// as useCapture parameter
256+
return !!options.capture;
257+
}
258+
if (!passiveSupported || !passive) {
259+
return options;
260+
}
261+
if (typeof options === 'boolean') {
262+
return {capture: options, passive: true};
257263
}
264+
if (!options) {
265+
return {passive: true};
266+
}
267+
if (typeof options === 'object' && options.passive !== false) {
268+
return {...options, passive: true};
269+
}
270+
return options;
258271
}
259272

260273
const customScheduleGlobal = function(task: Task) {
@@ -263,7 +276,6 @@ export function patchEventTarget(
263276
if (taskData.isExisting) {
264277
return;
265278
}
266-
checkIsPassive(task);
267279
return nativeAddEventListener.call(
268280
taskData.target, taskData.eventName,
269281
taskData.capture ? globalZoneAwareCaptureCallback : globalZoneAwareCallback,
@@ -311,7 +323,6 @@ export function patchEventTarget(
311323
};
312324

313325
const customScheduleNonGlobal = function(task: Task) {
314-
checkIsPassive(task);
315326
return nativeAddEventListener.call(
316327
taskData.target, taskData.eventName, task.invoke, taskData.options);
317328
};
@@ -338,6 +349,7 @@ export function patchEventTarget(
338349
(patchOptions && patchOptions.diff) ? patchOptions.diff : compareTaskCallbackVsDelegate;
339350

340351
const blackListedEvents: string[] = (Zone as any)[zoneSymbol('BLACK_LISTED_EVENTS')];
352+
const passiveEvents: string[] = _global[zoneSymbol('PASSIVE_EVENTS')];
341353

342354
const makeAddListener = function(
343355
nativeListener: any, addSource: string, customScheduleFn: any, customCancelFn: any,
@@ -372,29 +384,25 @@ export function patchEventTarget(
372384
return;
373385
}
374386

375-
const options = arguments[2];
387+
const passive =
388+
passiveSupported && !!passiveEvents && passiveEvents.indexOf(eventName) !== -1;
389+
const options = buildEventListenerOptions(arguments[2], passive);
376390

377391
if (blackListedEvents) {
378392
// check black list
379393
for (let i = 0; i < blackListedEvents.length; i++) {
380394
if (eventName === blackListedEvents[i]) {
381-
return nativeListener.apply(this, arguments);
395+
if (passive) {
396+
return nativeListener.call(target, eventName, delegate, options);
397+
} else {
398+
return nativeListener.apply(this, arguments);
399+
}
382400
}
383401
}
384402
}
385403

386-
let capture;
387-
let once = false;
388-
if (options === undefined) {
389-
capture = false;
390-
} else if (options === true) {
391-
capture = true;
392-
} else if (options === false) {
393-
capture = false;
394-
} else {
395-
capture = options ? !!options.capture : false;
396-
once = options ? !!options.once : false;
397-
}
404+
const capture = !options ? false : typeof options === 'boolean' ? true : options.capture;
405+
const once = options && typeof options === 'object' ? options.once : false;
398406

399407
const zone = Zone.current;
400408
let symbolEventNames = zoneSymbolEventNames[eventName];
@@ -508,17 +516,7 @@ export function patchEventTarget(
508516
}
509517
const options = arguments[2];
510518

511-
let capture;
512-
if (options === undefined) {
513-
capture = false;
514-
} else if (options === true) {
515-
capture = true;
516-
} else if (options === false) {
517-
capture = false;
518-
} else {
519-
capture = options ? !!options.capture : false;
520-
}
521-
519+
const capture = !options ? false : typeof options === 'boolean' ? true : options.capture;
522520
const delegate = arguments[1];
523521
if (!delegate) {
524522
return nativeRemoveEventListener.apply(this, arguments);

packages/zone.js/test/browser/browser.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ describe('Zone', function() {
222222
});
223223

224224
zone.run(() => { document.dispatchEvent(scrollEvent); });
225+
(document as any).removeAllListeners('scroll');
225226
});
226227

227228
it('should be able to clear on handler added before load zone.js', function() {
@@ -799,6 +800,7 @@ describe('Zone', function() {
799800

800801
button.dispatchEvent(clickEvent);
801802
expect(logs).toEqual([]);
803+
(document as any).removeAllListeners('click');
802804
});
803805
}));
804806

@@ -1035,6 +1037,42 @@ describe('Zone', function() {
10351037
button.removeEventListener('click', listener);
10361038
}));
10371039

1040+
describe('passiveEvents by global settings', () => {
1041+
let logs: string[] = [];
1042+
const listener = (e: Event) => {
1043+
logs.push(e.defaultPrevented ? 'defaultPrevented' : 'default will run');
1044+
e.preventDefault();
1045+
logs.push(e.defaultPrevented ? 'defaultPrevented' : 'default will run');
1046+
};
1047+
const testPassive = function(eventName: string, expectedPassiveLog: string, options: any) {
1048+
(button as any).addEventListener(eventName, listener, options);
1049+
const evt = document.createEvent('Event');
1050+
evt.initEvent(eventName, true, true);
1051+
button.dispatchEvent(evt);
1052+
expect(logs).toEqual(['default will run', expectedPassiveLog]);
1053+
(button as any).removeAllListeners(eventName);
1054+
};
1055+
beforeEach(() => { logs = []; });
1056+
it('should be passive with global variable defined',
1057+
() => { testPassive('touchstart', 'default will run', {passive: true}); });
1058+
it('should not be passive without global variable defined',
1059+
() => { testPassive('touchend', 'defaultPrevented', undefined); });
1060+
it('should be passive with global variable defined even without passive options',
1061+
() => { testPassive('touchstart', 'default will run', undefined); });
1062+
it('should be passive with global variable defined even without passive options and with capture',
1063+
() => { testPassive('touchstart', 'default will run', {capture: true}); });
1064+
it('should be passive with global variable defined with capture option',
1065+
() => { testPassive('touchstart', 'default will run', true); });
1066+
it('should not be passive with global variable defined with passive false option',
1067+
() => { testPassive('touchstart', 'defaultPrevented', {passive: false}); });
1068+
it('should be passive with global variable defined and also blacklisted', () => {
1069+
(document as any).removeAllListeners('scroll');
1070+
testPassive('scroll', 'default will run', undefined);
1071+
});
1072+
it('should not be passive without global variable defined and also blacklisted',
1073+
() => { testPassive('wheel', 'defaultPrevented', undefined); });
1074+
});
1075+
10381076
it('should support Event.stopImmediatePropagation',
10391077
ifEnvSupports(supportEventListenerOptions, function() {
10401078
const hookSpy = jasmine.createSpy('hook');

packages/zone.js/test/test_fake_polyfill.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,8 @@
7878
global['__Zone_ignore_on_properties'] =
7979
[{target: TestTarget.prototype, ignoreProperties: ['prop1']}];
8080
global[zoneSymbolPrefix + 'FakeAsyncTestMacroTask'] = [{source: 'TestClass.myTimeout'}];
81-
global[zoneSymbolPrefix + 'UNPATCHED_EVENTS'] = ['scroll'];
81+
// will not monkey patch scroll and wheel event.
82+
global[zoneSymbolPrefix + 'UNPATCHED_EVENTS'] = ['scroll', 'wheel'];
83+
// touchstart and scroll will be passive by default.
84+
global[zoneSymbolPrefix + 'PASSIVE_EVENTS'] = ['touchstart', 'scroll'];
8285
})(typeof window === 'object' && window || typeof self === 'object' && self || global);

0 commit comments

Comments
 (0)