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.
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 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 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, BindingMode } from 'aurelia';
export class NameComponent {
@bindable({ mode: BindingMode.toView }) firstName = '';
@bindable({ mode: BindingMode.toView }) lastName = '';
}<p>Hello ${firstName} ${lastName}. How are you today?</p>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.
{% hint style="warning" %} 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. {% endhint %}
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:
``
`typescript import { bindable } from 'aurelia';
export class NameComponent { @bindable firstName = ''; @bindable lastName = '';
}
In the above example, even though propertyChanged can be used for multiple properties (like firstName and lastName), it's only called individually for each of those properties. If you wish to act on a group of changes, like both firstName and lastName at once in the above example, propertiesChanged callback can used instead, like the following example:
For the order of callbacks when there are multiple callbacks involved, refer to the following example: If we have a component class that looks like this:
When we do
the console logs will look like the following:
Note: The individual change callback (propChanged) and propertyChanged execute immediately when the property is set, while propertiesChanged is deferred and executes asynchronously in the next tick.
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
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' })
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 (also known as toView). This means values flow into your component but not back out of it (hence the name, one way).
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.
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:
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.
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.
Suppose you have a carousel component in which you want to enable navigator feature for it.
In version two, you can easily implement such a capability with the set feature.
Define your property like this:
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 standalonenavigatorproperty.true: When thenavigatorproperty set totrue."true": When thenavigatorproperty set to"true".
So our function will be like this
Now, we should set truthyDetector function as follows:
Although, there is another way to write the functionality too:
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 work with bindable fields most of the time, like the examples above. But there are cases where it makes sense to expose a bindable getter (or getter/setter pair) so you can compute the value on the fly.
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:
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:
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.
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.
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
type in @bindableYou need to specify the explicit type in the @bindable definition.
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
coerce methodYou 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.
According to the Person#coercer implementation, for the example above MyEl#person will be assigned an instance of Person that is equivalent to new Person('john', null).
Using the @coercer decorator
@coercer decoratorAurelia2 also offers a @coercer decorator to declare a static method in the class as the coercer. The previous example can be rewritten as follows using the @coercer decorator.
With the @coercer decorator, you are free to name the static method as you like.
Coercing nullable values
To maintain backward compatibility, Aurelia2 does not attempt to coerce null and undefined values. We believe that this default choice should avoid unnecessary surprises and code breaks when migrating to newer versions of Aurelia.
However, you can explicitly mark a @bindable to be not nullable.
When nullable is set to false, Aurelia2 will try to coerce the null and undefined values.
set and auto-coercion
set and auto-coercionIt is important to note that an explicit set (see bindable setter) function is always prioritized over the type. In fact, the auto-coercion is the fallback for the set function. Hence whenever set is defined, the auto-coercion becomes non-operational.
However, this gives you an opportunity to:
Override any of the default primitive type coercing behavior, or
Disable coercion selectively for a few selective
@bindableby using anoopfunction forset.
Union types
When using TypeScript, usages of union types are not rare. However, using union types for @bindable will deactivate the auto-coercion.
For the example above, the type metadata supplied by TypeScript will be Object disabling the auto-coercion.
To coerce union types, you can explicitly specify a type.
However, using a setter would be more straightforward to this end.
Bindables spreading
Spreading syntaxes are supported for simpler binding of multiple bindable properties.
Given the following component:
with template:
and its usage template:
The rendered html will be:
Here we are using ...$bindables to express that we want to bind all properties in the object { first: 'John', last: 'Doe' } to bindable properties on <name-tag> component. The ...$bindables="..." syntax will only connect properties that are matching with bindable properties on <name-tag>, so even if an object with hundreds of properties are given to a ...$bindables binding, it will still resulted in 2 bindings for first and last.
...$bindables also work with any expression, rather than literal object, per the following examples:
Shorthand syntax
Sometimes when the expression of the spread binding is simple, we can simplify the binding even further. Default templating syntax of Aurelia supports a shorter version of the above examples:
Remember that HTML is case insensitive, so
...firstNameactually will be seen as...firstname, for exampleBindables properties will be tried to matched as is, which means a
firstNamebindable property will match an objectfirstNameproperty, but notfirst-nameIf the expression contains space, it will result into multiple attributes and thus won't work as intended with spread syntax
.... For example...a + bwill be actually turned into 3 attributes:...a,+andb
Binding orders
The order of the bindings created will be the same with the order declared in the template. For example, for the NameTag component above, if we have a usage
Then the value of the first property in NameTag with id=1 will be Jane, and the value of first property in NameTag with id=2 will be John.
An exception of this order is when bindables spreading is used together with
...$attrs,...$attrswill always result in bindings after...$bindables/$bindables.spread/...expression.
Observation behavior
Bindings will be created based on the keys available in the object evaluated from the expression of a spread binding. The following example illustrate the behavior:
For the NameTag component above:
The rendered HTML of <name-tag> will be
When clicking on the button with text Change last name, the rendered html of <name-tag> won't be changed, as the original object given to <name-tag> doesn't contain last, hence it wasn't observed, which ignore our new value set from the button click. If it's desirable to reset the observation, give a new object to the spread binding, like the following example:
With the above behavior of non-eager binding, applications can have the opportunity to leave some bindable properties untouched, while with the opposite behavior of always observing all properties on the given object based on the number of bindable properties, missing value (
null/undefined) will start flowing in in an unwanted way.
There are some other behaviors of the spread binding that are worth noting:
All bindings created with
$bindables.spreador...syntax will have binding mode equivalent toto-view, binding behavior cannot alter this. Though other binding behavior likethrottle/debouncecan still work.If the same object is returned from evaluating the expression, the spread binding won't try to rebind its inner bindings. This means mutating and then reassigning won't result in new binding, instead, give the spread binding a new object.
Attributes Transferring
Attribute transferring is a way to relay the binding(s) on a custom element to its child element(s).
As an application grows, the components inside it also grow. Something that starts simple, like the following component
with the template
can quickly grow out of hand with a number of needs for configuration: aria, type, min, max, pattern, tooltip, validation etc...
After a while, the FormInput component above will become more and more like a relayer to transfer the bindings from outside, to the elements inside it. This often results in an increase in the number of @bindable. While this is fine, you end up with components that have a lot of boilerplate.
And the usage of our component would look like this:
to be repeated like this inside:
To juggle all the relevant pieces for such a task isn't difficult, but somewhat tedious. With attribute transferring, which is roughly close to object spreading in JavaScript, the above template should be as simple as:
, which reads like this: for some bindings on <form-input>, change the targets of those bindings to the <input> element inside it.
Usage
To transfer attributes & bindings from a custom element, there are two steps:
Set
capturetotrueon a custom element via@customElementdecorator:
Or use the capture decorator from aurelia package if you don't want to declare the customElement decorator and have to specify your name and template values.
As the name suggests, this is to signal the template compiler that all the bindings & attributes, with some exceptions, should be captured for future usage.
Spread the captured attributes onto an element
Using the ellipsis syntax which you might be accustomed to from Javascript, we can spread our attributes onto an element proceeding the magic variable $attrs
Spread attributes and overriding specific ones
In case you want to spread all attributes while explicitly overriding individual ones, make sure these come after the spread operator.
It's recommended that this feature should not be overused in multi-level capturing & transferring. This is often known as prop-drilling in React and could have a bad effect on the overall & long-term maintainability of an application. It's probably healthy to limit the max level of transferring to 2.
Usage with conventions
Aurelia conventions enable the setting of capture metadata from the template via <capture> tag, like the following example:
Attribute filtering
Sometimes it is desirable to capture only certain attributes on a custom element. Aurelia supports this via a function form of the custom element capture value: a function that takes 1 parameter (the attribute name) and returns a boolean to indicate whether it should be captured.
Note: When using a capture filter function, you cannot use the standalone @capture decorator. You must specify the filter function within the @customElement decorator's capture property.
How it works
What attributes are captured
Everything except the template controller and custom element bindables are captured.
A usage example is as follows:
What is captured:
value.bind="extraComment"class="form-control"style="background: var(--theme-purple)"tooltip="Hello, ${tooltip}"
What is not captured:
if.bind="needsComment"(ifis a template controller)label.bind="label"(labelis a bindable property)
How will attributes be applied in ...$attrs
Attributes that are spread onto an element will be compiled as if it was declared on that element.
This means .bind command will work as expected when it's transferred from some element onto some element that uses .two-way for .bind.
It also means that spreading onto a custom element will also work: if a captured attribute targets a bindable property of the applied custom element. An example:
if value is a bindable property of my-input, the end result will be a binding that connects the message property of the corresponding app.html view model with <my-input> view model value property. The binding mode is also preserved like normal attributes.
Performance Considerations
Spread vs Individual Bindings
When deciding between spread syntax and individual bindings, consider the following performance implications:
Spread Syntax Benefits:
Reduces template verbosity and maintains cleaner code
Automatically handles dynamic property sets
Eliminates the need for manual bindable declarations for pass-through properties
Individual Binding Benefits:
Slightly more efficient for small, known sets of properties
Explicit property access provides better type safety
Easier to debug specific binding issues
Memory and Observation Overhead
Understanding the observation behavior helps optimize performance:
Best Practice: Use spread syntax when you need to pass through a focused set of properties, not entire large objects.
Advanced Patterns
Complex Expression Patterns
Spread syntax supports complex expressions and transformations:
Combining Spread with Other Binding Features
Error Handling Patterns
Best Practices
When to Use Spread Syntax
✅ Good Use Cases:
Component composition and wrapper components
Dynamic forms with varying field sets
Passing through configuration objects
Creating reusable component libraries
❌ Avoid When:
You need explicit control over individual bindings
Working with large objects with many irrelevant properties
Performance is critical and you're binding a small, known set of properties
You need different binding modes for different properties
Maintainability Guidelines
Recommended Limits:
Maximum 2 levels of attribute transferring to avoid prop-drilling
Use spread for groups of related properties, not entire application state
Document spread behavior in component interfaces
Type Safety Considerations
Common Patterns and Anti-Patterns
✅ Recommended Patterns
❌ Anti-Patterns to Avoid
Debugging and Troubleshooting
Common Issues
HTML Case Sensitivity
Property Observation Not Working
Binding Mode Conflicts
Debugging Tips
Understanding these patterns and considerations will help you use Aurelia's spread syntax effectively while maintaining good performance and code maintainability.
Last updated
Was this helpful?