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 withif
s. This is because most handlers will deal with multiple events that are grouped together logically. If yourhandleEvent
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 ofjobs
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, yourhandler
should update thejob
list and save this tostate
. Then it would emit an event telling the component to fetch the updated list fromstate
rather than pass the update via the event. This ensures that there is a single source of truth (yourstate
) 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.