Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Observable events #585

Merged
merged 13 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading