Type-Safe Custom Event Handling Pattern
Rune provides a type-safe custom event handling pattern.
Custom events with no detail
value
You can set up an event for use as follows by creating and exporting it:
export class DialogOpened extends CustomEventWithoutDetail {}
When propagating the event, you must call this.dispatchEvent
and pass two arguments: the class that extends CustomEvent
and CustomEventInit
.
export class DialogView extends View {
open() {
// ...
this.dispatchEvent(DialogOpened, { bubbles: true });
// ok
this.dispatchEvent(DialogOpened, { bubbles: true, detail: 'my-data' });
// TS2322: Type string is not assignable to type undefined
}
}
Custom events with an optional detail
value
There aren’t many typical use cases where a custom event with an optional detail
is perfectly suitable, so use this pattern carefully. Here is an example:
export class DataLoaded extends CustomEventOptionalDetail<{ body: string; loadedAt: Date }> {}
export class DataLoaderView extends View {
download() {
// ...
if (isFail) {
this.dispatchEvent(DataLoaded, { bubbles: true });
// ok
} else {
this.dispatchEvent(DataLoaded, { detail: { body: '...', loadedAt: new Date() } });
// ok
this.dispatchEvent(DataLoaded, { detail: { body: '...' } });
// TS2741: Property loadedAt is missing in type { body: string; } but required in type { body: string; loadedAt: Date; }
}
}
}
Custom events with a required detail
value
To create custom events that require a detail
value, import and extend CustomEventWithDetail
.
import { CustomEventWithDetail } from 'rune-ts';
export class SegmentSelected extends CustomEventWithDetail<Segment> {}
By making the detail
property mandatory, the dispatch call is also constrained to include it:
export class SegmentControlView extends View<Segment[]> {
// ...
@on('click', 'button:not(.selected)')
private select(e: MouseEvent) {
//...
this.dispatchEvent(SegmentSelected, {
detail: this.selectedSegment(),
bubbles: true
});
// ok
this.dispatchEvent(SegmentSelected, { bubbles: true });
// TS2345: Property detail is missing in type { bubbles: true; } but required in type CustomEventWithDetailInit<Segment>
}
selectedSegment() {
return this.data[this.selectedIndex];
}
}
Listening to custom events
You can register the event by passing the event class instead of a string as shown below. In doing so, (e: SegmentSelected) => void
is inferred, and e.detail
is inferred as the type passed when dispatching the event.
this.addEventListener(SegmentSelected, (e: SegmentSelected) => {
const segment: Segment = e.detail;
});
This approach is also powerful when used with delegate
. In the first argument (the event type position), pass the event class; in the second argument (the CSS selector position), specify the SubView class from which the event will be dispatched. This filters events that occur in the child view and provides inference such as (e: RemoveRequested, todoItemView: TodoItemView)
, while also passing the TodoItemView
object that triggered the event:
this.delegate(
RemoveRequested,
TodoItemView,
(e: RemoveRequested, todoItemView: TodoItemView) => {
this.remove(todoItemView.data);
},
);