Using Event Decorators with lit-element and Web Components
Tags: webcomponents, typescript
Published: 2020-06-29 02:24:26

---
title: Using Event Decorators with lit-element and Web Components
published: true
date: 2020-06-22 05:00:00 UTC
tags: webcomponents, typescript
canonical_url: https://coryrylan.com/blog/using-event-decorators-with-lit-element-and-web-components
---

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.

```typescript
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.

```typescript
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.

```html
<my-widget></my-widget>
```

```typescript
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](https://coryrylan.com/assets/images/posts/2020-06-22-using-event-decorators-with-lit-element-and-web-components/example-alert-web-component.png)

Our Web Component is using [lit-element](https://lit-element.polymer-project.org/). 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:

```typescript
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`.

```typescript
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.

```typescript
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.

```typescript
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](https://www.typescriptlang.org/docs/handbook/generics.html) to describe what type we expect to emit. In this use case, we will be emitting string values.

```typescript
@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.

```typescript
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.

```typescript
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.

```typescript
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](https://coryrylan.com/blog/introduction-to-typescript-property-decorators). The full working demo can be found here https://stackblitz.com/edit/typescript-xuydzd