Attribute transferring

Forward bindings from a custom element to its inner template using Aurelia's spread operators.

Attribute transferring lets a custom element pass bindings it receives down to elements inside its own template. It keeps component APIs small while still exposing the flexibility callers need.

As an application grows, the components inside it also grow. Something that starts simple like the following component

export class FormInput {
  @bindable label
  @bindable value
}

with the template

<label>${label}
  <input value.bind="value">
</label>

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 that simply passes bindings through. The number of @bindable properties increases and maintenance becomes tedious:

export class FormInput {
  @bindable label
  @bindable value
  @bindable type
  @bindable tooltip
  @bindable arias
  @bindable etc
}

And the usage of such element may look like this

<form-input
  label.bind="label"
  value.bind="message"
  tooltip.bind="Did you know Aurelia syntax comes from an idea of an Angular community member? We greatly appreciate Angular and its community for this."
  validation.bind="...">

to be repeated like this inside:

<label>${label}
  <input value.bind tooltip.bind validation.bind min.bind max.bind>
</label>

Coordinating all of those bindings isn't difficult, just repetitive. Attribute transferring, conceptually similar to JavaScript spread syntax, reduces the template to:

<label>${label}
  <input ...$attrs>
</label>

This moves the bindings declared on <form-input> onto the <input> element inside the component.

Aurelia Spread Operators Overview

Aurelia provides several spread operators for different use cases:

Operator
Purpose
Example

...$attrs

Spread captured attributes from parent element

<input ...$attrs>

...$bindables

Spread object properties to bindable properties

<my-component ...$bindables="user">

...expression

Shorthand for bindable spreading

<my-component ...user>

Each operator serves a specific purpose in component composition and data flow.

Usage

To transfer attributes and bindings from a custom element, there are two steps:

  • Set capture to true on a custom element via @customElement decorator:

@customElement({
  ...,
  capture: true
})

This tells the template compiler to capture bindings and attributes (with some exceptions) for later reuse.

  • Spread the captured attributes onto an element:

<input ...$attrs>

How it works

What attributes are captured

Everything except template controller and custom element bindables are captured. For the following example:

View model:

export class FormInput {
  @bindable label
}

Usage:

<form-input if.bind="needsComment" label.bind="label" value.bind="extraComment" class="form-control" style="background: var(--theme-purple)" tooltip="Hello, ${tooltip}">

