@component

:point_right: Make sure to first read the introduction to decorators.

Makes the decorated widget class a “custom component” with the following features:

Isolation

A widget class decorated with @component will not allow its own children to be selected by public API or by any of its parents, preventing accidental manipulation due to clashing id or class values. The class itself can still select its own children using the protected methods _children, _find and _apply, or by using @getById on a private/protected property.

@component
class CustomComponent extends Composite {

  constructor(properties?: Properties<Composite>) {
    super();
    this.set(properties).append(
      <TextInput id='foo' text='bar'/>
    );
  }

  public getFoo() {
    return this._find('#foo').only(TextInput).text;
  }

}

const myComponent = new CustomComponent();

// Prints 'bar':
console.log(myComponent.getFoo());

// Throws since no matching widget can be found:
console.log(myComponent.find('#foo').only(TextInput).text);

One way bindings

For one-way bindings, @component enables JSX attributes of the following format:

bind-<targetProperty>=<Binding>

Where <Binding> can be

  • a path string of the format '<componentProperty>[.<subProperty>]'
  • an object of the type {path: string, converter?: Function}
  • or a call to(path: string, converter: Function) (“to” must be imported from 'tabris-decorators')

This actively copies values from the component to the target element. The target element has to be a child (or indirect descendant) widget.

Example:

@component
class CustomComponent extends Composite {

  @property public myText: string = 'foo';

  constructor(properties?: Properties<Composite>) {
    super();
    this.set(properties).append(
      <TextView bind-text='myText'/>
    );
  }

}

This applies changes of the component property myText to the target property text of the target element textView. The component property must be a “real” Tabris.js-style property, i.e. fire change events and perform type checks. This can be achieved by simply adding a @property decorator to any field, but an explicit implementation with set/get also works. The bindings are resolved when append is called the first time. Appending/detaching widgets after that has no effect.

Binding to sub-property

You can bind to a property of a component property value if its a model-like object:

class MyItem {
  public myText: string = 'foo';
}

@component
class CustomComponent extends Composite {

  @property public item: MyItem = new MyItem();

  constructor(properties?: Properties<Composite>) {
    super();
    this.set(properties).append(
      <TextView bind-text='item.myText'/>
    );
  }

}

The item is treated as immutable, meaning The binding will not update the target property when the property on the item changes, only when the item is replaced. This may change in the future.

Conversion

The value of the component property can be manipulated or converted in a binding using a converter function.

In this example Date instance person.dob, (date of birth) will be converted to a localized string:

<TextView bind-text={{path: 'person.dob', converter: v => v.toLocaleString()}} />

There is also a utility function to that makes this expression slightly shorter:

import {to} from 'tabris-decorators';

//...

<TextView bind-text={to('person.dob', v => v.toLocaleString())} />`

It can also be used to define reuseable shorthands:

 // define shorthand, maybe in some other module:
 const toLocaleString = (path: string) => to(path, v => v.toLocaleString());

 // later use:
<TextView bind-text={toLocaleString('person.dob')} />

Fallback value

If the component property is set to undefined, the target property will be reset to its initial value (from the point in time when the binding was initialized). In the above example this would be an empty string, since that is the default value of the TextView property text. But it can also be the value that is given in JSX:

<TextView bind-text='myText' text='fallback value'/>

This behavior exists only for undefined, null is passed through without changes. To be able to set undefined on the target property via a binding you have to make that its initial value.

Template strings

Using template- as a JSX attribute prefix creates a one-way binding where the component property value is embedded in a template string:

template-<targetProperty>='<string>${<path>}<string>'

The template has to contain exactly one ${<path>} placeholder, where <path> is a string of the same syntax as one-way bindings using the bind- prefix. While this feature lends from the JavaScript template string syntax, it is not using backticks!

Example:

  @component
  class CustomComponent extends Composite {

    @property public name: string = 'Peter';

    constructor(properties: CompositeProperties) {
      super(properties);
      this.append(
        <textView template-text='Hello ${name}!' text='No one here?'/>
      );
    }

  }

This results in 'Hello Peter!' initially, and falls back to 'No one here?' if name is set to undefined.

Notes on type safety

The data binding enabled by @component can not rely on the TypeScript compiler to ensure type safety. Therefore runtime type value checks need to be performed.

For all properties of built-in Tabris.js widgets this is already the case. Also, if a property is decorated with @property or @bind, type checks are added implicitly. However, if the property type is an advanced type or an interface, this is not possible and the binding will fail as a precaution. In this case you need to set the typeGuard parameter of @property/@bind to perform the check explicitly.

If the properties involved are not decorated by @property or @bind they are expected to perform the type check in the setter.

Two way bindings

See @bind.

Direct Child Access

See @getById.