添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Bindable properties

How to create components that accept one or more bindable properties. You might know these as "props" if you are coming from other frameworks and libraries.

Bindable properties

When creating components, sometimes you will want the ability for data to be passed into them instead of their host elements. The @bindable decorator allows you to specify one or more bindable properties for a component.

The @bindable attribute also can be used with custom attributes as well as custom elements. The decorator denotes bindable properties on components on the view model of a component.

loader-component.ts
import { bindable } from 'aurelia';
export class LoaderComponent {
    @bindable loading = false;
}

This will allow our component to be passed in values. Our specified bindable property here is called loading and can be used like this:

loader-component.html
<loader loading.bind="true"></loader>

In the example above, we are binding the boolean literal true to the loading property.

Instead of literal, you can also bind another property ( loadingVal in the following example) to the loading property.

loader-component.html
<loader loading.bind="loadingVal"></loader>

As seen in the following example, you can also bind values without the loading.bind part.

<loader loading="true"></loader>

Aurelia treats attribute values as strings. This means when working with primitives such as booleans or numbers, they won't come through in that way and need to be coerced into their primitive type using a bindable setter or specifying the bindable type explicitly using bindable coercion .

The @bindable decorator signals to Aurelia that a property is bindable in our custom element. Let's create a custom element where we define two bindable properties.

import { bindable } from 'aurelia';
export class NameComponent {
    @bindable firstName = '';
    @bindable lastName  = '';
}

