Reactive APIs
    Preparing search index...

    Module @conterra/reactivity-events - v0.7.0

    @conterra/reactivity-events NPM Version

    Event support for reactive objects. Based on @conterra/reactivity-core.

    Click here to visit the rendered API Documentation.

    import { emit, emitter, on } from "@conterra/reactivity-events";

    interface ClickEvent {
    x: number;
    y: number;
    }

    class View {
    readonly clicked = emitter<ClickEvent>();
    }

    const view = new View();

    // Subscribe to the click event
    on(view.clicked, (event) => {
    console.log(`Click at ${event.x}, ${event.y}`);
    });

    // Emit the click event, calling all subscribers
    emit(view.clicked, { x: 10, y: 20 });

    Use emitter() to create an event emitter. Event emitters can be used to emit events (using emit()) and to subscribe to them (using on / onSync).

    Example:

    import { emitter } from "@conterra/reactivity-events";

    // Emits numbers
    const e1 = emitter<number>();

    // Emits objects with x and y properties
    const e2 = emitter<{ x: number; y: number }>();

    // Emits nothing (void)
    const e3 = emitter();

    Use emit(emitter, event) to emit an event. For example:

    import { emit, emitter } from "@conterra/reactivity-events";

    // Emits objects with x and y properties
    const clicked = emitter<{ x: number; y: number }>();

    // Broadcasts the event to all subscribers of `clicked`
    emit(clicked, { x: 10, y: 20 });

    Use on(emitter, callback) (or onSync()) to subscribe to events:

    import { emitter, on } from "@conterra/reactivity-events";

    const clicked = emitter<{ x: number; y: number }>();

    // Callback will be invoked whenever the event is emitted
    const handle = on(clicked, (event) => {
    console.log("Clicked at", event.x, event.y);
    });

    // Use the handle to unsubscribe from the event when no longer needed
    handle.destroy();

    Similar to watch / watchSync in @conterra/reactivity-core, there are two versions of the on function: on and onSync:

    • on: Callbacks are called asynchronously (in a future task, similar to setTimeout(cb, 0)). This is usually what you want.
    • onSync: Callbacks are called synchronously, immediately after they have been emitted. Note that there is a subtle interaction with batch (see Integration with batch).

    on and onSync support multiple kinds of event source parameters:

    • A plain event source (like view.clicked in the example above; not reactive).
    • A signal holding an event source (reactive).
    • A function returning an event source, implemented using signals (reactive).

    In the following examples, the signal currentLogger points to the active logger object (either logger1 or logger2). The subscription configured by on automatically switches between the two loggers whenever the signal changes. In other words, it always stays subscribed to the current one.

    import { nextTick, reactive } from "@conterra/reactivity-core";
    import { emit, emitter, on } from "@conterra/reactivity-events";

    class Logger {
    onMessage = emitter<string>();
    }

    const logger1 = new Logger();
    const logger2 = new Logger();
    const currentLogger = reactive(logger1);

    // Stays subscribed to the current logger, even if it changes.
    on(
    () => currentLogger.value.onMessage,
    (message) => console.log("on message:", message)
    );
    emit(logger1.onMessage, "Message to logger 1");

    // Wait for event listener to be called (just for illustration).
    await nextTick();

    // Signal changes, `on` automatically unsubscribes from logger1 and subscribes to logger2.
    currentLogger.value = logger2;
    emit(logger2.onMessage, "Message to logger 2");

    Configuring once: true within a subscription will automatically unsubscribe after the first event has been emitted:

    import { emit, emitter, on } from "@conterra/reactivity-events";

    const clicked = emitter();

    on(clicked, () => console.log("clicked"), { once: true });
    emit(clicked);
    emit(clicked);
    // Output: clicked (only once)

    Note that is still a good idea to call destroy on the handle returned by on if you want to unsubscribe manually before the first event has been emitted.

    When providing an event, you can use the subscribed and unsubscribed options to detect when your event is actually being used:

    • subscribed() will be called when the first subscriber subscribes to the event. This can be used to initialize the event source lazily.
    • unsubscribed() will be called when the last subscriber unsubscribes from the event. This can be used to clean up resources initialized in subscribed().

    Example:

    const mouseMoved = emitter({
    subscribed: () => {
    // Start listening for mouse events
    }),
    unsubscribed: () => {
    // Stop listening for mouse events
    },
    });

    This package provides two important TypeScript types:

    • EventEmitter<T>. The return type of emitter<T>. This type supports both emitting and subscribing to events.
    • EventSource<T>. This type allows only subscribing to events. This is useful for public interfaces where users are not supposed to emit events themselves.

    Note that the restrictions imposed by EventSource<T> are a compile time feature only: at runtime, both interfaces are represented by the same object.

    The following example demonstrates the use of EventSource to separate the public interface from the implementation:

    import { emit, emitter, on, EventSource } from "@conterra/reactivity-events";

    interface ClickEvent {
    x: number;
    y: number;
    }

    // Public interface
    interface ViewApi {
    readonly clicked: EventSource<ClickEvent>;
    }

    // Private implementation
    class ViewImpl implements ViewApi {
    clicked = emitter<ClickEvent>();
    }

    const viewImpl = new ViewImpl();
    const viewApi: ViewApi = viewImpl;
    on(viewApi.clicked, (event) => console.log("Clicked at", event.x, event.y));

    emit(viewImpl.clicked, { x: 1, y: 2 });

    // would be a TypeScript error:
    // emit(viewApi.clicked, { x: 3, y: 4 });

    The batch() function from @conterra/reactivity-core can be used to group reactive changes to one or more signals, comparable to a transaction. Effects or watches are not executed until the batch completes. This prevents intermediate states (which may be inconsistent) from being observed by other parts of the application.

    Event handling works the same way: even when onSync is used, event handlers are not running immediately, but only after the batch completes. This prevents event handlers from observing intermediate states as well.

    For example:

    import { batch, reactive } from "@conterra/reactivity-core";
    import { emit, emitter, onSync } from "@conterra/reactivity-events";

    class Foo {
    changed = emitter();

    a = reactive(0);
    b = reactive(1);

    changeValues() {
    batch(() => {
    console.debug("batch start");

    // Values are changed together.
    this.a.value += 1;
    emit(this.changed); // Callbacks are _not_ running here
    this.b.value += 1;

    console.debug("batch end");
    }); // Callbacks are running here
    }
    }

    const foo = new Foo();
    onSync(foo.changed, () => {
    console.log("on change");
    });
    foo.changeValues();
    // Output:
    // batch start
    // batch end
    // on change

    Try removing the batch call to see the difference.

    Events

    EmitterOptions
    EventEmitter
    EventSource
    emit
    emitter

    Subscribing

    SubscribeOptions
    on
    onSync

    Helpers

    EventArgs
    EventCallback