Skip to content

Commit 0efb517

Browse files
committed
feat(platform-browser): can config zone/once/passive/capture in event listener.
Close #19878 1. Can't pass `passive`, `capture`, `once` parameters to `addEventListener`. 2. Not easy to config use ngzone or noopzone with HostListener user need to explicitly call `runOutsideOfAngular` to make sure `eventhandler` run outsideof`ngZone` ``` @HostListener('window:resize', ['$event.target']) onResize(target: any) { this.ngZone.runOutsideOfAngular(() => { console.log('resize triggered'); }); } ``` or call `ngZone.run` explicitly make sure `eventhandler` run into `ngZone`. ``` @HostListener('window:resize', ['$event.target']) onResize(target: any) { this.ngZone.run(() => { console.log('resize triggered'); }); } ``` ``` <div (mouseover)="mouseover();"></div> ``` ``` mouseover() { this.ngZone.runOfAngular(() => { ... }); } ``` 3. Can config which `zone` will event handler will run into in `@HostListener` decorator. 4. can config `passive`, `capture`, `once` parameters to `addEventListener`. - in template, can config those parameter like below. ``` <div (mouseover.capture.once.passive)="mouseover();"></div> ``` even the handler was added not in `ngZone`. ``` <div (mouseover.ngZone)="mouseover();"></div> ``` ``` @HostListener('window:resize.ngZone', ['$event.target']) onResize(target: any) { console.log('resize triggered'); } ``` - Can config `noopZone` which will guarantee eventhandler run outside of `ngZone` even the handler was added in `ngZone`. And it can also work with #20672 to get better performance. ``` <div (mouseover.noopZone)="mouseover();"></div> ``` ``` @HostListener('window:resize.noopZone', ['$event.target']) onResize(target: any) { console.log('resize triggered'); } ```
1 parent 57575e3 commit 0efb517

12 files changed

Lines changed: 282 additions & 37 deletions

File tree

goldens/public-api/platform-browser/platform-browser.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ export declare class EventManager {
3636
getZone(): NgZone;
3737
}
3838

39+
export declare interface EventManagerPluginOptions {
40+
capture?: boolean;
41+
once?: boolean;
42+
passive?: boolean;
43+
zone?: 'ngZone' | 'noopZone';
44+
}
45+
3946
export declare const HAMMER_GESTURE_CONFIG: InjectionToken<HammerGestureConfig>;
4047

4148
export declare const HAMMER_LOADER: InjectionToken<HammerLoader>;
@@ -90,6 +97,8 @@ export declare type MetaDefinition = {
9097
[prop: string]: string;
9198
};
9299

100+
export declare function onAndCancelWithZone(element: any, eventName: string, handler: EventListener, ngZone: NgZone, options?: EventManagerPluginOptions): Function;
101+
93102
export declare const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef;
94103

95104
export declare interface SafeHtml extends SafeValue {

goldens/size-tracking/aio-payloads.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"master": {
1313
"uncompressed": {
1414
"runtime-es2015": 2987,
15-
"main-es2015": 448419,
15+
"main-es2015": 449415,
1616
"polyfills-es2015": 52630
1717
}
1818
}
@@ -21,7 +21,7 @@
2121
"master": {
2222
"uncompressed": {
2323
"runtime-es2015": 3097,
24-
"main-es2015": 429885,
24+
"main-es2015": 430875,
2525
"polyfills-es2015": 52195
2626
}
2727
}

goldens/size-tracking/integration-payloads.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"master": {
44
"uncompressed": {
55
"runtime-es2015": 1485,
6-
"main-es2015": 141151,
6+
"main-es2015": 142629,
77
"polyfills-es2015": 36571
88
}
99
}
@@ -21,7 +21,7 @@
2121
"master": {
2222
"uncompressed": {
2323
"runtime-es2015": 1485,
24-
"main-es2015": 147573,
24+
"main-es2015": 148562,
2525
"polyfills-es2015": 36571
2626
}
2727
}
@@ -30,7 +30,7 @@
3030
"master": {
3131
"uncompressed": {
3232
"runtime-es2015": 1485,
33-
"main-es2015": 136168,
33+
"main-es2015": 137158,
3434
"polyfills-es2015": 37248
3535
}
3636
}
@@ -39,7 +39,7 @@
3939
"master": {
4040
"uncompressed": {
4141
"runtime-es2015": 2289,
42-
"main-es2015": 245351,
42+
"main-es2015": 246352,
4343
"polyfills-es2015": 36938,
4444
"5-es2015": 751
4545
}
@@ -49,7 +49,7 @@
4949
"master": {
5050
"uncompressed": {
5151
"runtime-es2015": 2289,
52-
"main-es2015": 221897,
52+
"main-es2015": 222792,
5353
"polyfills-es2015": 36938,
5454
"5-es2015": 779
5555
}
@@ -62,7 +62,7 @@
6262
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
6363
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
6464
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
65-
"bundle": 1209659
65+
"bundle": 1211019
6666
}
6767
}
6868
}

packages/common/src/dom_adapter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ export abstract class DomAdapter {
5252
abstract isShadowRoot(node: any): boolean;
5353

5454
// Used by KeyEventsPlugin
55-
abstract onAndCancel(el: any, evt: any, listener: any): Function;
55+
abstract onAndCancel(el: any, evt: any, listener: any, options?: AddEventListenerOptions):
56+
Function;
5657
abstract supportsDOMEvents(): boolean;
5758

5859
// Used by PlatformLocation and ServerEventManagerPlugin

packages/platform-browser/src/browser/browser_adapter.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
5454
}
5555
}
5656

57-
onAndCancel(el: Node, evt: any, listener: any): Function {
58-
el.addEventListener(evt, listener, false);
57+
onAndCancel(el: Node, evt: any, listener: any, options?: AddEventListenerOptions): Function {
58+
options ? el.addEventListener(evt, listener, options) : el.addEventListener(evt, listener);
5959
// Needed to follow Dart's subscription semantic, until fix of
6060
// https://code.google.com/p/dart/issues/detail?id=17406
6161
return () => {
62-
el.removeEventListener(evt, listener, false);
62+
options && typeof options.capture === 'boolean' ?
63+
el.removeEventListener(evt, listener, {capture: options.capture}) :
64+
el.removeEventListener(evt, listener);
6365
};
6466
}
6567
dispatchEvent(el: Node, evt: any) {

packages/platform-browser/src/dom/events/dom_events.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {DOCUMENT} from '@angular/common';
9+
import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common';
1010
import {Inject, Injectable} from '@angular/core';
1111

12-
import {EventManagerPlugin} from './event_manager';
12+
import {EventManagerPlugin, EventManagerPluginOptions} from './event_manager';
13+
import {onAndCancelWithZone} from './zone_event_util';
1314

1415
@Injectable()
1516
export class DomEventsPlugin extends EventManagerPlugin {
@@ -23,12 +24,10 @@ export class DomEventsPlugin extends EventManagerPlugin {
2324
return true;
2425
}
2526

26-
addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
27-
element.addEventListener(eventName, handler as EventListener, false);
28-
return () => this.removeEventListener(element, eventName, handler as EventListener);
29-
}
30-
31-
removeEventListener(target: any, eventName: string, callback: Function): void {
32-
return target.removeEventListener(eventName, callback as EventListener);
27+
addEventListener(
28+
element: HTMLElement, eventName: string, handler: Function,
29+
options?: EventManagerPluginOptions): Function {
30+
return onAndCancelWithZone(
31+
element, eventName, handler as EventListener, this.manager.getZone(), options);
3332
}
3433
}

packages/platform-browser/src/dom/events/event_manager.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ export class EventManager {
4747
*/
4848
addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
4949
const plugin = this._findPluginFor(eventName);
50-
return plugin.addEventListener(element, eventName, handler);
50+
const eventNameWithOptions = this._splitEventNameAndOptions(eventName);
51+
return plugin.addEventListener(
52+
element, eventNameWithOptions.eventName, handler, eventNameWithOptions.options);
5153
}
5254

5355
/**
@@ -61,7 +63,9 @@ export class EventManager {
6163
*/
6264
addGlobalEventListener(target: string, eventName: string, handler: Function): Function {
6365
const plugin = this._findPluginFor(eventName);
64-
return plugin.addGlobalEventListener(target, eventName, handler);
66+
const eventNameWithOptions = this._splitEventNameAndOptions(eventName);
67+
return plugin.addGlobalEventListener(
68+
target, eventNameWithOptions.eventName, handler, eventNameWithOptions.options);
6569
}
6670

6771
/**
@@ -88,6 +92,60 @@ export class EventManager {
8892
}
8993
throw new Error(`No event manager plugin found for event ${eventName}`);
9094
}
95+
96+
/** @internal */
97+
_splitEventNameAndOptions(eventName: string):
98+
{eventName: string, options?: EventManagerPluginOptions} {
99+
const parts: string[] = eventName.split('.');
100+
let domEventName = parts.shift();
101+
const r: {eventName: string, options?: EventManagerPluginOptions} = {eventName: domEventName!};
102+
parts.forEach(p => {
103+
const pLower = p.toLowerCase();
104+
if (!r.options &&
105+
(pLower === 'capture' || pLower === 'once' || pLower === 'passive' ||
106+
pLower === 'ngzone' || pLower === 'noopzone')) {
107+
r.options = {};
108+
}
109+
if (pLower === 'capture') {
110+
r.options!.capture = true;
111+
} else if (pLower === 'once') {
112+
r.options!.once = true;
113+
} else if (pLower === 'passive') {
114+
r.options!.passive = true;
115+
} else if (pLower === 'ngzone') {
116+
r.options!.zone = 'ngZone';
117+
} else if (pLower === 'noopzone') {
118+
r.options!.zone = 'noopZone';
119+
} else {
120+
// combine the part with eventName
121+
// since Angular supports the eventName with modifier such as `keydown.enter`.
122+
r.eventName += `.${p}`;
123+
}
124+
});
125+
return r;
126+
}
127+
}
128+
129+
/**
130+
* Event Manager Plugin listener options.
131+
* The options includes the following keys.
132+
*
133+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener}
134+
* 1. capture: indicate `capture` option of `addEventListener`, default is `false`.
135+
* 2. once: indicate `once` option of `addEventListener`, default is `false`.
136+
* 3. passive: indicate `passive` option of `addEventListener`, default is `false`.
137+
* 4. zone: a string indicating the Zone that the event handler want to run into, by default, the
138+
* event handler will run into the Zone when the handler is registered. The available options are
139+
* - ngZone: force the event handler run into angular zone.
140+
* - noop: force the event handler run outside of angular zone.
141+
*
142+
* @publicApi
143+
*/
144+
export interface EventManagerPluginOptions {
145+
capture?: boolean;
146+
once?: boolean;
147+
passive?: boolean;
148+
zone?: 'ngZone'|'noopZone';
91149
}
92150

93151
export abstract class EventManagerPlugin {
@@ -98,13 +156,17 @@ export abstract class EventManagerPlugin {
98156

99157
abstract supports(eventName: string): boolean;
100158

101-
abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function;
159+
abstract addEventListener(
160+
element: HTMLElement, eventName: string, handler: Function,
161+
options?: EventManagerPluginOptions): Function;
102162

103-
addGlobalEventListener(element: string, eventName: string, handler: Function): Function {
163+
addGlobalEventListener(
164+
element: string, eventName: string, handler: Function,
165+
options?: EventManagerPluginOptions): Function {
104166
const target: HTMLElement = getDOM().getGlobalEventTarget(this._doc, element);
105167
if (!target) {
106168
throw new Error(`Unsupported event target ${target} for event ${eventName}`);
107169
}
108-
return this.addEventListener(target, eventName, handler);
170+
return this.addEventListener(target, eventName, handler, options);
109171
}
110172
}

packages/platform-browser/src/dom/events/key_events.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common';
1010
import {Inject, Injectable, NgZone} from '@angular/core';
11-
import {EventManagerPlugin} from './event_manager';
11+
import {EventManagerPlugin, EventManagerPluginOptions} from './event_manager';
1212

1313
/**
1414
* Defines supported modifiers for key events.
@@ -100,14 +100,16 @@ export class KeyEventsPlugin extends EventManagerPlugin {
100100
* event object as an argument.
101101
* @returns The key event that was registered.
102102
*/
103-
addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
103+
addEventListener(
104+
element: HTMLElement, eventName: string, handler: Function,
105+
options?: EventManagerPluginOptions): Function {
104106
const parsedEvent = KeyEventsPlugin.parseEventName(eventName)!;
105107

106108
const outsideHandler =
107109
KeyEventsPlugin.eventCallback(parsedEvent['fullKey'], handler, this.manager.getZone());
108110

109111
return this.manager.getZone().runOutsideAngular(() => {
110-
return getDOM().onAndCancel(element, parsedEvent['domEventName'], outsideHandler);
112+
return getDOM().onAndCancel(element, parsedEvent['domEventName'], outsideHandler, options);
111113
});
112114
}
113115

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ɵgetDOM as getDOM} from '@angular/common';
10+
import {NgZone} from '@angular/core';
11+
import {EventManagerPluginOptions} from './event_manager';
12+
13+
/**
14+
*
15+
* @param element
16+
* @param eventName
17+
* @param handler
18+
* @param ngZone
19+
* @param options
20+
*
21+
* @publicApi
22+
*/
23+
export function onAndCancelWithZone(
24+
element: any, eventName: string, handler: EventListener, ngZone: NgZone,
25+
options?: EventManagerPluginOptions): Function {
26+
const dom = getDOM();
27+
const zoneOption = options?.zone;
28+
// remove options.zone and only pass AddEventListenerOptions to the dom
29+
let hasDOMOptions = false;
30+
if (typeof options?.capture === 'boolean' || typeof options?.once === 'boolean' ||
31+
typeof options?.passive === 'boolean') {
32+
hasDOMOptions = true;
33+
}
34+
let listenerOptions = hasDOMOptions ? {...options} : undefined;
35+
if (listenerOptions) {
36+
delete listenerOptions.zone;
37+
}
38+
if (zoneOption === 'noopZone') {
39+
if (NgZone.isInAngularZone()) {
40+
// Currently inside ngZone, we need to add the listener outside of ngZone
41+
return ngZone.runOutsideAngular(() => {
42+
return dom.onAndCancel(element, eventName, handler, listenerOptions);
43+
});
44+
}
45+
}
46+
if (zoneOption === 'ngZone') {
47+
if (!NgZone.isInAngularZone()) {
48+
// Currently outside ngZone, we need to add the listener inside ngZone
49+
return ngZone.run(() => {
50+
return dom.onAndCancel(element, eventName, handler, listenerOptions);
51+
});
52+
}
53+
}
54+
return dom.onAndCancel(element, eventName, handler, listenerOptions);
55+
}

packages/platform-browser/src/platform-browser.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ export {Title} from './browser/title';
1212
export {disableDebugTools, enableDebugTools} from './browser/tools/tools';
1313
export {BrowserTransferStateModule, makeStateKey, StateKey, TransferState} from './browser/transfer_state';
1414
export {By} from './dom/debug/by';
15-
export {EVENT_MANAGER_PLUGINS, EventManager} from './dom/events/event_manager';
15+
export {EVENT_MANAGER_PLUGINS, EventManager, EventManagerPluginOptions} from './dom/events/event_manager';
1616
export {HAMMER_GESTURE_CONFIG, HAMMER_LOADER, HAMMER_PROVIDERS__POST_R3__ as ɵHAMMER_PROVIDERS__POST_R3__, HammerGestureConfig, HammerLoader, HammerModule} from './dom/events/hammer_gestures';
17+
export {onAndCancelWithZone} from './dom/events/zone_event_util';
1718
export {DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl, SafeValue} from './security/dom_sanitization_service';
1819

1920
export * from './private_export';

0 commit comments

Comments
 (0)