Skip to content

Commit

Permalink
feat(platform-browser): Expose EventManagerPlugin in the public api
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JeanMeche committed Apr 23, 2023
1 parent c029c67 commit ba4972d
Show file tree
Hide file tree
Showing 17 changed files with 201 additions and 11 deletions.
7 changes: 7 additions & 0 deletions 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",
)
Empty file.
19 changes: 19 additions & 0 deletions 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: `
<app-child />
`,
})
export class AppComponent {
name = 'Angular';

@HostListener('click', ['$event'])
onClick(event: Event) {
console.log('click parent', event);
}
}
16 changes: 16 additions & 0 deletions 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: `
<button id="stoping" (click.stop)="onClick($event)">Propagation stopped</button>
<br />
<button id="propagating" (click)="onClick($event)">Propagating...</button>
`,
})
export class ChildComponent {
onClick(event: Event) {
console.log('click child', event?.target);
}
}
26 changes: 26 additions & 0 deletions 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
}
13 changes: 13 additions & 0 deletions aio/content/examples/custom-events/src/index.html
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ContentProjection</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>
13 changes: 13 additions & 0 deletions 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
});
10 changes: 10 additions & 0 deletions 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"]
}
1 change: 1 addition & 0 deletions aio/content/examples/examples.bzl
Expand Up @@ -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},
Expand Down
57 changes: 57 additions & 0 deletions 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.

<code-example header="src/app/stop-event-plugin.ts" path="attribute-directives/src/app/highlight.directive.2.ts" region="mouse-methods"></code-example>

<code-example language="html">
&lt;navbutton (click)="onClick($event)"&gt;You can click me&lt;nav/button&gt;
</code-example>

## 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 :

<code-example header="src/app/stop-event-plugin.ts (supports method)" path="custom-events/src/app/stop-event-plugin.ts" region="supports"></code-example>

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.

<code-example header="src/app/stop-event-plugin.ts (addEventListener method)" path="custom-events/src/app/stop-event-plugin.ts" region="addEventListener"></code-example>

3. Add the plugin to the list of providers of the app.

<code-example header="src/main.ts" path="custom-events/src/main.ts" region="providers"></code-example>


You can now listen to the new custom events via [Event bindings](/guide/event-binding) or the `HostListener` decorator.

<code-example language="html">
&lt;navbutton (click.stop)="onClick($event)"&gt;Propagation stopped&lt;nav/button&gt;
</code-example>



Try this <live-example name="custom-events"></live-example>.
1 change: 1 addition & 0 deletions aio/content/guide/event-binding.md
Expand Up @@ -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
9 changes: 9 additions & 0 deletions goldens/public-api/platform-browser/index.md
Expand Up @@ -92,6 +92,15 @@ export class EventManager {
static ɵprov: i0.ɵɵInjectableDeclaration<EventManager>;
}

// @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<HammerGestureConfig>;

Expand Down
3 changes: 2 additions & 1 deletion packages/platform-browser/src/dom/events/dom_events.ts
Expand Up @@ -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);
}
Expand Down
22 changes: 18 additions & 4 deletions packages/platform-browser/src/dom/events/event_manager.ts
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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;
}
Expand Up @@ -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
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-browser/src/platform-browser.ts
Expand Up @@ -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';
Expand Down
11 changes: 7 additions & 4 deletions packages/platform-server/src/server_events.ts
Expand Up @@ -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);
}
}

0 comments on commit ba4972d

Please sign in to comment.