You can then use the component in this way,` <name-component first-name="John" last-name="Smith"></name-component>

Calling a change function when bindable is modified

By default, Aurelia will call a change callback (if it exists) which takes the bindable property name followed by Changed added to the end. For example, firstNameChanged(newVal, previousVal) would fire every time the firstName bindable property is changed.

Due to the way the Aurelia binding system works, change callbacks will not be fired upon initial component initialization. If you worked with Aurelia 1, this behavior differs from what you might expect.

If you would like to call your change handler functions when the component is initially bound (like v1), you can achieve this the following way:

import { bindable } from 'aurelia';
export class NameComponent {
    @bindable firstName = '';
    @bindable lastName  = '';
    bound() {
        this.firstNameChanged(this.firstName, undefined);
    firstNameChanged(newVal, oldVal) {
        console.log('Value changed');
}

Configuring bindable properties

Like almost everything in Aurelia, you can configure how bindable properties work.

Change the binding mode using mode

You can specify the binding mode using the mode property and passing in a valid BindingMode to it; @bindable({ mode: BindingMode.twoWay}) - this determines which way changes flow in your binding. By default, this will be BindingMode.oneWay

Please consult the binding modes documentation below to learn how to change the binding modes. By default, the binding mode for bindable properties will be one-way

Change the name of the change callback

You can change the name of the callback that is fired when a change is made @bindable({ callback: 'propChanged' })

import { bindable } from 'aurelia';
export class NameComponent {
    @bindable({ mode: BindingMode.twoWay}) firstName = '';
    @bindable({ callback: 'lnameChanged' }) lastName  = '';
    lnameChanged(val) {}
}

Bindable properties support many different binding modes determining the direction the data is bound in and how it is bound.

One way binding

By default, bindable properties will be one-way binding. This means values flow into your component but not back out of it (hence the name, one way).

Bindable properties without an mode explicitly set will be one-way by default. You can also explicitly specify the binding mode.

import { bindable, BindingMode } from 'aurelia';
export class Loader {
    @bindable({ mode: BindingMode.oneWay })
}

Two-way binding

Unlike the default, the two-way binding mode allows data to flow in both directions. If the value is changed with your component, it flows back out.

import { bindable, BindingMode } from 'aurelia';
export class Loader {
    @bindable({ mode: BindingMode.twoWay})
}

Working with two-way binding

Much like most facets of binding in Aurelia, two-way binding is intuitive. Instead of .bind you use .two-way if you need to be explicit, but in most instances, you will specify the type of binding relationship a bindable property is using with @bindable instead.

Explicit two-way binding looks like this:

<input type="text" value.two-way="myVal">

The myVal variable will get a new value whenever the text input is updated. Similarly, if myVal were updated from within the view model, the input would get the updated value.

When using .bind for input/form control values such as text inputs, select dropdowns and other form elements. Aurelia will automatically create a two-way binding relationship. So, the above example using a text input can be rewritten to be value.bind="myVal" , and it would still be a two-way binding.

Bindable setter

In some cases, you want to make an impact on the value that is binding. For such a scenario, you can use the possibility of new set .

@bindable({
    set: value => someFunction(value),  /* HERE */
    // Or set: value => value,
    mode: /* ... */
})

Suppose you have a carousel component in which you want to enable navigator feature for it.

<!-- Enable -->
<my-carousel navigator.bind="true">
<my-carousel navigator="true">
<my-carousel navigator=true>
<my-carousel navigator>
<!-- Disable -->
<my-carousel navigator.bind="false">
<my-carousel navigator="false">
<my-carousel navigator=false>
<my-carousel>

In version two, you can easily implement such a capability with the set feature.

Define your property like this:

@bindable({ set: /* ? */, mode: BindingMode.toView }) public navigator: boolean = false;

For set part, we need functionality to check the input. If the value is one of the following, we want to return true , otherwise, we return the false value.

  • '' : No input for a standalone navigator property.

  • true : When the navigator property set to true .

  • "true" : When the navigator property set to "true" .

So our function will be like this

export function truthyDetector(value: unknown) {
    return value === '' || value === true || value === "true";
}

Now, we should set truthyDetector function as follows:

@bindable({ set: truthyDetector, mode: BindingMode.toView }) public navigator: boolean = false;

Although, there is another way to write the functionality too:

@bindable({ set: v => v === '' || v === true || v === "true", mode: BindingMode.toView }) public navigator: boolean = false;

You can simply use any of the above four methods to enable/disable your feature. As you can see, set can be used to transform the values being bound into your bindable property and offer more predictable results when dealing with primitives like booleans and numbers.

Bindable & getter/setter

By default, you'll find yourself work with binable and field most of the time, like the examples given above. But there' cases where it makes sense to have bindable as a getter, or a pair of getter/setter to do more logic when get/set.

For example, a component card nav that allow parent component to query its active status. With bindable on field, it would be written like this:

@customElement({ name: 'card-nav', template })
export class CardNav implements ICustomElementViewModel {
  @bindable routes: RouteLink[] = [];
  @bindable({ mode: BindingMode.fromView }) active?: string;
  bound() {
    this.setActive();
  setActive() {
    this.active = this.routes.find((y) => y.isActive)?.path;
  handleClick(route: RouteLink) {
    this.routes.forEach((x) => (x.isActive = x === route));
    this.setActive();
}

Note that because active value needs to computed from other variables, we have to "actively" call setActive . It's not a big deal, but sometimes not desirable.

For cases like this, we can turn active into a getter, and decorate it with bindable, like the following:

@customElement({ name: 'card-nav', template })
export class CardNav implements ICustomElementViewModel {
  @bindable routes: RouteLink[] = [];
  @bindable({ mode: BindingMode.fromView }) get active() {
    return this.routes.find((y) => y.isActive)?.path;
  handleClick(route: RouteLink) {
    this.routes.forEach((x) => (x.isActive = x === route));
}

Simpler, since the value of active is computed, and observed based on the properties/values accessed inside the getter.

Bindable coercion

The bindable setter section shows how to adapt the value is bound to a @bindable property. One common usage of the setter is to coerce the values that are bound from the view. Consider the following example.

@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable public num: number;
}

Without any setter for the @bindable num we will end up with the string '42' as the value for num in MyEl . You can write a setter to coerce the value. However, it is a bit annoying to write setters for every @bindable .

Automatic type coercion

To address this issue, Aurelia 2 supports type coercion. To maintain backward compatibility, automatic type coercion is disabled by default and must be enabled explicitly.

new Aurelia()
    .register(
      StandardConfiguration
        .customize((config) => {
          config.coercingOptions.enableCoercion = true;
          // config.coercingOptions.coerceNullish = true;
    );

There are two relevant configuration options.

enableCoercion

The default value is false ; that is Aurelia 2 does not coerce the types of the @bindable by default. It can be set to true to enable the automatic type-coercion.

coerceNullish

The default value is false ; that is Aurelia2 does not coerce the null and undefined values. It can be set to true to coerce the null and undefined values as well. This property can be thought of as the global counterpart of the nullable property in the bindable definition (see Coercing nullable values section).

Additionally, depending on whether you are using TypeScript or JavaScript for your app, there can be several ways to use automatic type coercion.

Specify type in @bindable

You need to specify the explicit type in the @bindable definition.

@customElement({ name:'my-el', template: 'not important' })
export class MyEl {
  @bindable({ type: Number }) num : number;
}

The rest of the document is based on TypeScript examples. However, we trust that you can transfer that knowledge to your JavaScript codebase if necessary.

Coercing primitive types

Currently, coercing four primitive types are supported out of the box. These are number , string , boolean , and bigint . The coercion functions for these types are respectively Number(value) , String(value) , Boolean(value) , and BigInt(value) .

Be mindful when dealing with bigint as the BigInt(value) will throw if the value cannot be converted to bigint; for example null , undefined , or non-numeric string literal.

Coercing to instances of classes

It is also possible to coerce values into instances of classes. There are two ways how that can be done.

Using a static coerce method

You can define a static method named coerce in the class used as a @bindable type. This method will be called by Aurelia2 automatically to coerce the bound value.

This is shown in the following example with the Person class.

export class Person {
  public constructor(
    public readonly name: string,
    public readonly age: number,
  ) { }
  public static coerce(value: unknown): Person {
    if (value instanceof Person) return value;
    if (typeof value === 'string') {
      try {
        const json = JSON.parse(value) as Person;
        return new this(json.name, json.age);
      } catch {
        return new this(value, null!);
    if (typeof value === 'number') {
      return new this(null!, value);
    if (typeof value === 'object' && value != null) {
      return new this((value as any).name, (value as any).age);
    return new this(null!, null!);
}