When Web Components need to communicate state changes to the application, it uses Custom Events, just like native events built into the browser. Let’s take a look at a simple example of a component emitting a custom event.
const template = document.createElement('template');
template.innerHTML = `<button>Emit Event!</button>`;
export class Widget extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('myCustomEvent', { detail: 'hello there' }));
});
}
}
customElements.define('my-widget', Widget);
Our widget is a basic custom element containing a single button. With our widget, we can listen to the click event from the component template to trigger a custom event.
this.dispatchEvent(new CustomEvent('myCustomEvent', { detail: 'hello there' }));
With custom events, we pass an event name and a configuration object. This configuration object allows us to pass a value using the detail
property. Once we have the event setup, we can listen to our new custom event.
<my-widget></my-widget>
import './widget';
const widget = document.querySelector('my-widget');
widget.addEventListener('myCustomEvent', (event) => {
alert(`myCustomEvent:, ${event.detail}`);
});
Just like any other DOM event, we can create an event listener to get notified when our event is triggered. We can improve the reliability of our custom events by using TypeScript decorators to create a custom @event
decorator.
Example Alert Web Component
For our example, we will be making a simple alert component to show messages to the user. This component will have a single property
to determine if the alert can be dismissed and a single event
to notify the application when the user has clicked the dismiss button.
Our Web Component is using lit-element. Lit Element is a lightweight library for making it easy to build Web Components. Lit Element and its templating library lit-html provide an easy way to bind data and render HTML within our components. Here is our example component:
import { LitElement, html, css, property } from 'lit-element';
import { event, EventEmitter } from './event';
class Alert extends LitElement {
@property() dismiss = true;
render() {
return html`
<slot></slot>
${this.dismissible
? html`<button aria-label="dismiss" @click=${() => this.dismissAlert()}>×</button>`
: ''}
`;
}
dismissAlert() {
this.dispatchEvent(new CustomEvent('dismissChange', { detail: 'are you sure?' }));
}
}
customElements.define('app-alert', Alert);
Our alert component can show or hide a dismiss button. When a user clicks the dismiss button, we emit a custom event dismissChange
.
this.dispatchEvent(new CustomEvent('dismissChange', { detail: 'are you sure?' }));
By using TypeScript, we can improve handling our custom events. Custom events are dynamic, so it’s possible to make a mistake emitting different types on the same event.
this.dispatchEvent(new CustomEvent('dismissChange', { detail: 'are you sure?' }));
this.dispatchEvent(new CustomEvent('dismissChange', { detail: 100 }));
I can emit a string or any other value and make the event type value inconsistent. This will make it hard to use the component in our application. By creating a custom decorator, we can catch some of these errors at build time.
TypeScript Decorator and Custom Events
Let’s take a look at what our custom decorator looks like in our alert Web Component.
import { LitElement, html, css, property } from 'lit-element';
import { event, EventEmitter } from './event';
class Alert extends LitElement {
@property() dismiss = true;
@event() dismissChange: EventEmitter<string>;
render() {
return html`
<slot></slot>
${this.dismiss
? html`<button aria-label="dismiss" @click=${() => this.dismissAlert()}>×</button>`
: ''}
`;
}
dismissAlert() {
this.dismissChange.emit('are you sure?');
}
}
customElements.define('app-alert', Alert);
Using the TypeScript decorator syntax, we can create a property in our class, which will contain an event emitter to manage our components events. The @event
decorator is a custom decorator that will allow us to emit events with type safety easily.
We leverage TypeScript Generic Types to describe what type we expect to emit. In this use case, we will be emitting string values.
@event() dismissChange: EventEmitter<string>;
...
dismissAlert() {
this.dismissChange.emit('are you sure?');
this.dismissChange.emit(100); // error: Argument of type '100' is not assignable to parameter of type 'string'
}
So let’s take a look at how we make a @event
decorator. First, we are going to make a small EventEmitter
class.
export class EventEmitter<T> {
constructor(private target: HTMLElement, private eventName: string) {}
emit(value: T, options?: EventOptions) {
this.target.dispatchEvent(
new CustomEvent<T>(this.eventName, { detail: value, ...options })
);
}
}
Our EventEmitter
class defines a generic type <T>
so we can ensure we always provide a consistent value type when emitting our event. Our decorator will create an instance of the EventEmitter
and assign it to the decorated property. Because decorators are not yet standardized in JavaScript, we have to check if the decorator is being used by TypeScript or Babel. If you are not writing a library but an application, this check may not be necessary.
export function event() {
return (protoOrDescriptor: any, name: string): any => {
const descriptor = {
get(this: HTMLElement) {
return new EventEmitter(this, name !== undefined ? name : protoOrDescriptor.key);
},
enumerable: true,
configurable: true,
};
if (name !== undefined) {
// legacy TS decorator
return Object.defineProperty(protoOrDescriptor, name, descriptor);
} else {
// TC39 Decorators proposal
return {
kind: 'method',
placement: 'prototype',
key: protoOrDescriptor.key,
descriptor,
};
}
};
}
Decorators are just JavaScript functions that can append behavior to and existing property or class. Here we create an instance of our EventEmitter
service and can start using it in our alert.
import { LitElement, html, css, property } from 'lit-element';
import { event, EventEmitter } from './event';
class Alert extends LitElement {
@property() dismiss = true;
@event() dismissChange: EventEmitter<string>;
render() {
return html`
<slot></slot>
${this.dismiss
? html`<button aria-label="dismiss" @click=${() => this.dismissAlert()}>×</button>`
: ''}
`;
}
dismissAlert() {
// type safe event decorator, try adding a non string value to see the type check
this.dismissChange.emit('are you sure?');
}
}
customElements.define('app-alert', Alert);
If you want an in-depth tutorial about TypeScript property decorators, check out Introduction to TypeScript Property Decorators. The full working demo can be found here https://stackblitz.com/edit/typescript-xuydzd
Top comments (0)