Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
11 changes: 11 additions & 0 deletions .changeset/docs_event-module-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@equinor/fusion-framework-docs": patch
---

Align the event module's vue-press documentation with its `docs/` folder: `README.md` and the
new `docs/{configuration,observable-patterns,lifecycle,testing}.md` pages now `@include` the
package's own docs instead of duplicating content, matching the `http`/`module` pattern.

The event module's React bindings page moves from `event/react.md` to `react/event/README.md`,
alongside `react/router/`, and is now an `@include` of `@equinor/fusion-framework-react-module-event`'s
README. The sidebar is updated to match.
29 changes: 29 additions & 0 deletions .changeset/module-event_configurator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@equinor/fusion-framework-module-event": minor
---

Add `EventModuleConfigurator`, a `BaseConfigBuilder`-based configurator with fluent
`setOnDispatch`/`setOnBubble` setters, replacing direct property assignment on the config
object.

```ts
// Before (still works, but deprecated)
config.event.onDispatch = (event) => { ... };
delete config.event.onBubble;

// After
configurator.setOnDispatch((event) => { ... });
configurator.setOnBubble(undefined);
```

`IEventModuleConfigurator` is renamed to `EventModuleConfig` and converted from an `interface`
to a `type`. `IEventModuleConfigurator` is kept as a deprecated type alias for backward
compatibility.

Also narrows the `dispatchEvent` return type for registered {@link FrameworkEventMap} keys and
pre-constructed event instances, so callers get back the specific event type instead of the
generic `FrameworkEvent`.

**Deprecated (since 6.1.0), no migration required yet:**
- `EventModuleConfigurator#onDispatch`/`#onBubble` property assignment — use `setOnDispatch`/`setOnBubble`.
- `IEventModuleConfigurator` type — use `EventModuleConfig`.
47 changes: 47 additions & 0 deletions .changeset/module-event_utils-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
"@equinor/fusion-framework-module-event": minor
---

Add `waitForEvent` and `watchEvents` helper utilities and `./operators` subpath.

**New subpath exports:**

```ts
import { filterEvent } from '@equinor/fusion-framework-module-event/operators';
import { waitForEvent, watchEvents } from '@equinor/fusion-framework-module-event/utils';
```

### `waitForEvent(provider, matcher, options?)`

Resolves with the next event matching `matcher`. Accepts a single event type string (uses the type-scoped `filterEvent` path and preserves type narrowing), an array of type strings, or a predicate function. Supports an optional `timeout` (ms) and `AbortSignal` so a test cannot hang indefinitely.

```ts
// Single type — typed result
const event = await waitForEvent(provider, 'onModulesLoaded');

// Array of types
const event = await waitForEvent(provider, ['myFeature.saved', 'myFeature.updated']);

// Predicate matching on payload
const event = await waitForEvent(provider, (e) => e.detail?.id === 1);

// With timeout
const event = await waitForEvent(provider, 'myFeature.saved', { timeout: 1000 });
```

### `watchEvents(provider, matcher)`

Collects all events matching `matcher` into an array. Only matching events are ever stored — a high volume of non-matching dispatches does not cause unbounded memory growth. Returns a handle with `events`, `lastEvent(type?)`, and `dispose()`.

```ts
const handle = watchEvents(provider, ['myFeature.saved', 'myFeature.deleted']);
// ... run code under test ...
expect(handle.lastEvent('myFeature.saved')?.detail).toEqual({ id: 1 });
handle.dispose();
```

### `./operators` subpath

`filterEvent` is now also exported from `@equinor/fusion-framework-module-event/operators`. The existing root export is preserved — no migration required for current consumers.

