Cory Rylan

My name is , Google Developer Expert, Speaker, Software Developer. Building Design Systems and Web Components.

Follow @coryrylan
Lit Web Components

Using Event Decorators with lit-element and Web Components

Cory Rylan

- 4 minutes

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.

A simple alert Web Component

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()}>&times;</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()}>&times;</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()}>&times;</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 below!

View Demo Code   
Twitter Facebook LinkedIn Email
 

No spam. Short occasional updates on Web Development articles, videos, and new courses in your inbox.

Related Posts

Lit Web Components

High Performance HTML Tables with Lit and CSS Contain

Learn how to easily create HTML tables in Lit with high performance rendering using CSS contain.

Read Article
Lit Web Components

High Performance HTML Tables with Lit and Virtual Scrolling

Learn how to easily create HTML tables in Lit from dynamic data sources.

Read Article
Lit Web Components

Creating Dynamic Tables in Lit

Learn how to easily create HTML tables in Lit from dynamic data sources.

Read Article