From ba4972dc68615672a2ff63b2ed798cbf4641d48f Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Sat, 22 Apr 2023 19:32:51 +0200 Subject: [PATCH] feat(platform-browser): Expose `EventManagerPlugin` in the public api This commit exposes the `EventManagerPlugin` abstract class to the public API. It provides a documentation with a usecase and a basic implementation describing the possibilities rationale for opening to the public API : 1. The `EVENT_MANAGER_PLUGINS` token is already public 2. The documentation provides a usecase 3. Plugins can already be implemented by following the same API 4. This does not increase the API surface because of 3. --- .../examples/custom-events/BUILD.bazel | 7 +++ .../custom-events/example-config.json | 0 .../custom-events/src/app/app.component.ts | 19 +++++++ .../custom-events/src/app/child.component.ts | 16 ++++++ .../src/app/stop-event-plugin.ts | 26 +++++++++ .../examples/custom-events/src/index.html | 13 +++++ .../examples/custom-events/src/main.ts | 13 +++++ .../examples/custom-events/stackblitz.json | 10 ++++ aio/content/examples/examples.bzl | 1 + aio/content/guide/custom-events-management.md | 57 +++++++++++++++++++ aio/content/guide/event-binding.md | 1 + goldens/public-api/platform-browser/index.md | 9 +++ .../src/dom/events/dom_events.ts | 3 +- .../src/dom/events/event_manager.ts | 22 +++++-- .../src/dom/events/hammer_gestures.ts | 2 +- .../platform-browser/src/platform-browser.ts | 2 +- packages/platform-server/src/server_events.ts | 11 ++-- 17 files changed, 201 insertions(+), 11 deletions(-) create mode 100644 aio/content/examples/custom-events/BUILD.bazel create mode 100644 aio/content/examples/custom-events/example-config.json create mode 100644 aio/content/examples/custom-events/src/app/app.component.ts create mode 100644 aio/content/examples/custom-events/src/app/child.component.ts create mode 100644 aio/content/examples/custom-events/src/app/stop-event-plugin.ts create mode 100644 aio/content/examples/custom-events/src/index.html create mode 100644 aio/content/examples/custom-events/src/main.ts create mode 100644 aio/content/examples/custom-events/stackblitz.json create mode 100644 aio/content/guide/custom-events-management.md diff --git a/aio/content/examples/custom-events/BUILD.bazel b/aio/content/examples/custom-events/BUILD.bazel new file mode 100644 index 00000000000000..507219e360c903 --- /dev/null +++ b/aio/content/examples/custom-events/BUILD.bazel @@ -0,0 +1,7 @@ +load("//aio/content/examples:examples.bzl", "docs_example") + +package(default_visibility = ["//visibility:public"]) + +docs_example( + name = "custom-events", +) diff --git a/aio/content/examples/custom-events/example-config.json b/aio/content/examples/custom-events/example-config.json new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/aio/content/examples/custom-events/src/app/app.component.ts b/aio/content/examples/custom-events/src/app/app.component.ts new file mode 100644 index 00000000000000..59a076083a9abf --- /dev/null +++ b/aio/content/examples/custom-events/src/app/app.component.ts @@ -0,0 +1,19 @@ +import {Component, HostListener} from '@angular/core'; +import {ChildComponent} from './child.component'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [ChildComponent], + template: ` + + `, +}) +export class AppComponent { + name = 'Angular'; + + @HostListener('click', ['$event']) + onClick(event: Event) { + console.log('click parent', event); + } +} diff --git a/aio/content/examples/custom-events/src/app/child.component.ts b/aio/content/examples/custom-events/src/app/child.component.ts new file mode 100644 index 00000000000000..e9154feeec3d98 --- /dev/null +++ b/aio/content/examples/custom-events/src/app/child.component.ts @@ -0,0 +1,16 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-child', + standalone: true, + template: ` + +
+ + `, +}) +export class ChildComponent { + onClick(event: Event) { + console.log('click child', event?.target); + } +} diff --git a/aio/content/examples/custom-events/src/app/stop-event-plugin.ts b/aio/content/examples/custom-events/src/app/stop-event-plugin.ts new file mode 100644 index 00000000000000..e85d068b563294 --- /dev/null +++ b/aio/content/examples/custom-events/src/app/stop-event-plugin.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import {Injectable} from '@angular/core'; +import {EventManagerPlugin} from '@angular/platform-browser'; + +@Injectable() +export class StopEventPlugin extends EventManagerPlugin { + // #docregion supports + supports(event: string): boolean { + return event.endsWith('.stop'); + } + // #enddocregion supports + + // #docregion addEventListener + addEventListener(element: HTMLElement, eventName: string, handler: (event: Event) => void): Function { + const eventWrapper = (event: Event) => { + event.stopPropagation(); + handler(event); + }; + + // striping the stop modifier from the event name; + const nonModifiedEventName = eventName.replace(/\.stop$/, ''); + + return this.manager.addEventListener(element, nonModifiedEventName, eventWrapper); + } + // #enddocregion addEventListener +} diff --git a/aio/content/examples/custom-events/src/index.html b/aio/content/examples/custom-events/src/index.html new file mode 100644 index 00000000000000..37a08c916952b7 --- /dev/null +++ b/aio/content/examples/custom-events/src/index.html @@ -0,0 +1,13 @@ + + + + + ContentProjection + + + + + + + + diff --git a/aio/content/examples/custom-events/src/main.ts b/aio/content/examples/custom-events/src/main.ts new file mode 100644 index 00000000000000..c36e406369f7f0 --- /dev/null +++ b/aio/content/examples/custom-events/src/main.ts @@ -0,0 +1,13 @@ +import { bootstrapApplication } from '@angular/platform-browser-dynamic'; +import { AppComponent } from './app/app.component'; +import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser'; +import { StopEventPlugin } from './app/stop-event-plugin'; + + +bootstrapApplication(AppComponent, { + // #docregion providers + providers: [ + { provide: EVENT_MANAGER_PLUGINS, multi: true, useClass: StopEventPlugin} + ] + // #enddocregion providers +}); diff --git a/aio/content/examples/custom-events/stackblitz.json b/aio/content/examples/custom-events/stackblitz.json new file mode 100644 index 00000000000000..d4e56b1c165411 --- /dev/null +++ b/aio/content/examples/custom-events/stackblitz.json @@ -0,0 +1,10 @@ +{ + "description": "Custom Events", + "files": [ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1,2].*" + ], + "file": "src/app/app.component.ts", + "tags": ["Custom Events"] +} diff --git a/aio/content/examples/examples.bzl b/aio/content/examples/examples.bzl index 2206c9e64fc57a..0b4d0483816a00 100644 --- a/aio/content/examples/examples.bzl +++ b/aio/content/examples/examples.bzl @@ -33,6 +33,7 @@ EXAMPLES = { "component-overview": {"stackblitz": True, "zip": True}, "component-styles": {"stackblitz": True, "zip": True}, "content-projection": {"stackblitz": True, "zip": True}, + "custom-events": {"stackblitz": True, "zip": True}, "dependency-injection": {"stackblitz": True, "zip": True}, "dependency-injection-in-action": {"stackblitz": True, "zip": True}, "deprecation-guide": {"stackblitz": True, "zip": True}, diff --git a/aio/content/guide/custom-events-management.md b/aio/content/guide/custom-events-management.md new file mode 100644 index 00000000000000..fae2b99f698264 --- /dev/null +++ b/aio/content/guide/custom-events-management.md @@ -0,0 +1,57 @@ +# Custom Events Handeling + +Angular allows you to handle custom events, to extend existing ones or support new ones. + +## Handling user Events + +[Event Binding](/guide/event-binding) and the the `HostListener` decorator lets you listen for and respond to DOM events and components events. + + + + + <navbutton (click)="onClick($event)">You can click me<nav/button> + + +## Event Handling by the EventManager + +Angular handles events using the `EventManager` service. It has a set of plugins that extend the `EventManagerPlugin` abstract class and delegates event subscription and handling to a plugin that supports particular events. + +Angular has a few built-in plugins such as one dedicated to [HammerJS](/api/platform-browser/HammerModule) events or the `KeyEventsPlugin` responsible for handling composite events such as `keydown.enter`. + +### `EventManagerPlugin` + +`EventManagerPlugin` is an abstract class with 2 methods to implement `supports` and `addEventListener`. + +Plugings are loaded using the `EVENT_MANAGER_PLUGINS` injection token and are provided to the `BrowserModule` using `multi: true`. + +For the `EventManager` to determine which plugin will handle the event, each plugin implements the `supports` method. +The event manager will call `addEventListener` on the first plugin where `supports` returns `true`. + +## Handle a custom event + +Let's implement a plugin that will register events and call `Event.stopPropagation()` on them. +This plugin will add the support for the `.stop` modifier and we'll be able to register events like `click.stop`. + +1. `support` needs to return `true` if the stop modifier is present : + + + +2. Wrap the event's handler to `stopPropagration` and register the wrapper. +3. Extract the original event name and registrer the event with the manager. + + + +3. Add the plugin to the list of providers of the app. + + + + +You can now listen to the new custom events via [Event bindings](/guide/event-binding) or the `HostListener` decorator. + + + <navbutton (click.stop)="onClick($event)">Propagation stopped<nav/button> + + + + +Try this . diff --git a/aio/content/guide/event-binding.md b/aio/content/guide/event-binding.md index fabd52c722a93b..faa43c1aa55b74 100644 --- a/aio/content/guide/event-binding.md +++ b/aio/content/guide/event-binding.md @@ -87,5 +87,6 @@ For more information, visit the full reference for [key](https://developer.mozil * [Property binding](guide/property-binding) * [Text interpolation](guide/interpolation) * [Two-way binding](guide/two-way-binding) +* [Handle custom events](guide/custom-events-management) @reviewed 2022-05-10 diff --git a/goldens/public-api/platform-browser/index.md b/goldens/public-api/platform-browser/index.md index df3ccad791ab8b..2461c91adb39c3 100644 --- a/goldens/public-api/platform-browser/index.md +++ b/goldens/public-api/platform-browser/index.md @@ -92,6 +92,15 @@ export class EventManager { static ɵprov: i0.ɵɵInjectableDeclaration; } +// @public +export abstract class EventManagerPlugin { + constructor(_doc: Document); + abstract addEventListener(element: HTMLElement, eventName: string, handler: (event: Event) => void): Function; + // (undocumented) + manager: EventManager; + abstract supports(eventName: string): boolean; +} + // @public export const HAMMER_GESTURE_CONFIG: InjectionToken; diff --git a/packages/platform-browser/src/dom/events/dom_events.ts b/packages/platform-browser/src/dom/events/dom_events.ts index e3826727e3dd55..626e618d630d33 100644 --- a/packages/platform-browser/src/dom/events/dom_events.ts +++ b/packages/platform-browser/src/dom/events/dom_events.ts @@ -23,7 +23,8 @@ export class DomEventsPlugin extends EventManagerPlugin { return true; } - override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { + override addEventListener( + element: HTMLElement, eventName: string, handler: (event: Event) => void): () => void { element.addEventListener(eventName, handler as EventListener, false); return () => this.removeEventListener(element, eventName, handler as EventListener); } diff --git a/packages/platform-browser/src/dom/events/event_manager.ts b/packages/platform-browser/src/dom/events/event_manager.ts index 1784364c688bc4..cd5ae9d2691d05 100644 --- a/packages/platform-browser/src/dom/events/event_manager.ts +++ b/packages/platform-browser/src/dom/events/event_manager.ts @@ -9,7 +9,7 @@ import {Inject, Injectable, InjectionToken, NgZone} from '@angular/core'; /** - * The injection token for the event-manager plug-in service. + * The injection token for plugins of the `EventManager` service. * * @publicApi */ @@ -48,7 +48,7 @@ export class EventManager { */ addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { const plugin = this._findPluginFor(eventName); - return plugin.addEventListener(element, eventName, handler); + return plugin.addEventListener(element, eventName, handler as (event: Event) => void); } /** @@ -77,13 +77,27 @@ export class EventManager { } } +/** + * The plugin definition for the EventManager class + * + * @publicApi + */ export abstract class EventManagerPlugin { - constructor(private _doc: any) {} + // TODO: should be removed in the next major, because it's unused but exposed in the public API by + // KeyEvent plugin. + constructor(private _doc: Document) {} // Using non-null assertion because it's set by EventManager's constructor manager!: EventManager; + /** + * Should return `true` for every event name that should be supported by this plugin + */ abstract supports(eventName: string): boolean; - abstract addEventListener(element: HTMLElement, eventName: string, handler: Function): Function; + /** + * Implement the behaviour for the supported events + */ + abstract addEventListener( + element: HTMLElement, eventName: string, handler: (event: Event) => void): Function; } diff --git a/packages/platform-browser/src/dom/events/hammer_gestures.ts b/packages/platform-browser/src/dom/events/hammer_gestures.ts index 8084b5b81d0917..81688506bc21b4 100644 --- a/packages/platform-browser/src/dom/events/hammer_gestures.ts +++ b/packages/platform-browser/src/dom/events/hammer_gestures.ts @@ -272,7 +272,7 @@ export class HammerGesturesPlugin extends EventManagerPlugin { * HammerJS to detect gesture events. * * Note that applications still need to include the HammerJS script itself. This module - * simply sets up the coordination layer between HammerJS and Angular's EventManager. + * simply sets up the coordination layer between HammerJS and Angular's `EventManager`. * * @publicApi */ diff --git a/packages/platform-browser/src/platform-browser.ts b/packages/platform-browser/src/platform-browser.ts index 2efc7402599ea0..de878f7d926735 100644 --- a/packages/platform-browser/src/platform-browser.ts +++ b/packages/platform-browser/src/platform-browser.ts @@ -73,7 +73,7 @@ export {Title} from './browser/title'; export {disableDebugTools, enableDebugTools} from './browser/tools/tools'; export {By} from './dom/debug/by'; export {REMOVE_STYLES_ON_COMPONENT_DESTROY} from './dom/dom_renderer'; -export {EVENT_MANAGER_PLUGINS, EventManager} from './dom/events/event_manager'; +export {EVENT_MANAGER_PLUGINS, EventManager, EventManagerPlugin} from './dom/events/event_manager'; export {HAMMER_GESTURE_CONFIG, HAMMER_LOADER, HammerGestureConfig, HammerLoader, HammerModule} from './dom/events/hammer_gestures'; export {DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl, SafeValue} from './security/dom_sanitization_service'; export {HydrationFeature, provideClientHydration, HydrationFeatureKind, withNoDomReuse, withNoHttpTransferCache} from './hydration'; diff --git a/packages/platform-server/src/server_events.ts b/packages/platform-server/src/server_events.ts index ea66012b0ed294..cc92ade976d838 100644 --- a/packages/platform-server/src/server_events.ts +++ b/packages/platform-server/src/server_events.ts @@ -8,17 +8,20 @@ import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common'; import {Inject, Injectable} from '@angular/core'; +import {EventManagerPlugin} from '@angular/platform-browser'; @Injectable() -export class ServerEventManagerPlugin /* extends EventManagerPlugin which is private */ { - constructor(@Inject(DOCUMENT) private doc: any) {} +export class ServerEventManagerPlugin extends EventManagerPlugin { + constructor(@Inject(DOCUMENT) private doc: any) { + super(doc); + } // Handle all events on the server. - supports(eventName: string) { + override supports(eventName: string) { return true; } - addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { + override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { return getDOM().onAndCancel(element, eventName, handler); } }