Andy Desmarais

A code ninja and all around tech geek

StencilJs - Part 3 - @Prop, @Watch, and @Method Decorators

2019-12-03 Andy DesmaraisWebComponents

Cover photo credit: Drew Patrick Miller

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.

Component interfaces

The @Prop and @Method decorators help us provide the interface for our web components to consumers. They handle the public facing contract that consumers will use to interact with and update our components.

There are some misnomers and nuance to be aware of with each of the three decorators. We’re going to take a deep look at:

The @Prop decorator

@Prop is a bit of a misnomer. It’s not only a property but it also represents an attribute on the tag of our component. This makes it a powerful way to interface with a consumer. They can declaratively pass down primitive types that can be parsed from a string using the attribute, or handle more complex data using the property.

Camel case to kebab case

Stencil reflects @Prop class properties to the tag as a kebab case translation of the camel case variable.

As an example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Prop() anExampleProp: string;
}

Is used in HTML like this:

<my-custom-element an-example-prop="Something stringy"></my-custom-element>

Primitive types

Props need to be a primitive type if they are going to be reflected as attributes. This means that we can only take primitive types in from attributes. Stencil will automatically handle the parsing and type casting of primitive types for you.

Here’s an example:

@Component({
  tag: 'my-custom-element',
})
export class MyCustomElement {
  @Prop() aString: string;
  @Prop() aNumber: number;
  @Prop() aBoolean: boolean;

  render() {
    return [
      <div>{typeof this.aString}</div>,
      <div>{typeof this.aNumber}</div>,
      <div>{typeof this.aBoolean}</div>,
    ];
  }
}

HTML:

  <my-component
    a-string="This is a string"
    a-number="12345"
    a-boolean="true"
    ></my-component>

The output of this is:

<div>string</div>
<div>number</div>
<div>boolean</div>

Complex data types

When you need a non-primitive type to be passed in then your consumer will need to reference the tag directly and assign properties on that object directly. This can be done as the element is created, but before it is attached to the DOM.

Here’s an example of what that can look like:

@Component({
  tag: 'my-custom-element',
})
export class MyCustomElement {
  @Prop() aUrl: URL;

  render() {
    return [
      <div>{this.aUrl.toString()}</div>,
    ];
  }
}

HTML:

  <my-custom-element></my-custom-element>
  <script>
    document.querySelector('my-custom-element').aUrl = new URL('https://google.com');
  </script>

The output of this is:

<div>https://google.com/</div>

Attribute name

A common problem with writing web components with Stencil is the desire to have the class property name differ from the attribute name on the tag. You may want isValid as the property, but valid as the attribute. This can be accomplished using the attribute property of @Prop.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Prop({ attribute: 'different' }) differentFromProperty: string;
  @Prop() sameAsProperty: string;
}

DOM usage:

<my-custom-element different="Simpler" same-as-property="potentially more complex"></my-custom-element>

Immutability

It’s important to know that all of the class properties marked with @Prop should be treated as immutable by default. This is good basic practice to ensure a more one-way binding approach is being taken.

However, there are exceptions to every rule, and because of this Stencil allows you to override this default. Setting the mutable property on the @Prop decorator will tell consumers you plan to modify the value of the property.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Prop({
    mutable: true // Defaults to false
  }) aMutableProp: string = 'A mutable default';
  @Prop() immutableProp: string = 'Should not be changed later';

  componentDidLoad() {
    this.aMutableProp = 'Something completely different';
  }
}

NOTE: As of version Stencil 1.3.3 they do not appear to be enforcing this! It’s up to you as a developer to be disciplined in using this property appropriately. You will not get an error if you change a prop that is marked mutable: false, which is the current default.

Reflect

This property is designed to allow you to reflect values of @Prop properties as attributes on the custom element tag. This is valuable for providing the DOM with updates as values are changing in your custom element.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  @Prop({
    reflect: true // Defaults to false
  }) aReflectedProp: string = 'I am reflected';
  @Prop() unreflectedProp: string = 'I am not updated in the DOM';
}

After the component is loaded the DOM will reflect the aReflectedProp value.

<my-custom-element a-reflected-prop="I am reflected"></my-custom-element>

The @Watch decorator

If you read through my series on web components you’ll know about the attributeChangedCallback. The @Watch decorator is how Stencil exposes this functionality.

@Watch takes the name of the @Prop variable to monitor as a parameter. Any time the value of that prop changes the function decorated by @Watch will be invoked with the ‘newValue’ and ‘oldValue’ as parameters. This is called first out of the lifecycle callbacks after a prop changes.

The value of @Watch is that you can do property validation before any of the other lifecycle events fire. If someone provided an invalid value to the @Prop, then throwing an error and correcting can prevent any potentially odd behavior.

NOTE: @Watch is NOT invoked for the first render. It is only invoked for subsequent changes.

Quick example:

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
  private isInvalidUsername: boolean = false;

  @Prop() username: string;
  @Watch('username')
  validateDate(newValue, oldValue) {
      if(newValue.trim() === '') {
          isInvalidUsername = true;
          throw new Error('username is required');
      }
  }
}

The @Method decorator

This might sound odd, but don’t use this if at all possible. Using publicly facing methods will be much more challenging for a consumer then a prop/attribute. It also breaks traditional models for how frameworks will interact with your component. They are available if you cannot find a way to work with a prop/attribute effectively.

Now let’s talk about how they actually work!

The @Method decorator is designed to let a consumer know about a publicly facing method. It has only one requirement: the function MUST return a promise. This can be accomplished either by marking the function async, or by returning a promise directly.

Quick example: mirrors Stencil docs

@Component({
  tag: 'my-custom-element'
})
export class MyCustomElement {
    // VALID: using async
    @Method()
    async myMethod() {
        return 42;
    }

    // VALID: using Promise.resolve()
    @Method()
    myMethod2() {
        return Promise.resolve(42);
    }

    // VALID: even it returns nothing, async will force it to return a promise
    @Method()
    async myMethod3() {
        console.log(42);
    }

    // INVALID
    @Method()
    notOk() {
        return 42;
    }
}

Wrapping up

This article covers all of the ways a consumer can directly interact with a Stencil component. It is strongly recommended to use @Prop instead of @Method for interacting with a component, and @Watch gives us a way to handle validation we would otherwise need a method for.