refactor(core): Rename BaseDispatcher to Dispatcher. (#55721)

Rename `BaseDispatcher` to `Dispatcher` and `Dispatcher` to
`LegacyDispatcher`. The `GlobalHandler` type and `stopPropagation`
function needs to be left for now in dispatcher.ts as it was not
exported previously from legacy_dispatcher.ts.

PR Close #55721
This commit is contained in:
Tom Wilkinson 2024-05-07 17:15:27 -05:00 committed by Andrew Scott
parent 7187394ffe
commit 0cb50317e1
10 changed files with 415 additions and 395 deletions

View file

@ -5,18 +5,16 @@
```ts
// @public
export class BaseDispatcher {
export function bootstrapEarlyEventContract(field: string, container: HTMLElement, appId: string, eventTypes?: string[], captureEventTypes?: string[], earlyJsactionTracker?: EventContractTracker<EarlyJsactionDataContainer>): void;
// @public
export class Dispatcher {
constructor(dispatchDelegate: (eventInfoWrapper: EventInfoWrapper) => void, { eventReplayer }?: {
eventReplayer?: Replayer;
});
dispatch(eventInfo: EventInfo): void;
queueEventInfoWrapper(eventInfoWrapper: EventInfoWrapper): void;
scheduleEventReplay(): void;
}
// @public
export function bootstrapEarlyEventContract(field: string, container: HTMLElement, appId: string, eventTypes?: string[], captureEventTypes?: string[], earlyJsactionTracker?: EventContractTracker<EarlyJsactionDataContainer>): void;
// @public (undocumented)
export interface EarlyJsactionDataContainer {
// (undocumented)
@ -33,12 +31,12 @@ export class EventContract implements UnrenamedEventContract {
cleanUp(): void;
// (undocumented)
ecaacs?: (updateEventInfoForA11yClick: typeof a11yClickLib.updateEventInfoForA11yClick, preventDefaultForA11yClick: typeof a11yClickLib.preventDefaultForA11yClick, populateClickOnlyAction: typeof a11yClickLib.populateClickOnlyAction) => void;
ecrd(dispatcher: Dispatcher, restriction: Restriction): void;
ecrd(dispatcher: Dispatcher_2, restriction: Restriction): void;
exportAddA11yClickSupport(): void;
handler(eventType: string): EventHandler | undefined;
// (undocumented)
static MOUSE_SPECIAL_SUPPORT: boolean;
registerDispatcher(dispatcher: Dispatcher, restriction: Restriction): void;
registerDispatcher(dispatcher: Dispatcher_2, restriction: Restriction): void;
replayEarlyEvents(earlyJsactionContainer?: EarlyJsactionDataContainer): void;
}
@ -99,7 +97,7 @@ export class EventInfoWrapper {
}
// @public
export function registerDispatcher(eventContract: UnrenamedEventContract, dispatcher: BaseDispatcher): void;
export function registerDispatcher(eventContract: UnrenamedEventContract, dispatcher: Dispatcher): void;
// (No @packageDocumentation comment for this package)

View file

@ -20,7 +20,7 @@ This can introduce a couple problems:
1. Server rendered applications will silently ignore user events that happen
before the app hydrates and registers handlers
```html
<!-- Let's say this server-rendered page is streamed to the browser -->
<body>
@ -45,14 +45,14 @@ This can introduce a couple problems:
2. Applications must eagerly load any possible handler that could be needed to
handle user interactions, even if that handler is never invoked or even
rendered on the page
```html
// This button is rarely clicked, but the code to show the dialog must be
// loaded for every user
<button type="button" (click)="showAdvancedOptionsDialog()">
Advanced options
</button>
// Non-admins will never see this button, and yet they still have to load
// this handler.
@if (isAdmin) {
@ -61,7 +61,7 @@ This can introduce a couple problems:
</button>
}
```
It's possible to write these handlers so that they will late-load their
inner logic, but that's a manual, opt-in solution.
@ -114,13 +114,13 @@ valid-char = character - invalid-name-chars
invalid-name-chars = ":" | ";" | "."
```
- Omitting the event type and colon will default the binding to the `click`
event (e.g.`jsaction="handleClick;hover:handleHover"`)
- Both the event type and handler name can be the empty string (but make sure
to keep the colon: `jsaction="change:;"`)
- The `event-handler` is an arbitrary string that can store metadata needed to
find the handler that handles the event. The user of JSAction can choose to
define the semantics of the handler string however they like.
- Omitting the event type and colon will default the binding to the `click`
event (e.g.`jsaction="handleClick;hover:handleHover"`)
- Both the event type and handler name can be the empty string (but make sure
to keep the colon: `jsaction="change:;"`)
- The `event-handler` is an arbitrary string that can store metadata needed to
find the handler that handles the event. The user of JSAction can choose to
define the semantics of the handler string however they like.
#### Example
@ -200,10 +200,8 @@ Finally, once your application is bootstrapped and ready to handle events,
you'll need to create a `Dispatcher` and register it with the `EventContract`
that has been queueing events.
<!-- TODO(b/337878694): Update this once the `BaseDispatcher` is renamed to be just `Dispatcher` -->
```javascript
import {BaseDispatcher as Dispatcher, registerDispatcher} from '@angular/core/primitives/event-dispatch/src/base_dispatcher';
import {Dispatcher, registerDispatcher} from '@angular/core/primitives/event-dispatch';
function handleEvent(eventInfoWrapper) {
// eventInfoWrapper contains all the information about the event
@ -246,16 +244,16 @@ some tradeoffs to doing this:
Pros of cleaning up event contract:
- Native handlers avoid the [quirks](#known-caveats) of JSAction dispatching
- Native handlers avoid the [quirks](#known-caveats) of JSAction dispatching
Pros of keeping event contract:
- JSAction's event delegation drastically reduces the number of event
listeners registered with the browser. In extreme cases, registering
thousands of listeners in your app can be noticably slow.
- There may be slight behavior differences when your event is dispatched via
JSAction vs native event listeners. Always using JSAction dispatch keeps
things consistent.
- JSAction's event delegation drastically reduces the number of event
listeners registered with the browser. In extreme cases, registering
thousands of listeners in your app can be noticably slow.
- There may be slight behavior differences when your event is dispatched via
JSAction vs native event listeners. Always using JSAction dispatch keeps
things consistent.
<!-- end list -->
@ -269,4 +267,4 @@ Because JSAction may potentially replay queued events some time after the events
originally fired, certain APIs like `e.preventDefault()` or
`e.stopPropagation()` won't function correctly.
<!-- TODO: Add a comprehensive list of known behavior differences for both replayed and delegated events. There are also plans to emulate some browser behavior (i.e. stopPropagation) that may fix some of these. -->
<!-- TODO: Add a comprehensive list of known behavior differences for both replayed and delegated events. There are also plans to emulate some browser behavior (i.e. stopPropagation) that may fix some of these. -->

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
export {BaseDispatcher, registerDispatcher} from './src/base_dispatcher';
export {Dispatcher, registerDispatcher} from './src/dispatcher';
export {EventContractContainer} from './src/event_contract_container';
export type {EarlyJsactionDataContainer} from './src/earlyeventcontract';
export {EventContract} from './src/eventcontract';

View file

@ -1,118 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {EventInfo, EventInfoWrapper} from './event_info';
import {UnrenamedEventContract} from './eventcontract';
import {Restriction} from './restriction';
/**
* A replayer is a function that is called when there are queued events,
* either from the `EventContract` or when there are no detected handlers.
*/
export type Replayer = (eventInfoWrappers: EventInfoWrapper[]) => void;
/**
* A handler is dispatched to during normal handling.
*/
export type EventInfoWrapperHandler = (eventInfoWrapper: EventInfoWrapper) => void;
/**
* Receives a DOM event, determines the jsaction associated with the source
* element of the DOM event, and invokes the handler associated with the
* jsaction.
*/
export class BaseDispatcher {
/** The queue of events. */
private readonly queuedEventInfoWrappers: EventInfoWrapper[] = [];
/** The replayer function to be called when there are queued events. */
private eventReplayer?: Replayer;
/** Whether the event replay is scheduled. */
private eventReplayScheduled = false;
/**
* Options are:
* 1. `eventReplayer`: When the event contract dispatches replay events
* to the Dispatcher, the Dispatcher collects them and in the next tick
* dispatches them to the `eventReplayer`.
* @param dispatchDelegate A function that should handle dispatching an `EventInfoWrapper` to handlers.
*/
constructor(
private readonly dispatchDelegate: (eventInfoWrapper: EventInfoWrapper) => void,
{eventReplayer = undefined}: {eventReplayer?: Replayer} = {},
) {
this.eventReplayer = eventReplayer;
}
/**
* Receives an event or the event queue from the EventContract. The event
* queue is copied and it attempts to replay.
* If event info is passed in it looks for an action handler that can handle
* the given event. If there is no handler registered queues the event and
* checks if a loader is registered for the given namespace. If so, calls it.
*
* Alternatively, if in global dispatch mode, calls all registered global
* handlers for the appropriate event type.
*
* The three functionalities of this call are deliberately not split into
* three methods (and then declared as an abstract interface), because the
* interface is used by EventContract, which lives in a different jsbinary.
* Therefore the interface between the three is defined entirely in terms that
* are invariant under jscompiler processing (Function and Array, as opposed
* to a custom type with method names).
*
* @param eventInfo The info for the event that triggered this call or the
* queue of events from EventContract.
*/
dispatch(eventInfo: EventInfo): void {
const eventInfoWrapper = new EventInfoWrapper(eventInfo);
if (eventInfoWrapper.getIsReplay()) {
if (!this.eventReplayer) {
return;
}
this.queueEventInfoWrapper(eventInfoWrapper);
this.scheduleEventReplay();
return;
}
this.dispatchDelegate(eventInfoWrapper);
}
/** Queue an `EventInfoWrapper` for replay. */
queueEventInfoWrapper(eventInfoWrapper: EventInfoWrapper) {
this.queuedEventInfoWrappers.push(eventInfoWrapper);
}
/**
* Replays queued events, if any. The replaying will happen in its own
* stack once the current flow cedes control. This is done to mimic
* browser event handling.
*/
scheduleEventReplay() {
if (
this.eventReplayScheduled ||
!this.eventReplayer ||
this.queuedEventInfoWrappers.length === 0
) {
return;
}
this.eventReplayScheduled = true;
Promise.resolve().then(() => {
this.eventReplayScheduled = false;
this.eventReplayer!(this.queuedEventInfoWrappers);
});
}
}
/**
* Registers deferred functionality for an EventContract and a Jsaction
* Dispatcher.
*/
export function registerDispatcher(
eventContract: UnrenamedEventContract,
dispatcher: BaseDispatcher,
) {
eventContract.ecrd((eventInfo: EventInfo) => {
dispatcher.dispatch(eventInfo);
}, Restriction.I_AM_THE_JSACTION_FRAMEWORK);
}

View file

@ -6,15 +6,22 @@
* found in the LICENSE file at https://angular.io/license
*/
import {BaseDispatcher, EventInfoWrapperHandler} from './base_dispatcher';
import {Char} from './char';
import * as eventLib from './event';
import {EventInfo, EventInfoWrapper} from './event_info';
import {EventType} from './event_type';
import {UnrenamedEventContract} from './eventcontract';
import {Restriction} from './restriction';
import {UnrenamedEventContract} from './eventcontract';
import * as eventLib from './event';
export type {EventInfoWrapperHandler as EventInfoHandler} from './base_dispatcher';
/**
* A replayer is a function that is called when there are queued events,
* either from the `EventContract` or when there are no detected handlers.
*/
export type Replayer = (eventInfoWrappers: EventInfoWrapper[]) => void;
/**
* A handler is dispatched to during normal handling.
*/
export type EventInfoHandler = (eventInfoWrapper: EventInfoWrapper) => void;
/**
* A global handler is dispatched to before normal handler dispatch. Returning
@ -22,65 +29,31 @@ export type {EventInfoWrapperHandler as EventInfoHandler} from './base_dispatche
*/
export type GlobalHandler = (event: Event) => boolean | void;
/**
* A replayer is a function that is called when there are queued events,
* either from the `EventContract` or when there are no detected handlers.
*/
export type Replayer = (eventInfoWrappers: EventInfoWrapper[], dispatcher: Dispatcher) => void;
/**
* Receives a DOM event, determines the jsaction associated with the source
* element of the DOM event, and invokes the handler associated with the
* jsaction.
*/
export class Dispatcher {
private readonly baseDispatcher: BaseDispatcher;
/** Whether to stop propagation for an `EventInfo`. */
private readonly stopPropagation: boolean;
/**
* The actions that are registered for this Dispatcher instance.
* This should be the primary one used once migration off of registerHandlers
* is done.
*/
private readonly actions: {[key: string]: EventInfoWrapperHandler} = {};
/** A map of global event handlers, where each key is an event type. */
private readonly globalHandlers = new Map<string, Set<GlobalHandler>>();
/** The event replayer. */
/** The queue of events. */
private readonly replayEventInfoWrappers: EventInfoWrapper[] = [];
/** The replayer function to be called when there are queued events. */
private eventReplayer?: Replayer;
/** Whether the event replay is scheduled. */
private eventReplayScheduled = false;
/**
* Receives a DOM event, determines the jsaction associated with the source
* element of the DOM event, and invokes the handler associated with the
* jsaction.
*
* @param getHandler A function that knows how to get the handler for a
* given event info.
* Options are:
* 1. `eventReplayer`: When the event contract dispatches replay events
* to the Dispatcher, the Dispatcher collects them and in the next tick
* dispatches them to the `eventReplayer`.
* @param dispatchDelegate A function that should handle dispatching an `EventInfoWrapper` to handlers.
*/
constructor(
private readonly getHandler?: (
eventInfoWrapper: EventInfoWrapper,
) => EventInfoWrapperHandler | void,
{
stopPropagation = false,
eventReplayer = undefined,
}: {stopPropagation?: boolean; eventReplayer?: Replayer} = {},
private readonly dispatchDelegate: (eventInfoWrapper: EventInfoWrapper) => void,
{eventReplayer = undefined}: {eventReplayer?: Replayer} = {},
) {
this.eventReplayer = eventReplayer;
this.baseDispatcher = new BaseDispatcher(
(eventInfoWrapper: EventInfoWrapper) => {
this.dispatchToHandler(eventInfoWrapper);
},
{
eventReplayer: (eventInfoWrappers) => {
this.eventReplayer?.(eventInfoWrappers, this);
},
},
);
this.stopPropagation = stopPropagation;
}
/**
@ -103,185 +76,33 @@ export class Dispatcher {
* @param eventInfo The info for the event that triggered this call or the
* queue of events from EventContract.
*/
dispatch(eventInfo: EventInfo, isGlobalDispatch?: boolean): void {
this.baseDispatcher.dispatch(eventInfo);
}
/**
* Dispatches an `EventInfoWrapper`.
*/
private dispatchToHandler(eventInfoWrapper: EventInfoWrapper) {
if (this.globalHandlers.size) {
const globalEventInfoWrapper = eventInfoWrapper.clone();
// In some cases, `populateAction` will rewrite `click` events to
// `clickonly`. Revert back to a regular click, otherwise we won't be able
// to execute global event handlers registered on click events.
if (globalEventInfoWrapper.getEventType() === EventType.CLICKONLY) {
globalEventInfoWrapper.setEventType(EventType.CLICK);
dispatch(eventInfo: EventInfo): void {
const eventInfoWrapper = new EventInfoWrapper(eventInfo);
if (eventInfoWrapper.getIsReplay()) {
if (!this.eventReplayer) {
return;
}
// Skip everything related to jsaction handlers, and execute the global
// handlers.
const event = globalEventInfoWrapper.getEvent();
const eventTypeHandlers = this.globalHandlers.get(globalEventInfoWrapper.getEventType());
let shouldPreventDefault = false;
if (eventTypeHandlers) {
for (const handler of eventTypeHandlers) {
if (handler(event) === false) {
shouldPreventDefault = true;
}
}
}
if (shouldPreventDefault) {
eventLib.preventDefault(event);
}
}
const action = eventInfoWrapper.getAction();
if (!action) {
this.scheduleEventInfoWrapperReplay(eventInfoWrapper);
return;
}
this.dispatchDelegate(eventInfoWrapper);
}
if (this.stopPropagation) {
stopPropagation(eventInfoWrapper);
}
let handler: EventInfoWrapperHandler | void = undefined;
if (this.getHandler) {
handler = this.getHandler(eventInfoWrapper);
}
if (!handler) {
handler = this.actions[action.name];
}
if (handler) {
handler(eventInfoWrapper);
/**
* Schedules an `EventInfoWrapper` for replay. The replaying will happen in its own
* stack once the current flow cedes control. This is done to mimic
* browser event handling.
*/
private scheduleEventInfoWrapperReplay(eventInfoWrapper: EventInfoWrapper) {
this.replayEventInfoWrappers.push(eventInfoWrapper);
if (this.eventReplayScheduled || !this.eventReplayer) {
return;
}
// No handler was found.
this.baseDispatcher.queueEventInfoWrapper(eventInfoWrapper);
}
/**
* Registers multiple methods all bound to the same object
* instance. This is a common case: an application module binds
* multiple of its methods under public names to the event contract of
* the application. So we provide a shortcut for it.
* Attempts to replay the queued events after registering the handlers.
*
* @param namespace The namespace of the jsaction name.
*
* @param instance The object to bind the methods to. If this is null, then
* the functions are not bound, but directly added under the public names.
*
* @param methods A map from public name to functions that will be bound to
* instance and registered as action under the public name. I.e. the
* property names are the public names. The property values are the
* methods of instance.
*/
registerEventInfoHandlers<T>(
namespace: string,
instance: T | null,
methods: {[key: string]: EventInfoWrapperHandler},
) {
for (const [name, method] of Object.entries(methods)) {
const handler = instance ? method.bind(instance) : method;
if (namespace) {
// Include a '.' separator between namespace name and action name.
// In the case that no namespace name is provided, the jsaction name
// consists of the action name only (no period).
const fullName = namespace + Char.NAMESPACE_ACTION_SEPARATOR + name;
this.actions[fullName] = handler;
} else {
this.actions[name] = handler;
}
}
this.baseDispatcher.scheduleEventReplay();
}
/**
* Unregisters an action. Provided as an easy way to reverse the effects of
* registerHandlers.
* @param namespace The namespace of the jsaction name.
* @param name The action name to unbind.
*/
unregisterHandler(namespace: string, name: string) {
const fullName = namespace ? namespace + Char.NAMESPACE_ACTION_SEPARATOR + name : name;
delete this.actions[fullName];
}
/** Registers a global event handler. */
registerGlobalHandler(eventType: string, handler: GlobalHandler) {
if (!this.globalHandlers.has(eventType)) {
this.globalHandlers.set(eventType, new Set<GlobalHandler>([handler]));
} else {
this.globalHandlers.get(eventType)!.add(handler);
}
}
/** Unregisters a global event handler. */
unregisterGlobalHandler(eventType: string, handler: GlobalHandler) {
if (this.globalHandlers.has(eventType)) {
this.globalHandlers.get(eventType)!.delete(handler);
}
}
/**
* Checks whether there is an action registered under the given
* name. This returns true if there is a namespace handler, even
* if it can not yet handle the event.
*
* @param name Action name.
* @return Whether the name is registered.
* @see #canDispatch
*/
hasAction(name: string): boolean {
return this.actions.hasOwnProperty(name);
}
/**
* Whether this dispatcher can dispatch the event. This can be used by
* event replayer to check whether the dispatcher can replay an event.
*/
canDispatch(eventInfoWrapper: EventInfoWrapper): boolean {
const action = eventInfoWrapper.getAction();
if (!action) {
return false;
}
return this.hasAction(action.name);
}
/**
* Sets the event replayer, enabling queued events to be replayed when actions
* are bound. To replay events, you must register the dispatcher to the
* contract after setting the `EventReplayer`. The event replayer takes as
* parameters the queue of events and the dispatcher (used to check whether
* actions have handlers registered and can be replayed). The event replayer
* is also responsible for dequeuing events.
*
* Example: An event replayer that replays only the last event.
*
* const dispatcher = new Dispatcher();
* // ...
* dispatcher.setEventReplayer((queue, dispatcher) => {
* const lastEventInfoWrapper = queue[queue.length -1];
* if (dispatcher.canDispatch(lastEventInfoWrapper.getAction())) {
* jsaction.replay.replayEvent(
* lastEventInfoWrapper.getEvent(),
* lastEventInfoWrapper.getTargetElement(),
* lastEventInfoWrapper.getEventType(),
* );
* queue.length = 0;
* }
* });
*
* @param eventReplayer It allows elements to be replayed and dequeuing.
*/
setEventReplayer(eventReplayer: Replayer) {
this.eventReplayer = eventReplayer;
this.eventReplayScheduled = true;
Promise.resolve().then(() => {
this.eventReplayScheduled = false;
this.eventReplayer!(this.replayEventInfoWrappers);
});
}
}

View file

@ -6,4 +6,325 @@
* found in the LICENSE file at https://angular.io/license
*/
export {Dispatcher as LegacyDispatcher, registerDispatcher} from './dispatcher';
import {
Dispatcher,
EventInfoHandler as EventInfoWrapperHandler,
GlobalHandler,
stopPropagation,
} from './dispatcher';
import {Char} from './char';
import * as eventLib from './event';
import {EventInfo, EventInfoWrapper} from './event_info';
import {EventType} from './event_type';
import {UnrenamedEventContract} from './eventcontract';
import {Restriction} from './restriction';
/** Re-exports that should eventually be moved into this file. */
export type {
EventInfoHandler,
EventInfoHandler as EventInfoWrapperHandler,
GlobalHandler,
} from './dispatcher';
export {stopPropagation} from './dispatcher';
/**
* A replayer is a function that is called when there are queued events,
* either from the `EventContract` or when there are no detected handlers.
*/
export type Replayer = (
eventInfoWrappers: EventInfoWrapper[],
dispatcher: LegacyDispatcher,
) => void;
/**
* Receives a DOM event, determines the jsaction associated with the source
* element of the DOM event, and invokes the handler associated with the
* jsaction.
*/
export class LegacyDispatcher {
private readonly dispatcher: Dispatcher;
/** Whether to stop propagation for an `EventInfo`. */
private readonly stopPropagation: boolean;
/**
* The actions that are registered for this Dispatcher instance.
* This should be the primary one used once migration off of registerHandlers
* is done.
*/
private readonly actions: {[key: string]: EventInfoWrapperHandler} = {};
/** A map of global event handlers, where each key is an event type. */
private readonly globalHandlers = new Map<string, Set<GlobalHandler>>();
/** The event replayer. */
private eventReplayer?: Replayer;
/** The event infos that have be replayed. */
private eventInfoWrapperQueue?: EventInfoWrapper[];
/** Whether event replay is scheduled. */
private eventReplayScheduled: boolean = false;
/**
* Receives a DOM event, determines the jsaction associated with the source
* element of the DOM event, and invokes the handler associated with the
* jsaction.
*
* @param getHandler A function that knows how to get the handler for a
* given event info.
*/
constructor(
private readonly getHandler?: (
eventInfoWrapper: EventInfoWrapper,
) => EventInfoWrapperHandler | void,
{
stopPropagation = false,
eventReplayer = undefined,
}: {stopPropagation?: boolean; eventReplayer?: Replayer} = {},
) {
this.eventReplayer = eventReplayer;
this.dispatcher = new Dispatcher(
(eventInfoWrapper: EventInfoWrapper) => {
this.dispatchToHandler(eventInfoWrapper);
},
{
eventReplayer: (eventInfoWrappers) => {
this.eventInfoWrapperQueue = eventInfoWrappers;
this.eventReplayer?.(this.eventInfoWrapperQueue, this);
},
},
);
this.stopPropagation = stopPropagation;
}
/**
* Receives an event or the event queue from the EventContract. The event
* queue is copied and it attempts to replay.
* If event info is passed in it looks for an action handler that can handle
* the given event. If there is no handler registered queues the event and
* checks if a loader is registered for the given namespace. If so, calls it.
*
* Alternatively, if in global dispatch mode, calls all registered global
* handlers for the appropriate event type.
*
* The three functionalities of this call are deliberately not split into
* three methods (and then declared as an abstract interface), because the
* interface is used by EventContract, which lives in a different jsbinary.
* Therefore the interface between the three is defined entirely in terms that
* are invariant under jscompiler processing (Function and Array, as opposed
* to a custom type with method names).
*
* @param eventInfo The info for the event that triggered this call or the
* queue of events from EventContract.
*/
dispatch(eventInfo: EventInfo, isGlobalDispatch?: boolean): void {
this.dispatcher.dispatch(eventInfo);
}
/**
* Dispatches an `EventInfoWrapper`.
*/
private dispatchToHandler(eventInfoWrapper: EventInfoWrapper) {
if (this.globalHandlers.size) {
const globalEventInfoWrapper = eventInfoWrapper.clone();
// In some cases, `populateAction` will rewrite `click` events to
// `clickonly`. Revert back to a regular click, otherwise we won't be able
// to execute global event handlers registered on click events.
if (globalEventInfoWrapper.getEventType() === EventType.CLICKONLY) {
globalEventInfoWrapper.setEventType(EventType.CLICK);
}
// Skip everything related to jsaction handlers, and execute the global
// handlers.
const event = globalEventInfoWrapper.getEvent();
const eventTypeHandlers = this.globalHandlers.get(globalEventInfoWrapper.getEventType());
let shouldPreventDefault = false;
if (eventTypeHandlers) {
for (const handler of eventTypeHandlers) {
if (handler(event) === false) {
shouldPreventDefault = true;
}
}
}
if (shouldPreventDefault) {
eventLib.preventDefault(event);
}
}
const action = eventInfoWrapper.getAction();
if (!action) {
return;
}
if (this.stopPropagation) {
stopPropagation(eventInfoWrapper);
}
let handler: EventInfoWrapperHandler | void = undefined;
if (this.getHandler) {
handler = this.getHandler(eventInfoWrapper);
}
if (!handler) {
handler = this.actions[action.name];
}
if (handler) {
handler(eventInfoWrapper);
return;
}
// No handler was found.
this.eventInfoWrapperQueue?.push(eventInfoWrapper);
}
/**
* Registers multiple methods all bound to the same object
* instance. This is a common case: an application module binds
* multiple of its methods under public names to the event contract of
* the application. So we provide a shortcut for it.
* Attempts to replay the queued events after registering the handlers.
*
* @param namespace The namespace of the jsaction name.
*
* @param instance The object to bind the methods to. If this is null, then
* the functions are not bound, but directly added under the public names.
*
* @param methods A map from public name to functions that will be bound to
* instance and registered as action under the public name. I.e. the
* property names are the public names. The property values are the
* methods of instance.
*/
registerEventInfoHandlers<T>(
namespace: string,
instance: T | null,
methods: {[key: string]: EventInfoWrapperHandler},
) {
for (const [name, method] of Object.entries(methods)) {
const handler = instance ? method.bind(instance) : method;
if (namespace) {
// Include a '.' separator between namespace name and action name.
// In the case that no namespace name is provided, the jsaction name
// consists of the action name only (no period).
const fullName = namespace + Char.NAMESPACE_ACTION_SEPARATOR + name;
this.actions[fullName] = handler;
} else {
this.actions[name] = handler;
}
}
this.scheduleEventReplay();
}
/**
* Unregisters an action. Provided as an easy way to reverse the effects of
* registerHandlers.
* @param namespace The namespace of the jsaction name.
* @param name The action name to unbind.
*/
unregisterHandler(namespace: string, name: string) {
const fullName = namespace ? namespace + Char.NAMESPACE_ACTION_SEPARATOR + name : name;
delete this.actions[fullName];
}
/** Registers a global event handler. */
registerGlobalHandler(eventType: string, handler: GlobalHandler) {
if (!this.globalHandlers.has(eventType)) {
this.globalHandlers.set(eventType, new Set<GlobalHandler>([handler]));
} else {
this.globalHandlers.get(eventType)!.add(handler);
}
}
/** Unregisters a global event handler. */
unregisterGlobalHandler(eventType: string, handler: GlobalHandler) {
if (this.globalHandlers.has(eventType)) {
this.globalHandlers.get(eventType)!.delete(handler);
}
}
/**
* Checks whether there is an action registered under the given
* name. This returns true if there is a namespace handler, even
* if it can not yet handle the event.
*
* @param name Action name.
* @return Whether the name is registered.
* @see #canDispatch
*/
hasAction(name: string): boolean {
return this.actions.hasOwnProperty(name);
}
/**
* Whether this dispatcher can dispatch the event. This can be used by
* event replayer to check whether the dispatcher can replay an event.
*/
canDispatch(eventInfoWrapper: EventInfoWrapper): boolean {
const action = eventInfoWrapper.getAction();
if (!action) {
return false;
}
return this.hasAction(action.name);
}
/**
* Sets the event replayer, enabling queued events to be replayed when actions
* are bound. To replay events, you must register the dispatcher to the
* contract after setting the `EventReplayer`. The event replayer takes as
* parameters the queue of events and the dispatcher (used to check whether
* actions have handlers registered and can be replayed). The event replayer
* is also responsible for dequeuing events.
*
* Example: An event replayer that replays only the last event.
*
* const dispatcher = new Dispatcher();
* // ...
* dispatcher.setEventReplayer((queue, dispatcher) => {
* const lastEventInfoWrapper = queue[queue.length -1];
* if (dispatcher.canDispatch(lastEventInfoWrapper.getAction())) {
* jsaction.replay.replayEvent(
* lastEventInfoWrapper.getEvent(),
* lastEventInfoWrapper.getTargetElement(),
* lastEventInfoWrapper.getEventType(),
* );
* queue.length = 0;
* }
* });
*
* @param eventReplayer It allows elements to be replayed and dequeuing.
*/
setEventReplayer(eventReplayer: Replayer) {
this.eventReplayer = eventReplayer;
}
/**
* Replays queued events, if any. The replaying will happen in its own
* stack once the current flow cedes control. This is done to mimic
* browser event handling.
*/
private scheduleEventReplay() {
if (this.eventReplayScheduled || !this.eventReplayer || !this.eventInfoWrapperQueue?.length) {
return;
}
this.eventReplayScheduled = true;
Promise.resolve().then(() => {
this.eventReplayScheduled = false;
this.eventReplayer!(this.eventInfoWrapperQueue!, this);
});
}
}
/**
* Registers deferred functionality for an EventContract and a Jsaction
* Dispatcher.
*/
export function registerDispatcher(
eventContract: UnrenamedEventContract,
dispatcher: LegacyDispatcher,
) {
eventContract.ecrd((eventInfo: EventInfo) => {
dispatcher.dispatch(eventInfo);
}, Restriction.I_AM_THE_JSACTION_FRAMEWORK);
}

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Dispatcher, Replayer} from '../src/dispatcher';
import {LegacyDispatcher, Replayer} from '../src/legacy_dispatcher';
import {
ActionInfo,
createEventInfo,
@ -63,7 +63,7 @@ describe('dispatcher test.ts', () => {
eventInfo = event;
};
const dispatcher = new Dispatcher();
const dispatcher = new LegacyDispatcher();
const actions = {'bar': actionHandler};
dispatcher.registerEventInfoHandlers('foo', null, actions);
@ -84,7 +84,7 @@ describe('dispatcher test.ts', () => {
const getEventInfoHandler = () => eventInfoHandler1;
const eventInfoHandler2 = jasmine.createSpy('eventInfoHandler2');
const dispatcher = new Dispatcher(/* getHandler= */ getEventInfoHandler);
const dispatcher = new LegacyDispatcher(/* getHandler= */ getEventInfoHandler);
const eventInfoHandlers = {'bar': eventInfoHandler2};
dispatcher.registerEventInfoHandlers('bar', null, eventInfoHandlers);
@ -98,7 +98,7 @@ describe('dispatcher test.ts', () => {
});
it('registered EventInfo handlers are found with hasAction', () => {
const dispatcher = new Dispatcher();
const dispatcher = new LegacyDispatcher();
dispatcher.registerEventInfoHandlers('', null, {
'foo': () => {},
@ -110,7 +110,7 @@ describe('dispatcher test.ts', () => {
});
it('EventInfo handlers can be unregistered', () => {
const dispatcher = new Dispatcher();
const dispatcher = new LegacyDispatcher();
dispatcher.registerEventInfoHandlers('prefix', null, {
'clickaction': () => {},
@ -133,7 +133,7 @@ describe('dispatcher test.ts', () => {
}
it('global event dispatch is not replayed', async () => {
const dispatcher = new Dispatcher();
const dispatcher = new LegacyDispatcher();
const eventReplayer = jasmine.createSpy('eventReplayer');
dispatcher.setEventReplayer(eventReplayer);
dispatcher.registerEventInfoHandlers('foo', null, {'bar': () => {}});
@ -151,7 +151,7 @@ describe('dispatcher test.ts', () => {
function expectEventReplayerToHaveBeenCalledWith(
eventReplayer: jasmine.Spy<Replayer>,
expectedEventInfos: EventInfo[],
expectedDispatcher: Dispatcher,
expectedDispatcher: LegacyDispatcher,
) {
const args = eventReplayer.calls.mostRecent().args;
expect(args.length).toBe(2);
@ -163,7 +163,7 @@ describe('dispatcher test.ts', () => {
}
it('events are collected and replayed', async () => {
const dispatcher = new Dispatcher();
const dispatcher = new LegacyDispatcher();
const eventReplayer = jasmine.createSpy<Replayer>('eventReplayer');
dispatcher.setEventReplayer(eventReplayer);
dispatcher.registerEventInfoHandlers('foo', null, {'bar': () => {}});
@ -181,7 +181,7 @@ describe('dispatcher test.ts', () => {
});
it('events are replayed when handlers are registered', async () => {
const dispatcher = new Dispatcher();
const dispatcher = new LegacyDispatcher();
const eventReplayer = jasmine.createSpy<Replayer>('eventReplayer');
dispatcher.setEventReplayer(eventReplayer);
let replayed = waitForEventReplayer(eventReplayer);
@ -202,7 +202,7 @@ describe('dispatcher test.ts', () => {
it('dispatches to registered global EventInfo handler', () => {
const handler = jasmine.createSpy('handler');
const dispatcher = new Dispatcher();
const dispatcher = new LegacyDispatcher();
dispatcher.registerGlobalHandler('click', handler);
const eventInfo = createTestEventInfo();
@ -214,7 +214,7 @@ describe('dispatcher test.ts', () => {
it('does not dispatch to non-matching registered global EventInfo handler', () => {
const handler = jasmine.createSpy('handler');
const dispatcher = new Dispatcher();
const dispatcher = new LegacyDispatcher();
dispatcher.registerGlobalHandler('click', handler);
const eventInfo = createTestEventInfo({
@ -232,7 +232,7 @@ describe('dispatcher test.ts', () => {
window.document.body.appendChild(container);
const targetElement = document.createElement('div');
container.appendChild(targetElement);
const dispatcher = new Dispatcher();
const dispatcher = new LegacyDispatcher();
const targetHandler = jasmine.createSpy('targetHandler');
targetHandler.and.callFake((event) => {
@ -262,7 +262,7 @@ describe('dispatcher test.ts', () => {
window.document.body.appendChild(container);
const targetElement = document.createElement('div');
container.appendChild(targetElement);
const dispatcher = new Dispatcher(undefined, {stopPropagation: true});
const dispatcher = new LegacyDispatcher(undefined, {stopPropagation: true});
const targetHandler = jasmine.createSpy('targetHandler');
targetHandler.and.callFake((event) => {
@ -292,7 +292,7 @@ describe('dispatcher test.ts', () => {
window.document.body.appendChild(container);
const targetElement = document.createElement('div');
container.appendChild(targetElement);
const dispatcher = new Dispatcher(undefined, {stopPropagation: true});
const dispatcher = new LegacyDispatcher(undefined, {stopPropagation: true});
const targetHandler = jasmine.createSpy('targetHandler');
targetHandler.and.callFake((event) => {

View file

@ -7,7 +7,7 @@
*/
import * as cache from '../src/cache';
import {stopPropagation} from '../src/dispatcher';
import {stopPropagation} from '../src/legacy_dispatcher';
import {
EarlyEventContract,
EarlyJsactionData,

View file

@ -7,7 +7,7 @@
*/
import {
BaseDispatcher,
Dispatcher,
EarlyJsactionDataContainer,
EventContract,
EventContractContainer,
@ -96,7 +96,7 @@ export function withEventReplay(): Provider[] {
eventContract.addEvent(et);
}
eventContract.replayEarlyEvents(container);
const dispatcher = new BaseDispatcher(() => {}, {
const dispatcher = new Dispatcher(() => {}, {
eventReplayer: (queue) => {
for (const event of queue) {
handleEvent(event);

View file

@ -1115,9 +1115,6 @@
{
"name": "init_authoring"
},
{
"name": "init_base_dispatcher"
},
{
"name": "init_bindings"
},
@ -1325,6 +1322,9 @@
{
"name": "init_discovery_utils"
},
{
"name": "init_dispatcher"
},
{
"name": "init_document"
},