Andy Desmarais

A code ninja and all around tech geek

StencilJs - Part 4 - @Event and @Listen Decorators

2019-12-15 Andy DesmaraisWebComponents

Cover photo credit: Alphacolor

This article assumes you have a working knowledge of web components. If you don’t, please check out my earlier series on web components.

It’s also building on a previous article, so please check that out too.

Firing events

Events are an important part of any web component that is going to take user feedback. It’s the component’s way to let consumers know that something has happened.

Luckily for us Stencil makes this incredibly easy. The @Event property allows us to expose an EventEmitter that will publish events. The @Event decorator takes some options that can help us customize how the event will be published.

The EventEmitter is a generic type allowing us to mark it with the data type that will be published. For example, if we want to publish a number in the event we would mark it like this: EventEmitter<number>

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Event() numberOfClicks: EventEmitter<number>;
}

Event name

By default the event will be named the same as the variable name. In the example above the event would be named numberOfClicks. This is not always desirable, and because of this we have the eventName option that can be provided to the @Event decorator. It does exactly what it says; sets the name of the published event.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Event({
    eventName: 'greatEventName'
  }) aTerribleEventNameYouWouldNotWantButMakesSenseInTheCode: EventEmitter<number>;
}

Bubbles

Nope, it’s not the name of your favorite pet. bubbles controls whether or not the event should bubble through the DOM. It’s talked about more thoroughly on MDN.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Event({
    bubbles: false // defaults to true
  }) anEventThatWillNotBubbleThroughTheDom: EventEmitter<number>;
}

Cancelable

This property is designed to allow events to be cancelled. This really translates to whether or not you can use event.preventDefault() or not. More reading on MDN if you’re interested.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Event({
    cancelable: false // defaults to true
  }) anEventThatCannotBeCancelled: EventEmitter<number>;
}

Composed

This is one of the cooler new properties for events. Setting the composed option to false will prevent the emitted event from crossing the shadow DOM boundary. It’s a powerful way to write components that have a heavy reliance on events internally, but don’t want those events exposed to consumers. Once again, more reading on MDN if you’re interested.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Event({
    composed: false // defaults to true
  }) willNotEscapedShadowRoot: EventEmitter<number>;
}

@Listen decorator

Emitting events isn’t helpful if we can’t respond to them in a reasonable way. Stencil handles this via the @Listen decorator. This decorates a function that will handle the event. It takes two parameters, the event name and a set of options to set the behavior of the event handling.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Listen('theNameOfAnEvent')
  function(event) {}
}

Target

The first option for the @Listen decorator is target. This option allows us to set where we will listen for the event. The options are:

  • parent - Good for keeping event response close to the emitter.
  • body - Good for capturing events fired by any element in the body.
  • document - Useful for document ready, and other document specific events.
  • window - Highest level, and helpful for things like window scroll events.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Listen('scoll', {
    target: 'window'
  })
  function(scrollEvent) {}
}

Capture

This option is about when during the event lifecycle the handler will be called. There’s some nuance to the event lifecycle, so I’ll leave that for those who would like some additional reading. The key part is that a listener marked as capture: true will be fired before one that is marked capture: false.

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Listen('theNameOfAnEvent', {
    capture: true
  })
  function(eventFiredFirst) {}

  @Listen('theNameOfAnEvent', {
    capture: false // The default
  })
  function(eventFiredSecond) {}
}

Passive

This will guarantee to the DOM that the event being fired will not .stopPropagation(). This allows the DOM more knowledge of how to work with the event handler. If .stopPropagation() is called inside a function marked passive it will not have any effect and a warning will be thrown to the console.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Listen('theNameOfAnEvent', {
    passive: true // The default is false
  })
  function(eventTheWillNotBeStopped) {}
}

Wrapping up

We’ve covered all of the important information needed to handle DOM events using Stencil. Between @Event and @Listen we can now emit all types of events, and handle them.