It's commonplace in Web Components to use custom events. The custom event type is an inheritor of
the Event class, but it includes a new field, detail: any
, that allows you to attach data to the
Event, which is useful for passing data up to the listener, right?
Wrong. It's a trap. You could, theoretically, inherit from CustomEvent and narrow the content of
detail
to what you specifically want, and I've seen lots of code where people do exactly that.
But there's a better way: just inherit from Event. Skip the CustomEvent class and just create your own events. I'll show you how.
I recently wrote a modal stack handler for the authentik project, and one of the things we needed was to track the "modal-show" and "modal-hide" events. I needed to track the modal's identity as a web object along with the events, first, to put it onto the stack, and second, to handle requests to remove it afterward. Here's what that second part looks like:
type ModalElement = LitElement & { closeModal(): void | boolean };
export class ModalHideEvent extends Event {
modal: ModalElement;
constructor(modal: ModalElement) {
super("ak-modal-hide", { bubbles: true, composed: true });
this.modal = modal;
}
}
declare global {
interface GlobalEventHandlersEventMap {
"ak-modal-hide": ModalHideEvent;
}
}
(ak-modal-show
is almost an exact copy, so I elided it from the example. I chose to create two
different explicit events: "Show" carries a distinct semantic meaning, so it has a distinct
identity.)
This idiom is much better than using CustomEvent with an untyped payload in detail
.
First, the name of the event is embedded in the event. There's no chance of your mis-typing the event name when you want to use it.
Second, the payload of the event has a readable name that's useful for receivers to understand and
read. You don't have to puzzle out what detail
is carrying, or add another layer of indirection so
that detail
carries an object with meaningful names around.
Third, by adding the declare global
declaration here, you inform the TypeScript compiler of the
type and how it's used; anyone instantiating the event will be dinged for not providing the required
field, and (in this case) for the required field to comply with the type (A LitElement with a
.closeModal()
method, in this example). And your IDE should highlight the fields if your handler
does not use the event object correctly.
Fourth, because this is a high-level ReactiveController, packaging these at the top of the handler's file gives anyone who's going to use this protocol notice that these are the events and the interface they must adhere to in order to use it correctly.
It's perfectly possible and reasonable to make the last argument to the constructor an instance of
EventOptions so you can override the bubbles
and composed
fields. I didn't here because we
didn't need it.
If you're programming in TypeScript, instead of using CustomEvent you should inherit directly from Event and write your event classes explicitly. The result is an event loop that is much easier to reason about, more difficult to get wrong, and possible to type-check correctly with tools such as TSC and ESlint. You'll have a better experience working with Events you defined yourself.