See how Stencil fits into the entire Ionic Ecosystem ->
Stencil is part of the Ionic Ecosystem ->

Prop Decorator

Props are custom attribute/properties exposed publicly on the element that developers can provide values for. Children components should not know about or reference parent components, so Props should be used to pass data down from the parent to the child. Components need to explicitly declare the Props they expect to receive using the @Prop() decorator. Props can be a number, string, boolean, or even an Object or Array. By default, when a member decorated with a @Prop() decorator is set, the component will efficiently rerender.

// TodoList.tsx
import { Prop } from '@stencil/core';
import { MyHttpService } from '../some/local/directory/MyHttpService';
...
export class TodoList {
  @Prop() color: string;
  @Prop() favoriteNumber: number;
  @Prop() isSelected: boolean;
  @Prop() myHttpService: MyHttpService;
}

When using user-defined types like MyHttpService, the type must be exported using the export keyword. Above, MyHttpService is imported from '../some/local/directory/MyHttpService', and must be exported from that file.

If MyHttpService were defined in TodoList.tsx, the export keyword would still be required, as Stencil needs to know what type myHttpService is when passing an instance of MyHttpService to TodoList from a parent component.

// TodoList.tsx
import { Prop } from '@stencil/core';
...
// the export keyword is still required here, so that a parent component knows what a `MyHttpService` is
export type MyHttpService = {
  // type definition goes here  
};
...
export class TodoList {
  @Prop() color: string;
  @Prop() favoriteNumber: number;
  @Prop() isSelected: boolean;
  @Prop() myHttpService: MyHttpService;
}

Within the TodoList class, the Props are accessed via the this operator.

logColor() {
  console.log(this.color)
}

Externally, Props are set on the element.

In HTML, you must set attributes using dash-case:

<todo-list color="blue" favorite-number="24" is-selected="true"></todo-list>

in JSX you set an attribute using camelCase:

<todo-list color="blue" favoriteNumber={24} isSelected="true"></todo-list>

They can also be accessed via JS from the element.

const todoListElement = document.querySelector('todo-list');
console.log(todoListElement.myHttpService); // MyHttpService
console.log(todoListElement.color); // blue

Prop options

The @Prop(opts?: PropOptions) decorator accepts an optional argument to specify certain options, such as the mutability, the name of the DOM attribute or if the value of the property should or shouldn't be reflected into the DOM.

export interface PropOptions {
  attribute?: string;
  mutable?: boolean;
  reflect?: boolean;
}

Prop mutability

It's important to know, that a Prop is by default immutable from inside the component logic. Once a value is set by a user, the component cannot update it internally.

However, it's possible to explicitly allow a Prop to be mutated from inside the component, by declaring it as mutable, as in the example below:

import { Prop } from '@stencil/core';

...
export class NameElement {

  @Prop({ mutable: true }) name: string = 'Stencil';

  componentDidLoad() {
    this.name = 'Stencil 0.7.0';
  }
}

Attribute Name

Properties and component attributes are strongly connected but not necessarily the same thing. While attributes are a HTML concept, properties are a JS one inherent from Object-Oriented Programming.

In Stencil, the @Prop() decorator applied to a property will instruct the Stencil compiler to also listen for changes in a DOM attribute.

Usually the name of a property is the same as the attribute, but this is not always the case. Take the following component as example:

import { Component, Prop } from '@stencil/core';

@Component({ tag: 'my-cmp' })
class Component {
  @Prop() value: string;
  @Prop() isValid: boolean;
  @Prop() controller: MyController;
}

This component has 3 properties, but the compiler will create only 2 attributes: value and is-valid.

<my-cmp value="Hello" is-valid></my-cmp>

Notice that the controller type is not a primitive, since DOM attributes can ONLY be strings, it does not make sense to have an associated DOM attribute called "controller".

At the same time, the isValid property follows a camelCase naming, but attributes are case-insensitive, so the attribute name will be is-valid by default.

Fortunately, this "default" behaviour can be changed using the attribute option of the @Prop() decorator:

import { Component, Prop } from '@stencil/core';

@Component({ tag: 'my-cmp' })
class Component {
  @Prop() value: string;
  @Prop({ attribute: 'valid' }) isValid: boolean;
  @Prop({ attribute: 'controller' }) controller: MyController;
}

By using this option, we are being explicit about which properties have an associated DOM attribute and the name of it.

Reflect Properties Values to Attributes

In some cases it may be useful to keep a Prop in sync with an attribute. In this case you can set the reflect option in the @Prop() decorator to true, since it defaults to false:

@Prop({
  reflect: true
})

When a "prop" is set to "reflect", it means that their value will be rendered in the DOM as an HTML attribute:

Take the following component as example:

@Component({ tag: 'my-cmp' })
class Cmp {
  @Prop({ reflect: true }) message = 'Hello';
  @Prop({ reflect: false }) value = 'The meaning of life...';
  @Prop({ reflect: true }) number = 42;
}

When rendered in the DOM, it will look like:

<my-cmp message="Hello" number="42"></my-cmp>

Notice that properties set to "reflect" (true) render as attributes, and properties not set to "reflect" do not.

While the properties not set to "reflect", such as 'value', are not rendered as attributes, it does not mean it's not there - the value property still contains the The meaning of life... value as assigned:

const cmp = document.querySelector('my-cmp');
console.log(cmp.value); // it prints 'The meaning of life...'

Prop default values and validation

Setting a default value on a Prop:

import { Prop } from '@stencil/core';

...
export class NameElement {
  @Prop() name: string = 'Stencil';
}

To do validation of a Prop, you can use the @Watch() decorator:

import { Prop, Watch } from '@stencil/core';

...
export class TodoList {
  @Prop() name: string = 'Stencil';

  @Watch('name')
  validateName(newValue: string, oldValue: string) {
    const isBlank = typeof newValue !== 'string' || newValue === '';
    const has2chars = typeof newValue === 'string' && newValue.length >= 2;
    if (isBlank) { throw new Error('name: required') };
    if (!has2chars) { throw new Error('name: has2chars') };
  }
}
BackNext
Contributors