What are captured:

  • value.bind="extraComment"

  • class="form-control"

  • style="background: var(--theme-purple)"

  • tooltip="Hello, ${tooltip}" What are not captured:

  • if.bind="needsComment" (if is a template controller)

  • label.bind="label" (label is 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 is targeting a bindable property of the applied custom element. An example:

app.html
<input-field value.bind="message">

input-field.html
<my-input ...$attrs>

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. Binding mode is also preserved like normal attributes.

Advanced Spread Patterns

Mixed Binding Patterns

You can combine multiple spread operators and explicit bindings on the same element:

<!-- Spread bindables, then attributes, then explicit bindings -->
<input-field ...user ...$attrs id.bind="fieldId" class="form-control">

Binding Priority (last wins):

  1. ...$bindables / ...expression (first)

  2. ...$attrs (second)

  3. Explicit bindings (last, highest priority)

Note: According to the existing documentation, ...$attrs will always result in bindings after ...$bindables/$bindables.spread/...expression, regardless of their order in the template.

<!-- The explicit value.bind will override any value from spreading -->
<input ...$attrs value.bind="explicitValue">

Complex Member Access

Spread operators support complex expressions:

<!-- Deep property access -->
<user-card ...user.profile.details>
<user-card ...user.addresses[0]>

<!-- Method calls and computed properties -->
<user-card ...user.getDetails()>
<user-card ...user.details | processUser>

<!-- For complex expressions, use the full syntax -->
<user-card ...$bindables="user.addresses.find(addr => addr.primary)">

Conditional Spreading

You can conditionally spread attributes based on expressions:

<!-- Only spread if user exists -->
<user-card ...$bindables="user || {}">

<!-- Spread different objects based on condition -->
<user-card ...$bindables="isAdmin ? adminUser : regularUser">

<!-- Combine with template controllers -->
<user-card if.bind="user" ...user>

Automatic Expression Inference

Aurelia can automatically infer property names in certain binding scenarios:

Shorthand Binding Syntax

<!-- These are equivalent -->
<input value.bind="value">
<input value.bind>  <!-- Auto-infers 'value' property -->

<!-- Works with different binding commands -->
<input value.two-way="value">
<input value.two-way>  <!-- Auto-infers 'value' property -->

<!-- Attribute binding -->
<div textcontent.bind="textcontent">
<div textcontent.bind>  <!-- Auto-infers 'textcontent' property -->

<!-- Custom attributes -->
<div tooltip.bind="tooltip">
<div tooltip.bind>  <!-- Auto-infers 'tooltip' property -->

Inference Rules

  • Property name must match the attribute name exactly

  • Only works with simple property access (no expressions)

  • Works with all binding commands (.bind, .two-way, .one-way, etc.)

  • Case-sensitive: firstName.bind infers firstName, not firstname

Performance Considerations

Binding Creation Optimization

Spread operators include several performance optimizations:

// Aurelia optimizes repeated spread operations
class UserCard {
  @bindable user = { name: 'John', age: 30 };
  
  updateUser() {
    // If the same object reference is returned, bindings aren't recreated
    this.user = this.user; // No rebinding
    
    // New object reference triggers binding recreation
    this.user = { ...this.user, age: 31 }; // Rebinding occurs
  }
}

One-time Change Detection

Spread operations are optimized to prevent unnecessary binding updates:

<!-- Bindings are created once and reused when possible -->
<user-card ...user>

Memory Usage Guidelines

  • Spread operators create bindings for each property accessed

  • Large objects with many properties create many bindings

  • Consider using specific bindable properties for frequently changing data

  • Use spreading primarily for configuration and setup data

Error Handling & Edge Cases

Null and Undefined Handling

Spread operators handle null and undefined values gracefully:

<!-- Safe spreading - handles null/undefined gracefully -->
<user-card ...user>           <!-- Safe even if user is null/undefined -->
<user-card ...$bindables="user || {}">  <!-- Explicit fallback -->

<!-- Member access on null/undefined -->
<user-card ...user?.profile>  <!-- Safe with optional chaining -->

Invalid Expressions

<!-- These will be handled gracefully -->
<user-card ...undefined>      <!-- No bindings created -->
<user-card ...nonExistentVar> <!-- No bindings created -->
<user-card ...user.invalid>   <!-- No bindings created -->

Type Safety with TypeScript

TypeScript provides compile-time validation for spread operations:

interface User {
  name: string;
  email: string;
  age: number;
}

export class UserCard {
  @bindable name: string;
  @bindable email: string;
  // age is not a bindable, so it won't be bound even if present in the object
}

const user: User = { name: 'John', email: '[email protected]', age: 30 };
<!-- Only name and email will be bound based on component's @bindable properties -->
<user-card ...user>

Advanced Capture Patterns

Capture Filtering

Filter which attributes are captured using a function:

@customElement({
  name: 'secure-input',
  template: '<input ...$attrs>',
  capture: attr => !attr.startsWith('on') // Exclude event handlers
})
export class SecureInput {
  @bindable value: string;
}
@customElement({
  name: 'styled-input',
  template: '<input ...$attrs>',
  capture: attr => ['class', 'style', 'disabled'].includes(attr) // Only style-related
})
export class StyledInput {
  @bindable value: string;
}

Multi-level Capture Guidelines

<!-- Level 1: App uses form-group -->
<form-group title="User Info" ...validation>
  <!-- Level 2: form-group uses input-field -->
  <input-field label="Email" ...validation>
    <!-- Level 3: input-field uses input -->
    <input ...$attrs>
  </input-field>
</form-group>

Template Controller Compatibility

Spread operators work with template controllers:

<!-- Template controllers are not captured -->
<input-field if.bind="showField" ...fieldProps>

<!-- Multiple template controllers -->
<input-field if.bind="showField" repeat.for="field of fields" ...field>

Integration Examples

Component Composition

// Base input component
export class BaseInput {
  @bindable value: string;
  @bindable placeholder: string;
  @bindable disabled: boolean;
}

// Specialized email input
@customElement({
  name: 'email-input',
  template: '<base-input type="email" ...$attrs>',
  capture: true
})
export class EmailInput {}

// Form field wrapper
@customElement({
  name: 'form-field',
  template: `
    <div class="form-field">
      <label if.bind="label">\${label}</label>
      <div class="input-wrapper">
        <div class="content-replaceable" replaceable part="input">
          <input ...$attrs>
        </div>
      </div>
      <div class="error" if.bind="error">\${error}</div>
    </div>
  `,
  capture: true
})
export class FormField {
  @bindable label: string;
  @bindable error: string;
}

Usage:

<!-- Complex composition -->
<form-field label="Email Address" error.bind="emailError">
  <email-input au-slot="input" value.bind="email" placeholder="Enter email">
</form-field>

Working with Third-party Components

// Wrapper for third-party component
@customElement({
  name: 'material-input',
  template: '<mat-input ...$attrs>',
  capture: attr => !attr.startsWith('au-') // Exclude Aurelia-specific attributes
})
export class MaterialInput {
  @bindable value: string;
}

Dynamic Component Creation

export class DynamicForm {
  @bindable fieldConfigs: FieldConfig[];
  
  createField(config: FieldConfig) {
    return {
      component: config.component,
      props: config.props
    };
  }
}
<div repeat.for="config of fieldConfigs">
  <compose 
    view-model.bind="config.component"
    ...$bindables="config.props">
  </compose>
</div>

Common Patterns and Best Practices

1. Configuration Objects

// Good: Use spreading for configuration
interface ButtonConfig {
  variant: 'primary' | 'secondary';
  size: 'small' | 'medium' | 'large';
  icon?: string;
}

const submitConfig: ButtonConfig = {
  variant: 'primary',
  size: 'medium',
  icon: 'save'
};
<custom-button ...submitConfig>Submit</custom-button>

2. Conditional Properties

// Good: Build objects conditionally
const inputProps = {
  value: userInput,
  ...(isRequired && { required: true }),
  ...(hasError && { 'aria-invalid': true }),
  ...(isDisabled && { disabled: true })
};
<input ...$bindables="inputProps">

3. Proxy Objects for Transformation

// Good: Transform data before spreading
const transformedUser = {
  displayName: user.fullName,
  email: user.contactInfo.email,
  isActive: user.status === 'active'
};
<user-card ...transformedUser>

4. Default Values with Spreading

// Good: Provide defaults
const defaultFieldProps = {
  size: 'medium',
  variant: 'outline'
};

const fieldProps = {
  ...defaultFieldProps,
  ...customProps
};
<form-field ...$bindables="fieldProps">

This comprehensive documentation now covers all the advanced patterns and edge cases developers might encounter when working with Aurelia's spread operators.

Last updated

Was this helpful?