Skip to content

Commit

Permalink
docs
Browse files Browse the repository at this point in the history
  • Loading branch information
PS committed Nov 14, 2024
1 parent de46e10 commit 0616a49
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 57 deletions.
119 changes: 115 additions & 4 deletions docs/basic-features/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,121 @@ 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.

### Initializing Observable

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

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

Observable.init();
}
```

**Accessing Observable in Controllers** is easy with [Dependency Injection](./object-container.md#1-dependency-injection).
**To access Observable 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 { Observable } from '@ima/core';

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

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

### 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.init();
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
7 changes: 7 additions & 0 deletions packages/core/src/config/bind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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 { HttpAgent } from '../http/HttpAgent';
import { HttpAgentImpl } from '../http/HttpAgentImpl';
import { HttpProxy } from '../http/HttpProxy';
Expand Down Expand Up @@ -95,6 +96,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 +177,10 @@ export const initBind: InitBindFunction = (ns, oc, config) => {
oc.provide(EventBus, EventBusImpl);
oc.bind('$EventBus', EventBus);

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

// Cache
oc.constant('$CacheStorage', oc.get(MapStorage));
oc.bind('$CacheFactory', CacheFactory);
Expand Down Expand Up @@ -206,6 +212,7 @@ export const initBind: InitBindFunction = (ns, oc, config) => {
$Dictionary: Dictionary,
$Dispatcher: Dispatcher,
$EventBus: EventBus,
$Observable: Observable,
$Helper: '$Helper',
$Http: HttpAgent,
$PageStateManager: PageStateManager,
Expand Down
39 changes: 15 additions & 24 deletions packages/core/src/event/Dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,16 @@ export abstract class Dispatcher {
}

/**
* Registers the provided event listener to be executed when the specified
* event is fired on this dispatcher.
* Registers the provided event listener to be executed when any event is fired
* on this dispatcher.
*
* When the specified event is fired, the event listener will be executed
* with the data passed with the event as the first argument.
* 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 for the
* same event and with the same scope multiple times has no effect.
* and should not be relied upon. Registering the same listener with the same
* scope multiple times has no effect.
*
* @param event The name of the event to listen for.
* @param listener The event listener to register.
* @param scope The object to which the `this` keyword
* will be bound in the event listener.
Expand Down Expand Up @@ -125,14 +124,11 @@ export abstract class Dispatcher {

/**
* Deregisters the provided event listener, so it will no longer be
* executed with the specified scope when the specified event is fired.
* executed when any event is fired.
*
* @param event The name of the event for which the listener
* should be deregistered.
* @param listener The event listener to deregister.
* @param scope The object to which the `this` keyword
* would be bound in the event listener.
* @return This dispatcher.
* @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]>,
Expand All @@ -150,27 +146,22 @@ export abstract class Dispatcher {
* 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,
eventType?: string
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;
}
}
2 changes: 1 addition & 1 deletion packages/core/src/event/DispatcherImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class DispatcherImpl extends Dispatcher {
this._eventListenersAll.set(listener, scopes);
}

this._eventListenersAll.get(listener)?.add(scope);
this._eventListenersAll.get(listener)!.add(scope);

return this;
}
Expand Down
66 changes: 63 additions & 3 deletions packages/core/src/event/Observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import { Settings } from '../boot';
import { Dependencies } from '../oc/ObjectContainer';
import { RouterEvents } from '../router/RouterEvents';

/**
* An Observable is a class that manages event listeners and allows distributing
* events to the registered listeners. It maintains a history of events and supports
* persistent events that are not cleared during route changes.
*
* @remarks
* - The Observable class relies on a Dispatcher to handle the actual event distribution.
* - It maintains a history of events, which can be limited by a maximum history length.
*/
export class Observable {
protected _dispatcher: Dispatcher;
protected _observers: Map<string, Map<DispatcherListener<any>, Set<unknown>>>;
Expand All @@ -19,34 +28,70 @@ export class Observable {
'?$Settings.$Observable',
];

/**
* Creates an instance of Observable.
*
* @param dispatcher - The dispatcher responsible for managing event listeners.
* @param settings - Optional settings for the Observable instance.
*/
constructor(dispatcher: Dispatcher, settings?: Settings['$Observable']) {
this._dispatcher = dispatcher;
this._observers = new Map();
this._activityHistory = new Map();
this._persistentEvents = new Set();
this._settings = settings;

this.clear();
}

/**
* Initializes the observable.
*/
init() {
this._dispatcher.listenAll(this._handleDispatcherEvent, this);
}

/**
* Destroys the observable by clearing its internal state and removing all event listeners.
*/
destroy() {
this.clear();
this._dispatcher.unlistenAll(this._handleDispatcherEvent, this);
}

/**
* Clears all persistent events, observers, and activity history from the observable.
*
* This method will remove all stored events, registered observers, and any recorded
* activity history, effectively resetting the observable to its initial state.
*/
clear() {
this._persistentEvents.clear();
this._observers.clear();
this._activityHistory.clear();
this._persistentEvents.clear();
}

/**
* Registers an event as persistent, meaning its history won't be cleared upon calling the `clear` method (route change).
*
* @param event - The name of the event to be registered as persistent. This can be a key from the DispatcherEventsMap or any string.
*/
registerPersistenEvent(event: keyof DispatcherEventsMap | string) {
this._persistentEvents.add(event);
}

/**
* Subscribes an observer to a specific event. When the event is dispatched,
* the observer will be notified and executed within the provided scope.
* If the event has already occurred, the observer will be immediately
* invoked with the historical data.
*
* @param event - The event to subscribe to. This can be a key from the
* DispatcherEventsMap or a custom string event.
* @param observer - The observer function to be called when the event is
* dispatched.
* @param scope - The scope in which the observer function should be executed.
* This is optional.
* @returns The instance of the Observable for chaining.
*/
subscribe(
event: keyof DispatcherEventsMap | string,
observer: DispatcherListener<any>,
Expand All @@ -71,6 +116,14 @@ export class Observable {
return this;
}

/**
* Unsubscribes an observer from a specific event.
*
* @param event - The event name or key from the DispatcherEventsMap.
* @param observer - The observer (listener) to be unsubscribed.
* @param scope - Optional scope to be used for the observer.
* @returns The current instance for chaining.
*/
unsubscribe(
event: keyof DispatcherEventsMap | string,
observer: DispatcherListener<any>,
Expand All @@ -93,6 +146,13 @@ export class Observable {
return this;
}

/**
* Handles dispatcher events by updating the activity history and notifying observers.
* It also resets the activity history for non-persistent events on `BEFORE_HANDLE_ROUTE` ecvent.
*
* @param event - The name of the event being dispatched.
* @param data - The data associated with the event.
*/
_handleDispatcherEvent(event: string, data: any) {
if (event === RouterEvents.BEFORE_HANDLE_ROUTE) {
for (const [eventKey] of this._activityHistory) {
Expand Down
16 changes: 6 additions & 10 deletions packages/core/src/page/manager/AbstractPageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,9 @@ export abstract class AbstractPageManager extends PageManager {
try {
await autoYield();
if (!isControllerViewResolved) {
this._dispatcher.fire(
RouterEvents.BEFORE_LOADING_ASYNC_ROUTE,
{ route },
true
);
this._dispatcher.fire(RouterEvents.BEFORE_LOADING_ASYNC_ROUTE, {
route,
});
}

await autoYield();
Expand All @@ -159,11 +157,9 @@ export abstract class AbstractPageManager extends PageManager {
return { status: 409 };
} finally {
if (!isControllerViewResolved) {
this._dispatcher.fire(
RouterEvents.AFTER_LOADING_ASYNC_ROUTE,
{ route },
true
);
this._dispatcher.fire(RouterEvents.AFTER_LOADING_ASYNC_ROUTE, {
route,
});
}
}

Expand Down
Loading

0 comments on commit 0616a49

Please sign in to comment.