Skip to content

Commit d20b7d0

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 d20b7d0

15 files changed

Lines changed: 390 additions & 58 deletions

File tree

aio/content/examples/event-binding/src/app/app.component.html

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,52 @@ <h4>Result: {{currentItem.name}}</h4>
2929
<!-- #enddocregion event-binding-3-->
3030
</div>
3131

32+
3233
<div class="group">
3334
<h3>Binding to a nested component</h3>
3435
<h4>Custom events with EventEmitter</h4>
3536
<!-- #docregion event-binding-to-component -->
3637
<app-item-detail (deleteRequest)="deleteItem($event)" [item]="currentItem"></app-item-detail>
37-
<!-- #enddocregion event-binding-to-component -->
38-
38+
<!-- #enddocregion event-binding-to-component -->
3939

4040
<h4>Click to see event target class:</h4>
41-
<div class="parent-div" (click)="onClickMe($event)" clickable>Click me (parent)
42-
<div class="child-div">Click me too! (child) </div>
41+
<div class="parent-div" (click)="onClickMe($event)" clickable>Click me (parent)
42+
<div class="child-div">Click me too! (child) </div>
43+
</div>
44+
45+
<h3>Saves only once:</h3>
46+
<div (click)="onSave()" clickable>
47+
<button (click)="onSave($event)">Save, no propagation</button>
48+
</div>
49+
50+
<h3>Saves twice:</h3>
51+
<div (click)="onSave()" clickable>
52+
<button (click)="onSave()">Save with propagation</button>
53+
</div>
4354
</div>
4455

45-
<h3>Saves only once:</h3>
46-
<div (click)="onSave()" clickable>
47-
<button (click)="onSave($event)">Save, no propagation</button>
56+
<div class="group">
57+
<h3>Apply capture/passive/once options when adding the event listener</h3>
58+
59+
<!-- #docregion event-binding-4-->
60+
<div (click)="handleClickOnParentDiv()">
61+
<div (click.capture)="handleClickOnChildDiv()"></div>
62+
</div>
63+
<div (scroll.passive)="handleScroll($event)"></div>
64+
<div (click.once)="handleOnceClick()"></div>
65+
<div (click.once.capture.passive)="handleClickWithMixedOptions()"></div>
66+
<!-- #enddocregion event-binding-4-->
4867
</div>
4968

50-
<h3>Saves twice:</h3>
51-
<div (click)="onSave()" clickable>
52-
<button (click)="onSave()">Save with propagation</button>
69+
<div class="group">
70+
<h3>Specify zone option on event handler</h3>
71+
<h4>Event handler inside angular zone?: {{zoneMessage}}</h4>
72+
73+
<!-- #docregion event-binding-5-->
74+
<div (click.noopZone)="clickInNoopZone()"></div>
75+
<button #btnToggle>Toggle</button>
76+
<div *ngIf="show">
77+
<div (click.ngZone)="clickInNgZone()"></div>
78+
</div>
79+
<!-- #enddocregion event-binding-5-->
5380
</div>
Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,72 @@
1-
import { Component } from '@angular/core';
2-
import { Item } from './item';
1+
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, NgZone, ViewChild} from '@angular/core';
32

4-
@Component({
5-
selector: 'app-root',
6-
templateUrl: './app.component.html',
7-
styleUrls: ['./app.component.css']
8-
})
9-
export class AppComponent {
3+
import {Item} from './item';
104

11-
currentItem = { name: 'teapot'} ;
5+
@Component(
6+
{selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css']})
7+
export class AppComponent implements AfterViewInit {
8+
@ViewChild('btnToggle') btnToggle: ElementRef;
9+
currentItem = {name: 'teapot'};
1210
clickMessage = '';
11+
zoneMessage = '';
12+
13+
show = false;
14+
15+
constructor(private ngZone: NgZone, private cdr: ChangeDetectorRef) {}
16+
17+
ngAfterViewInit() {
18+
this.ngZone.runOutsideAngular(() => {
19+
const el = this.btnToggle.nativeElement as HTMLElement;
20+
el.addEventListener('click', e => {
21+
this.show = true;
22+
this.cdr.detectChanges();
23+
})
24+
})
25+
}
1326

1427
onSave(event?: KeyboardEvent) {
1528
const evtMsg = event ? ' Event target is ' + (<HTMLElement>event.target).textContent : '';
1629
alert('Saved.' + evtMsg);
17-
if (event) { event.stopPropagation(); }
30+
if (event) {
31+
event.stopPropagation();
32+
}
1833
}
1934

2035
deleteItem(item: Item) {
2136
alert(`Delete the ${item.name}.`);
2237
}
2338

2439
onClickMe(event?: KeyboardEvent) {
25-
const evtMsg = event ? ' Event target class is ' + (<HTMLElement>event.target).className : '';
40+
const evtMsg = event ? ' Event target class is ' + (<HTMLElement>event.target).className : '';
2641
alert('Click me.' + evtMsg);
2742
}
2843

44+
handleClickOnParentDiv() {
45+
alert('clicked on parent');
46+
}
47+
48+
handleClickOnChildDiv() {
49+
alert('clicked on child');
50+
}
51+
52+
handleScroll(event?: Event) {
53+
event.preventDefault()
54+
alert(`preventDefault does not work in passive event handler, ${event.defaultPrevented}`)
55+
}
56+
57+
handleOnceClick() {
58+
alert('this handle should only be called once');
59+
}
60+
61+
handleClickWithMixedOptions() {}
62+
63+
clickInNoopZone() {
64+
this.zoneMessage =
65+
NgZone.isInAngularZone() ? `inside the angular zone` : 'outside of the angular zone';
66+
}
67+
68+
clickInNgZone() {
69+
this.zoneMessage =
70+
NgZone.isInAngularZone() ? `inside the angular zone` : 'outside of the angular zone';
71+
}
2972
}

aio/content/guide/event-binding.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ To update the `name` property, the changed text is retrieved by following the pa
6868
If the event belongs to a directive&mdash;recall that components
6969
are directives&mdash;`$event` has whatever shape the directive produces.
7070

71+
## Apply `capture`, `passive`, `once`, `ngZone`, `noopZone` options
72+
73+
It is possible to apply [AddEventListenerOptions](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) `capture`, `passive`, `once` by combining the event name with the option.
74+
75+
Consider this example:
76+
77+
<code-example path="event-binding/src/app/app.component.html" region="event-binding-4" header="src/app/app.component.html"></code-example>
78+
79+
And it is also possible to apply `ngZone` or `noopZone` option to make sure the event handler run into the specified `zone`.
80+
81+
- `ngZone`: The event handler always runs into the `angular zone`, so it triggers the change detection automatically.
82+
- `noopZone`: The event handler always runs outside of the `angular zone`, so it does not trigger the change detection automatically.
83+
84+
Consider this example:
85+
86+
<code-example path="event-binding/src/app/app.component.html" region="event-binding-5" header="src/app/app.component.html"></code-example>
87+
7188
## Custom events with `EventEmitter`
7289

7390
Directives typically raise custom events with an Angular [EventEmitter](api/core/EventEmitter).

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
}

0 commit comments

Comments
 (0)