Skip to content

Commit

Permalink
Merge pull request #585 from seznam/dispatcherx
Browse files Browse the repository at this point in the history
  • Loading branch information
jsimck authored Nov 20, 2024
2 parents 158f55e + c54e3e8 commit 45c7b86
Show file tree
Hide file tree
Showing 22 changed files with 798 additions and 121 deletions.
106 changes: 88 additions & 18 deletions docs/basic-features/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,20 +138,6 @@ way to propagate event in other directions, or to other parts of the UI, or
from the controller to the UI is using the `app/event/Dispatcher` API.

**Accessing Dispatcher in Controllers** is easy with [Dependency Injection](./object-container.md#1-dependency-injection).
**To access Dispatcher from Views and Components** you should register it in [ComponentUtils](./views-and-components.md#utilities-shared-across-views-and-components).

```javascript
// app/config/bind.js
import { Dispatcher } from '@ima/core';

export let init = (ns, oc, config) => {
const ComponentUtils = oc.get('$ComponentUtils');

ComponentUtils.register({
$Dispatcher: Dispatcher
});
}
```


### Firing and listening to Dispatcher events
Expand Down Expand Up @@ -195,10 +181,94 @@ will still receive the `showLightbox` event when it's fired.
> **Note:** A great place to
mount components like Lightbox is [ManagedRootView](./rendering-process.md#managedrootview).

Note that events distributed using the Dispatcher are useful only in very
specific use-cases, so the Dispatcher logs a warning to the console if there
are no listeners registered for the fired event in order to notify you of
possible typos in event names.
### Listening to all Dispatcher events

You can listen to all events dispatched by the `Dispatcher` by using the `listenAll()`
and `unlistenAll()` methods.

```javascript
// app/component/eventLogger/EventLogger.jsx

componentDidMount() {
this.utils.$Dispatcher.listenAll(this._onDispatcherEvent, this);
}

componentWillUnmount() {
this.utils.$Dispatcher.unlistenAll(this._onDispatcherEvent, this);
}

_onDispatcherEvent(eventName, data) {
// ...
}
```

## Observable

The `Observable` class allows you to subscribe to events dispatched by the
`Dispatcher`. Upon subscribing, subscribers will be notified of past and future
events.

**Accessing Observable in Controllers** is easy with [Dependency Injection](./object-container.md#1-dependency-injection).

### Subscribing and unsubscribing to events

You can subscribe to events dispatched by the `Dispatcher` using the `subscribe()`, and unsubscribe using the `unsubscribe()` method.

```javascript
// app/component/media/Media.jsx

componentDidMount() {
this.utils.$Observable.subscribe('showLightbox', this._onShowLightbox, this);
}

componentWillUnmount() {
this.utils.$Observable.unsubscribe('showLightbox', this._onShowLightbox, this);
}

_onShowLightbox(data) {
// ...
}
```

> **Note:** If the `showLightbox` event was already dispatched before the `Media` component was mounted,
the `_onShowLightbox` method will be called immediately upon subscribing with the data that was passed to the event.
> **Note:** If the event was dispatched multiple times before the `Media` component was mounted,
the `_onShowLightbox` method will be called for each event.

### Persistent events

The `Observable` class clears its history of dispatched events when the `RouterEvents.BEFORE_HANDLE_ROUTE` event is dispatched.
If you want to keep the history of dispatched events, you can use the `registerPersistentEvent()` method.

```javascript
// app/config/services.js

export const initServicesApp = (ns, oc, config) => {
const Observable = oc.get('$Observable');

Observable.registerPersistentEvent('scriptLoaded');
}
```

### Settings

By default, the `Observable` class holds the last 10 events dispatched by the `Dispatcher`.
You can change this by modifying the `$Observable.maxHistoryLength` setting.

```javascript
// app/config/settings.js

export default (ns, oc, config) => {
return {
prod: {
// ...
$Observable: {
maxHistoryLength: 20
}
}
};
}
```

## Built-in events

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ export interface Settings {
$Page: {
$Render: PageRendererSettings;
};
$Observable?: {
maxHistoryLength?: number;
};
}

/**
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/config/bind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { Dispatcher } from '../event/Dispatcher';
import { DispatcherImpl } from '../event/DispatcherImpl';
import { EventBus } from '../event/EventBus';
import { EventBusImpl } from '../event/EventBusImpl';
import { Observable } from '../event/Observable';
import { ObservableImpl } from '../event/ObservableImpl';
import { HttpAgent } from '../http/HttpAgent';
import { HttpAgentImpl } from '../http/HttpAgentImpl';
import { HttpProxy } from '../http/HttpProxy';
Expand Down Expand Up @@ -95,6 +97,7 @@ export interface OCAliasMap {
$SessionMapStorage: InstanceType<typeof SessionMapStorage>;
$Dispatcher: Dispatcher;
$EventBus: EventBus;
$Observable: Observable;
$CacheStorage: OCAliasMap['$MapStorage'];
$CacheFactory: InstanceType<typeof CacheFactory>;
$Cache: Cache;
Expand Down Expand Up @@ -175,6 +178,10 @@ export const initBind: InitBindFunction = (ns, oc, config) => {
oc.provide(EventBus, EventBusImpl);
oc.bind('$EventBus', EventBus);

// Observable
oc.provide(Observable, ObservableImpl);
oc.bind('$Observable', Observable);

// Cache
oc.constant('$CacheStorage', oc.get(MapStorage));
oc.bind('$CacheFactory', CacheFactory);
Expand Down Expand Up @@ -206,6 +213,7 @@ export const initBind: InitBindFunction = (ns, oc, config) => {
$Dictionary: Dictionary,
$Dispatcher: Dispatcher,
$EventBus: EventBus,
$Observable: Observable,
$Helper: '$Helper',
$Http: HttpAgent,
$PageStateManager: PageStateManager,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { InitServicesFunction } from '../Bootstrap';
export const initServices: InitServicesFunction = (ns, oc, config) => {
oc.get('$Dictionary').init(config.dictionary);
oc.get('$Dispatcher').clear();
oc.get('$Observable').init();

if (!oc.get('$Window').isClient()) {
oc.get('$Request').init(config.request!);
Expand Down Expand Up @@ -34,6 +35,7 @@ export const initServices: InitServicesFunction = (ns, oc, config) => {
if ($Debug && typeof window !== 'undefined') {
window.__IMA_HMR?.emitter?.once('destroy', async () => {
oc.get('$Dispatcher').clear();
oc.get('$Observable').destroy();
oc.get('$Router').unlisten();
oc.get('$PageRenderer').unmount();
await oc.get('$PageManager').destroy();
Expand Down
60 changes: 51 additions & 9 deletions packages/core/src/event/Dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export interface DispatcherEventsMap
RouterDispatcherEvents,
PageRendererDispatcherEvents {}
export type DispatcherListener<D> = (data: D) => void;
export type DispatcherListenerAll<D> = (
event: keyof DispatcherEventsMap | string,
data: D
) => void;

/**
* A Dispatcher is a utility that manager event listeners registered for events
Expand Down Expand Up @@ -64,6 +68,31 @@ export abstract class Dispatcher {
return this;
}

/**
* Registers the provided event listener to be executed when any event is fired
* on this dispatcher.
*
* When any event is fired, the event listener will be executed with the data
* passed with the event as the first argument.
*
* The order in which the event listeners will be executed is unspecified
* and should not be relied upon. Registering the same listener with the same
* scope multiple times has no effect.
*
* @param listener The event listener to register.
* @param scope The object to which the `this` keyword
* will be bound in the event listener.
* @return This dispatcher.
*/
listenAll<E extends keyof DispatcherEventsMap>(
listener: DispatcherListenerAll<DispatcherEventsMap[E]>,
scope?: unknown
): this;
listenAll(listener: DispatcherListenerAll<any>, scope?: unknown): this;
listenAll(listener: DispatcherListenerAll<any>, scope?: unknown): this {
return this;
}

/**
* Deregisters the provided event listener, so it will no longer be
* executed with the specified scope when the specified event is fired.
Expand Down Expand Up @@ -93,33 +122,46 @@ export abstract class Dispatcher {
return this;
}

/**
* Deregisters the provided event listener, so it will no longer be
* executed when any event is fired.
*
* @param listener The event listener function to deregister for all events.
* @param scope Optional. The object to which the `this` keyword would be bound in the event listener.
* @return This dispatcher instance.
*/
unlistenAll<E extends keyof DispatcherEventsMap>(
listener: DispatcherListenerAll<DispatcherEventsMap[E]>,
scope?: unknown
): this;
unlistenAll(listener: DispatcherListenerAll<any>, scope?: unknown): this;
unlistenAll(listener: DispatcherListenerAll<any>, scope?: unknown): this {
return this;
}

/**
* Fires a new event of the specified name, carrying the provided data.
*
* The method will synchronously execute all event listeners registered for
* the specified event, passing the provided data to them as the first
* argument.
*
* It will also execute all event listeners registered to listen to all events.
*
* Note that this method does not prevent the event listeners to modify the
* data in any way. The order in which the event listeners will be executed
* is unspecified and should not be relied upon.
*
* @param event The name of the event to fire.
* @param data The data to pass to the event listeners.
* @param [imaInternalEvent=false] The flag signalling whether
* this is an internal IMA event. The fired event is treated as a
* custom application event if this flag is not set.
* The flag is used only for debugging and has no effect on the
* propagation of the event.
* @return This dispatcher.
*/
fire<E extends keyof DispatcherEventsMap>(
event: E,
data: DispatcherEventsMap[E],
imaInternalEvent?: boolean
data: DispatcherEventsMap[E]
): this;
fire(event: string, data: any, imaInternalEvent?: boolean): this;
fire(event: string, data: any, imaInternalEvent?: boolean): this {
fire(event: string, data: any): this;
fire(event: string, data: any): this {
return this;
}
}
Loading

0 comments on commit 45c7b86

Please sign in to comment.