Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support passive events by define global variables in zone.js config file #34503

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions aio/content/guide/user-input.md
Expand Up @@ -298,7 +298,23 @@ Following is all the code discussed in this page.
</code-tabs>


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

Expand Down
72 changes: 35 additions & 37 deletions packages/zone.js/lib/common/events.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -263,7 +276,6 @@ export function patchEventTarget(
if (taskData.isExisting) {
return;
}
checkIsPassive(task);
return nativeAddEventListener.call(
taskData.target, taskData.eventName,
taskData.capture ? globalZoneAwareCaptureCallback : globalZoneAwareCallback,
Expand Down Expand Up @@ -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);
};
Expand All @@ -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,
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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);
Expand Down
38 changes: 38 additions & 0 deletions packages/zone.js/test/browser/browser.spec.ts
Expand Up @@ -222,6 +222,7 @@ describe('Zone', function() {
});

zone.run(() => { document.dispatchEvent(scrollEvent); });
(document as any).removeAllListeners('scroll');
JiaLiPassion marked this conversation as resolved.
Show resolved Hide resolved
});

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

button.dispatchEvent(clickEvent);
expect(logs).toEqual([]);
(document as any).removeAllListeners('click');
});
}));

Expand Down Expand Up @@ -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');
Expand Down
5 changes: 4 additions & 1 deletion packages/zone.js/test/test_fake_polyfill.ts
Expand Up @@ -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);