Events and Handlers

Events are listened to and emitted from the manager. Events can be setup and listened to from both components and handlers.

Component Events

First, let's see how we bind an event in a component:

_setupEventListeners(): void {

    this.manager.on("my-first-event", () => alert("My First Event Fired!"));
    this.manager.on("my-second-event", () => alert("My Second Event Fired!"));
}

Mostly, components will listen to events when the outcome of an event needs to do some UI work. Therefore, not all components (quite a lot in fact) will listen to events. This is especially true if you have a well defined component / props tree. You may have one component at the top which listens and then relies on the render cycle to pass updates down the tree.

Components, however, will probably emit quite a lot of events. For example, if in your app you have a button which should trigger the device camera to open and take a picture - this would be handled by emitting an event. This could happen like so:

onClick = (): void => {

    this.manager.on(MyAppEvents.TakePhoto);
}

Usually, events are also typed into Typescript enums, like the above: MyAppEvents.TakePhoto. This is usually found within your events/index.ts and would look like:

export const enum MyAppEvents {

    TakePhoto = "take-photo"
    // ... more events here
}

Handlers

A handler is an event handler that isn't tied to any kind of UI element. Components exist on the UI element and therefore may not always be visible or in the DOM (and in that case will not be listening to events). Handlers by contrast are always available ambiently to handle and respond to events.

The primary purpose for sending events to handlers is to process buiness logic. Components should be relatively dumb and deal with UI only. This allows them to be more reusable. Your application specific logic should instead live within handlers as these are less likely to be shared across applications.

In the case of the MyAppEvents.TakePhoto above, we can deal with all the checks that are required for taking a photo inside a handler. For example, checking if the hardware is available, if the app can open the camera, if the required permissions are granted, etc. None of this is relevant to the component and the component should NEVER contain this logic. Instead the handler will deal with all of this, keeping the component nice and light and all hardware accessible logic in one place.

Using a handler

Handlers are placed inside the handlers folder. To create a handler to deal with the TakePhoto event, we'll need to create a handler looking like:

export class TakePhotoHandler extends BaseHandler<TestingAppManager> {

    eventType: MyAppEvents[] = [];

    protected async handleMessage(message: WSEventArgs): Promise<void> {}

    protected async handleEvent(eventType: TestingAppEvents, eventArgs: EventArgs): Promise<void> {}
}

Handlers can listen to one or more events and this is denoted in the eventType: MyAppEvents[] = []; line. To listen to events, change this to something like:

eventType: MyAppEvents[] = [

    MyAppEvents.TakePhoto
];

Finally, handlers must be registered before they are bound. This is done in the root index.ts file, like so;

const myAppHandlers: HandlerClasses = {

    takePhotoHandler: TakePhotoHandler
};

Now, whenever MyAppEvents.TakePhoto is emitted, our handler will be called. But what will it do?

Right now, it won't do anything. We need to create some logic. This is done by expanding the handleEvent function:

protected async handleEvent(eventType: MyAppEvents, eventArgs: EventArgs): Promise<void> {

    if (eventType === MyAppEvents.TakePhoto) {

        alert("Take photo emitted!");
    }
}

So when the MyAppEvents.TakePhoto we now show the alert. This is pretty useless but shows how handlers can be bound to events.

Note

It's a good idea to wrap your handleEvent functions with ifs. This is because most handlers will deal with multiple events that are grouped together logically. If your handleEvent function becomes too long, consider abstracting the event processing code out into their own functions.

handleEvent is also async meaning that await can be used throughout. This is useful in case your need save state or make http requests, both of which are perfectly valid to be done from within the handler.

Rememeber, the handler has easy access to the manager, so extending your handleEvent to save state like below is perfectly fine.

protected async handleEvent(eventType: MyAppEvents, eventArgs: EventArgs): Promise<void> {

    if (eventType === MyAppEvents.TakePhoto) {

        // Save some state first
        await this.manager.store.saveSomeState("Going to save this to state.");

        alert("Take photo emitted!");
    }
}

Passing Data with Events

You can pass data with events which is usually done via an EventArgs class, like so:

this.manager.emit(MyAppEvents.TakePhoto, new TakePhotoEventArgs("photo-file-name.jpg"));

Event args live within the events/event-args/ directory and they extend from EventArgs. A TakePhotoEventArgs could look like:

export class TakePhotoEventArgs extends EventArgs {

    public filename: string;

    constructor(filename: string) {

        super();

        this.filename = filename;
    }
}

This is then used in a handler like so:

this.manager.on(MyAppEvents.TakePhoto, (args: TakePhotoEventArgs) => {

    this.filename = args.filename;
});

It's possible to send event data without EventArgs, however this isn't recommended. Sending data with EventArgs ensures the args are typed and allows for custom logic. It's also usually easier to read when you need to send multiple properties via an event.

Note

You can pass data with events but it is strongly advised that you do not pass stateful data. For example, let's say you have a list of jobs and that your handler would listen to a websocket message which adds new jobs to the list. Your components should NOT be listening for this same event. Instead, your handler should update the job list and save this to state. Then it would emit an event telling the component to fetch the updated list from state rather than pass the update via the event. This ensures that there is a single source of truth (your state) and no consistency errors can occur.

Why Events / Handlers?

Components are great for UI and UI logic but they are not good places for lots of business logic, which many applications will require. Also, unlike other frameworks, our components are decentralised and in the future may not always connected to each other, which is why the manager exists.

Both of these reasons have led to the development of the event / handler system which has ended up working quite nicely. You're able to de-couple components from complex logic which means that they are often lean and easily re-used.

Handlers are where big blocks of logic can reside which means that we don't need to use anything like classes for our objects. For example, a job class is never needed. Instead we work from job object, which is dumb and therefore can easily be serialised into our persistent storage (unlike a class) and out again without the need for a mapper.

Operations to the job can be done via events and handlers, which could call out to specific funtions if that logic needs to be shared. For example, a addItemToJob function could exist which looks like: addItemToJob(job: Job, item: Item) and work in a functional manner that can be called from a handler, manager function or (very much less likely) a component.

results matching ""

    No results matching ""