Resolves [equinor/fusion-core-tasks#1656](https://github.com/equinor/fusion-core-tasks/issues/1656).
153 changes: 17 additions & 136 deletions packages/modules/event/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ Async event dispatching module for the Fusion Framework. Enables type-safe commu
- **Application developers** that want to intercept, log, or cancel events flowing through the framework.
- **Library consumers** that subscribe to event streams for analytics, debugging, or cross-cutting concerns.

## Documentation

| Topic | Description |
|---|---|
| [Configuration](docs/configuration.md) | `onDispatch`/`onBubble` hooks and registering custom event types via `FrameworkEventMap` |
| [Observable Patterns](docs/observable-patterns.md) | `event$`, `filterEvent`, and the `./operators` subpath |
| [Lifecycle](docs/lifecycle.md) | Dispatch sequence, cancelable events, and bubbling |
| [Testing](docs/testing.md) | `waitForEvent`, `watchEvents`, and using a bespoke `ModulesConfigurator` in tests |

## Quick start

### Install
Expand Down Expand Up @@ -54,143 +63,15 @@ if (!event.canceled) {
| `FrameworkEventHandler` | Type | Listener callback signature (sync or async) |
| `IEventModuleProvider` | Interface | Public API for the event provider (`addEventListener`, `dispatchEvent`, `event$`) |
| `EventModuleProvider` | Class | Default provider implementation |
| `IEventModuleConfigurator` | Interface | Configuration hooks (`onDispatch`, `onBubble`) |
| `EventModuleConfig` | Type | Resolved configuration hooks (`onDispatch`, `onBubble`) |
| `EventModuleConfigurator` | Class | Fluent config builder (`setOnDispatch`, `setOnBubble`) — see [Configuration](docs/configuration.md) |
| `IEventModuleConfigurator` | Type | _Deprecated_ alias for `EventModuleConfig` |
| `filterEvent` | Function | RxJS operator to narrow `event$` to a single registered event type |
| `EventModule` / `eventModuleKey` | Type / Const | Module definition and key (`'event'`) |

## Configuration

Configure the event module during framework setup to hook into dispatch lifecycle:

```ts
import type { FrameworkEvent } from '@equinor/fusion-framework-module-event';

const configurator = (config) => {
// Inspect or cancel events before listeners run
config.event.onDispatch = (event: FrameworkEvent) => {
if (!isAllowed(event)) {
event.preventDefault();
}
};

// Disable bubbling to parent providers
delete config.event.onBubble;
};
```

### `onDispatch`

Called **before** registered listeners. Use it to log, validate, or cancel events globally.

### `onBubble`

Called **after** all listeners if the event still bubbles. By default, the framework wires this to forward events to the parent provider. Delete it to isolate events to the current scope.

## Registering custom event types

Extend `FrameworkEventMap` via TypeScript declaration merging to get type-safe `addEventListener` and `dispatchEvent` calls:

```ts
import type {
FrameworkEvent,
FrameworkEventInit,
} from '@equinor/fusion-framework-module-event';

interface MyPayload {
id: string;
value: number;
}

declare module '@equinor/fusion-framework-module-event' {
interface FrameworkEventMap {
'myFeature': FrameworkEvent<FrameworkEventInit<MyPayload>>;
}
}
```

After registration, both the event name and payload are type-checked:

```ts
modules.event.addEventListener('myFeature', (event) => {
// event.detail is typed as MyPayload
console.log(event.detail.id);
});
```

## Observable event stream

The `event$` observable emits every dispatched event. Subscribers receive events **after** dispatch and **cannot** call `preventDefault` or `stopPropagation` — use `addEventListener` for side-effect-capable handling.

```ts
import { filterEvent } from '@equinor/fusion-framework-module-event';

// Subscribe to all events
const sub = modules.event.event$.subscribe((event) => {
console.log(event.type, event.detail);
});

// Or filter to a specific registered event type
const filtered = modules.event.event$.pipe(
filterEvent('onModulesLoaded'),
).subscribe((event) => {
// event is narrowed to the registered type
console.log(event.detail);
});

// Unsubscribe on teardown
sub.unsubscribe();
filtered.unsubscribe();
```

## Event lifecycle

1. `dispatchEvent` is called with a name + init or a `FrameworkEvent` instance.
2. The `onDispatch` hook runs (if configured). Canceling here stops all listeners.
3. Registered listeners execute sequentially. For cancelable events each listener is `await`ed; non-cancelable listeners fire without awaiting.
4. If the event still bubbles, the `onBubble` hook runs (typically forwarding to a parent provider).
5. The event is pushed to `event$` for observable subscribers.

## Cancelable events

Mark an event as `cancelable` in its init and `await` dispatch:

```ts
const event = await modules.event.dispatchEvent('myEvent', {
detail: data,
cancelable: true,
});

if (event.canceled) {
// A listener called event.preventDefault()
return;
}
```
### Subpaths

A listener cancels the event by calling `preventDefault()`:

```ts
modules.event.addEventListener('myEvent', (event) => {
if (shouldBlock(event.detail)) {
event.preventDefault();
}
});
```

## Bubbling

Events bubble to parent providers by default (`canBubble: true`). A listener can stop propagation:

```ts
modules.event.addEventListener('myEvent', (event) => {
event.stopPropagation(); // prevents bubbling to parent
});
```

Or disable bubbling for a specific event at dispatch time:

```ts
await modules.event.dispatchEvent('myEvent', {
detail: data,
canBubble: false,
});
```
| Subpath | Exports | Purpose |
|---|---|---|
| `./operators` | `filterEvent` | RxJS pipeable operators over `event$` (also re-exported from the package root) |
| `./utils` | `waitForEvent`, `watchEvents` | Plain helpers for waiting on / collecting dispatched events, in application code or tests |
97 changes: 97 additions & 0 deletions packages/modules/event/docs/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Configuration

## Dispatch hooks

The event module is configured through `EventModuleConfigurator`, a `BaseConfigBuilder` with
fluent `setOnDispatch`/`setOnBubble` setters:

```ts
import { EventModuleConfigurator } from '@equinor/fusion-framework-module-event';

const doNotHandleEvents = ['onMyEvent'];
const doNotPropagateEvents = ['myOtherEvent'];

const configurator = new EventModuleConfigurator();

// Inspect or cancel events before listeners run
configurator.setOnDispatch((event) => {
if (doNotHandleEvents.includes(event.type)) {
event.preventDefault();
}
if (doNotPropagateEvents.includes(event.type)) {
event.stopPropagation();
}
});

// Disable bubbling to parent providers
configurator.setOnBubble(undefined);
```

> **Deprecated:** assigning `configurator.onDispatch`/`configurator.onBubble` directly still
> works but is deprecated since `6.1.0` — use `setOnDispatch`/`setOnBubble` instead.

### `onDispatch`

Called **before** registered listeners. Use it to log, validate, or cancel events globally.

### `onBubble`

Called **after** all listeners if the event still bubbles. By default, the framework wires this to forward events to the parent provider. Pass `undefined` to isolate events to the current scope.

## Registering custom event types

Extend `FrameworkEventMap` via TypeScript declaration merging to get type-safe `addEventListener` and `dispatchEvent` calls. Declaring the map entry adds type hinting only — it does not add any runtime behavior:

```ts
import type {
FrameworkEvent,
FrameworkEventInit,
} from '@equinor/fusion-framework-module-event';

interface MyPayload {
id: string;
value: number;
}

declare module '@equinor/fusion-framework-module-event' {
interface FrameworkEventMap {
'myFeature': FrameworkEvent<FrameworkEventInit<MyPayload>>;
}
}
```

After registration, both the event name and payload are type-checked:

```ts
modules.event.addEventListener('myFeature', (event) => {
// event.detail is typed as MyPayload
console.log(event.detail.id);
});
```

## Custom event classes

For behavior beyond a typed `detail`, subclass `FrameworkEvent` directly:

```ts
class MyEvent extends FrameworkEvent<MyPayload, MySource> {
Comment thread
odinr marked this conversation as resolved.
Outdated
constructor(readonly obj: MyObj, init: FrameworkEventInit<MyPayload, MySource>) {
super('onMyEvent', init);
}
}

// add type hinting
declare module '@equinor/fusion-framework-module-event' {
interface FrameworkEventMap {
onMyEvent: MyEvent;
}
}

modules.event.dispatchEvent(new MyEvent(someObj, { detail, source }));

modules.event.addEventListener('onMyEvent', (event) => {
console.log('is my custom event:', event instanceof MyEvent);
console.log('my custom obj', event.obj);
});
```

Loading
Loading