LogoLogo
HomeDiscourseBlogDiscord
  • Introduction
  • Introduction
    • Quick start
    • Aurelia for new developers
    • Hello world
      • Creating your first app
      • Your first component - part 1: the view model
      • Your first component - part 2: the view
      • Running our app
      • Next steps
  • Templates
    • Template Syntax
      • Attribute binding
      • Event binding
      • Text interpolation
      • Template promises
      • Template references
      • Template variables
      • Globals
    • Custom attributes
    • Value converters (pipes)
    • Binding behaviors
    • Form Inputs
    • CSS classes and styling
    • Conditional Rendering
    • List Rendering
    • Lambda Expressions
    • Local templates (inline templates)
    • SVG
  • Components
    • Component basics
    • Component lifecycles
    • Bindable properties
    • Styling components
    • Slotted content
    • Scope and context
    • CustomElement API
    • Template compilation
      • processContent
      • Extending templating syntax
      • Modifying template parsing with AttributePattern
      • Extending binding language
      • Using the template compiler
      • Attribute mapping
  • Getting to know Aurelia
    • Routing
      • @aurelia/router
        • Getting Started
        • Creating Routes
        • Routing Lifecycle
        • Viewports
        • Navigating
        • Route hooks
        • Router animation
        • Route Events
        • Router Tutorial
        • Router Recipes
      • @aurelia/router-lite
        • Getting started
        • Router configuration
        • Configuring routes
        • Viewports
        • Navigating
        • Lifecycle hooks
        • Router hooks
        • Router events
        • Navigation model
        • Current route
        • Transition plan
    • App configuration and startup
    • Enhance
    • Template controllers
    • Understanding synchronous binding
    • Dynamic composition
    • Portalling elements
    • Observation
      • Observing property changes with @observable
      • Effect observation
      • HTML observation
      • Using observerLocator
    • Watching data
    • Dependency injection (DI)
    • App Tasks
    • Task Queue
    • Event Aggregator
  • Developer Guides
    • Animation
    • Testing
      • Overview
      • Testing attributes
      • Testing components
      • Testing value converters
      • Working with the fluent API
      • Stubs, mocks & spies
    • Logging
    • Building plugins
    • Web Components
    • UI virtualization
    • Errors
      • Kernel Errors
      • Template Compiler Errors
      • Dialog Errors
      • Runtime HTML Errors
    • Bundlers
    • Recipes
      • Apollo GraphQL integration
      • Auth0 integration
      • Containerizing Aurelia apps with Docker
      • Cordova/Phonegap integration
      • CSS-in-JS with Emotion
      • DOM style injection
      • Firebase integration
      • Markdown integration
      • Multi root
      • Progress Web Apps (PWA's)
      • Securing an app
      • SignalR integration
      • Strongly-typed templates
      • TailwindCSS integration
      • WebSockets Integration
      • Web Workers Integration
    • Playground
      • Binding & Templating
      • Custom Attributes
        • Binding to Element Size
      • Integration
        • Microsoft FAST
        • Ionic
    • Migrating to Aurelia 2
      • For plugin authors
      • Side-by-side comparison
    • Cheat Sheet
  • Aurelia Packages
    • Validation
      • Validation Tutorial
      • Plugin Configuration
      • Defining & Customizing Rules
      • Architecture
      • Tagging Rules
      • Model Based Validation
      • Validation Controller
      • Validate Binding Behavior
      • Displaying Errors
      • I18n Internationalization
      • Migration Guide & Breaking Changes
    • i18n Internationalization
    • Fetch Client
      • Overview
      • Setup and Configuration
      • Response types
      • Working with forms
      • Intercepting responses & requests
      • Advanced
    • Event Aggregator
    • State
    • Store
      • Configuration and Setup
      • Middleware
    • Dialog
  • Tutorials
    • Building a ChatGPT inspired app
    • Building a realtime cryptocurrency price tracker
    • Building a todo application
    • Building a weather application
    • Building a widget-based dashboard
    • React inside Aurelia
    • Svelte inside Aurelia
    • Synthetic view
    • Vue inside Aurelia
  • Community Contribution
    • Joining the community
    • Code of conduct
    • Contributor guide
    • Building and testing aurelia
    • Writing documentation
    • Translating documentation
Powered by GitBook
On this page
  • Table of Contents
  • Introduction
  • Creating a Basic Custom Attribute
  • Example: Red Square Attribute
  • Custom Attribute Definition Approaches
  • Convention-Based Approach
  • Decorator-Based Approach (Recommended)
  • Static Definition Approach (Framework Internal)
  • Explicit Custom Attributes
  • Explicit Attribute Naming
  • Attribute Aliases
  • Single Value Binding
  • Bindable Properties and Change Detection
  • Binding Modes
  • Primary Bindable Property
  • Bindable Interceptors
  • Custom Change Callbacks
  • Options Binding for Multiple Properties
  • Advanced Bindable Configuration
  • Lifecycle Hooks
  • Example: Using Lifecycle Hooks
  • Aggregated Change Callbacks
  • Accessing the Host Element
  • Finding Related Custom Attributes
  • Example: Searching by Attribute Name
  • Example: Searching by Constructor
  • Template Controller Custom Attributes
  • Creating a Template Controller
  • Advanced Configuration Options
  • No Multi-Bindings
  • Dependencies
  • Container Strategy (Template Controllers Only)
  • Default Binding Mode
  • Watch Integration
  • Integrating Third-Party Libraries
  • When to Use Custom Attributes for Integration
  • Example: Integrating a Hypothetical Slider Library
  • Best Practices
  • Separation of Concerns
  • Performance
  • Memory Management
  • Testing
  • Documentation
  • Type Safety

Was this helpful?

Export as PDF
  1. Templates

Custom attributes

Learn how to build and enhance Aurelia 2 custom attributes, including advanced configuration, binding strategies, and accessing the host element.

Custom attributes in Aurelia empower you to extend and decorate standard HTML elements by embedding custom behavior and presentation logic. They allow you to wrap or integrate existing HTML plugins and libraries, or simply enhance your UI components with additional dynamic functionality. This guide provides a comprehensive overview—from basic usage to advanced techniques—to help you leverage custom attributes effectively in your Aurelia 2 projects.


Table of Contents

  1. Introduction

  2. Creating a Basic Custom Attribute

  3. Custom Attribute Definition Approaches

    • Convention-Based Approach

    • Decorator-Based Approach

    • Static Definition Approach

  4. Explicit Custom Attributes

    • Explicit Attribute Naming

    • Attribute Aliases

  5. Single Value Binding

  6. Bindable Properties and Change Detection

    • Binding Modes

    • Primary Bindable Property

    • Bindable Interceptors

    • Custom Change Callbacks

  7. Options Binding for Multiple Properties

  8. Advanced Bindable Configuration

  9. Lifecycle Hooks

  10. Aggregated Change Callbacks

  11. Accessing the Host Element

  12. Finding Related Custom Attributes

  13. Template Controller Custom Attributes

  14. Advanced Configuration Options

  15. Watch Integration

  16. Integrating Third-Party Libraries

  17. Best Practices


Introduction

Custom attributes are one of the core building blocks in Aurelia 2. Similar to components, they encapsulate behavior and style, but are applied as attributes to existing DOM elements. This makes them especially useful for:

  • Decorating elements with additional styling or behavior.

  • Wrapping third-party libraries that expect to control their own DOM structure.

  • Creating reusable logic that enhances multiple elements across your application.

  • Creating template controllers that control the rendering of content.


Creating a Basic Custom Attribute

At its simplest, a custom attribute is defined as a class that enhances an element. Consider this minimal example:

export class CustomPropertyCustomAttribute {
  // Custom logic can be added here
}

When you apply a similar pattern using CustomElement instead, you are defining a component. Custom attributes are a more primitive (yet powerful) way to extend behavior without wrapping the entire element in a component.

Example: Red Square Attribute

This custom attribute adds a fixed size and a red background to any element it is applied to:

import { INode, resolve } from 'aurelia';

export class RedSquareCustomAttribute {
  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    // Set fixed dimensions and a red background on initialization
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}

Usage in HTML:

<import from="./red-square"></import>

<div red-square></div>

The <import> tag ensures that Aurelia's dependency injection is aware of your custom attribute. When applied, the <div> will render with the specified styles.


Custom Attribute Definition Approaches

Aurelia 2 provides multiple approaches for defining custom attributes. For most user scenarios, you'll use either the convention-based or decorator-based approach:

Convention-Based Approach

Classes ending with CustomAttribute are automatically recognized as custom attributes:

import { INode, resolve } from 'aurelia';

export class RedSquareCustomAttribute {
  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}

The attribute name is derived from the class name (red-square in this case).

Decorator-Based Approach (Recommended)

Use the @customAttribute decorator for explicit control and better IDE support:

import { customAttribute, INode, resolve } from 'aurelia';

@customAttribute({ name: 'red-square' })
export class RedSquare {
  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}

Static Definition Approach (Framework Internal)

For completeness, the framework also supports defining attributes using a static $au property. This approach is primarily used by the framework itself to avoid conventions and decorators, but is available if needed:

import { INode, resolve, type CustomAttributeStaticAuDefinition } from 'aurelia';

export class RedSquare {
  public static readonly $au: CustomAttributeStaticAuDefinition = {
    type: 'custom-attribute',
    name: 'red-square'
  };

  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}

When to use each approach:

  • Convention-based: Quick prototyping, simple attributes where the class name matches desired attribute name

  • Decorator-based: Production code, when you need explicit control over naming, aliases, or other configuration

  • Static definition: Advanced scenarios, framework extensions, or when you need to avoid decorators for tooling reasons


Explicit Custom Attributes

To gain finer control over your attribute's name and configuration, Aurelia provides the @customAttribute decorator. This lets you explicitly define the attribute name and even set up aliases.

Explicit Attribute Naming

By default, the class name might be used to infer the attribute name. However, you can explicitly set a custom name:

import { customAttribute, INode, resolve } from 'aurelia';

@customAttribute({ name: 'red-square' })
export class RedSquare {
  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}

Attribute Aliases

You can define one or more aliases for your custom attribute. This allows consumers of your attribute flexibility in naming:

import { customAttribute, INode, resolve } from 'aurelia';

@customAttribute({ name: 'red-square', aliases: ['redify', 'redbox'] })
export class RedSquare {
  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}

Now the attribute can be used interchangeably using any of the registered names:

<div red-square></div>
<div redify></div>
<div redbox></div>

Single Value Binding

For simple cases, you might want to pass a single value to your custom attribute without explicitly declaring a bindable property. Aurelia will automatically populate the value property if a value is provided.

import { INode, resolve } from 'aurelia';

export class RedSquareCustomAttribute {
  private element: HTMLElement = resolve(INode) as HTMLElement;
  private value: string;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    // Use a default color, but override it if a value is supplied during binding.
    this.element.style.backgroundColor = 'red';
  }

  bind() {
    if (this.value) {
      this.element.style.backgroundColor = this.value;
    }
  }
}

To further handle changes in the value over time, you can define the property as bindable:

import { bindable, INode, resolve } from 'aurelia';

export class RedSquareCustomAttribute {
  private element: HTMLElement = resolve(INode) as HTMLElement;

  @bindable() private value: string;

  constructor() {
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }

  bound() {
    this.element.style.backgroundColor = this.value;
  }

  valueChanged(newValue: string, oldValue: string) {
    this.element.style.backgroundColor = newValue;
  }
}

Bindable Properties and Change Detection

Custom attributes often need to be configurable. Using the @bindable decorator, you can allow users to pass in parameters that change the behavior or style dynamically.

Binding Modes

Bindable properties support different binding modes that determine how data flows:

import { bindable, INode, resolve, BindingMode } from 'aurelia';

export class InputWrapperCustomAttribute {
  @bindable({ mode: BindingMode.twoWay }) value: string = '';
  @bindable({ mode: BindingMode.toView }) placeholder: string = '';
  @bindable({ mode: BindingMode.fromView }) isValid: boolean = true;
  @bindable({ mode: BindingMode.oneTime }) label: string = '';

  private element: HTMLElement = resolve(INode) as HTMLElement;

  // ... implementation
}

Available binding modes:

  • BindingMode.toView (default): Data flows from view model to view

  • BindingMode.fromView: Data flows from view to view model

  • BindingMode.twoWay: Data flows both ways

  • BindingMode.oneTime: Data is set once and never updated

Primary Bindable Property

You can mark one property as primary, allowing simpler binding syntax:

import { bindable, INode, resolve } from 'aurelia';

export class ColorSquareCustomAttribute {
  @bindable({ primary: true }) color: string = 'red';
  @bindable() size: string = '100px';

  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    this.applyStyles();
  }

  bound() {
    this.applyStyles();
  }

  colorChanged(newColor: string) {
    this.element.style.backgroundColor = newColor;
  }

  sizeChanged(newSize: string) {
    this.element.style.width = this.element.style.height = newSize;
  }

  private applyStyles() {
    this.element.style.width = this.element.style.height = this.size;
    this.element.style.backgroundColor = this.color;
  }
}

With a primary property defined, you can bind directly:

<import from="./color-square"></import>

<!-- Using a literal value -->
<div color-square="blue"></div>

<!-- Or binding the value dynamically -->
<div color-square.bind="myColour"></div>

Bindable Interceptors

You can intercept and transform values being set on bindable properties:

import { bindable, INode, resolve } from 'aurelia';

export class ValidatedInputCustomAttribute {
  @bindable({
    set: (value: string) => value?.trim().toLowerCase()
  }) email: string = '';

  @bindable({
    set: (value: number) => Math.max(0, Math.min(100, value))
  }) progress: number = 0;

  private element: HTMLElement = resolve(INode) as HTMLElement;
}

Custom Change Callbacks

You can specify custom callback names for change handlers:

import { bindable } from 'aurelia';

export class DataVisualizationCustomAttribute {
  @bindable({ callback: 'onDataUpdate' }) dataset: any[] = [];
  @bindable({ callback: 'onConfigChange' }) config: any = {};

  onDataUpdate(newData: any[], oldData: any[]) {
    // Handle data changes
    this.redrawChart();
  }

  onConfigChange(newConfig: any, oldConfig: any) {
    // Handle configuration changes
    this.updateChartSettings();
  }
}

Options Binding for Multiple Properties

When you have more than one bindable property, you can use options binding syntax to bind multiple properties at once. Each bindable property in the view model corresponds to a dash-case attribute in the DOM. For instance:

<import from="./color-square"></import>

<div color-square="color.bind: myColor; size.bind: mySize;"></div>

The Aurelia binding engine converts the attribute names (e.g., color-square) to the corresponding properties in your class.


Advanced Bindable Configuration

You can also define bindables in the static definition or decorator:

import { customAttribute, INode, resolve, BindingMode } from 'aurelia';

@customAttribute({
  name: 'advanced-input',
  bindables: {
    value: { mode: BindingMode.twoWay, primary: true },
    placeholder: { mode: BindingMode.toView },
    validation: { callback: 'validateInput' }
  }
})
export class AdvancedInputCustomAttribute {
  value: string;
  placeholder: string;
  validation: any;

  private element: HTMLElement = resolve(INode) as HTMLElement;

  validateInput(newValidation: any, oldValidation: any) {
    // Handle validation changes
  }
}

Or using the static $au approach:

import { INode, resolve, BindingMode, type CustomAttributeStaticAuDefinition } from 'aurelia';

export class AdvancedInput {
  public static readonly $au: CustomAttributeStaticAuDefinition = {
    type: 'custom-attribute',
    name: 'advanced-input',
    bindables: {
      value: { mode: BindingMode.twoWay, primary: true },
      placeholder: { mode: BindingMode.toView },
      validation: { callback: 'validateInput' }
    }
  };

  value: string;
  placeholder: string;
  validation: any;

  private element: HTMLElement = resolve(INode) as HTMLElement;

  validateInput(newValidation: any, oldValidation: any) {
    // Handle validation changes
  }
}

Lifecycle Hooks

Custom attributes support a comprehensive set of lifecycle hooks that allow you to run code at different stages of their existence:

  • created(controller): Called after the attribute instance is created

  • binding(initiator, parent): Called before data binding begins

  • bind(): Called when data binding begins (simplified version)

  • bound(initiator, parent): Called after data binding is complete

  • attaching(initiator, parent): Called before the element is attached to the DOM

  • attached(initiator): Called after the element is attached to the DOM

  • detaching(initiator, parent): Called before the element is detached from the DOM

  • unbinding(initiator, parent): Called before data binding is removed

  • unbind(): Called when data binding is removed (simplified version)

Example: Using Lifecycle Hooks

import { bindable, INode, resolve, customAttribute, ICustomAttributeController, IHydratedController } from 'aurelia';

@customAttribute({ name: 'lifecycle-demo' })
export class LifecycleDemoCustomAttribute {
  @bindable() value: string = '';

  private element: HTMLElement = resolve(INode) as HTMLElement;

  created(controller: ICustomAttributeController) {
    // Called when the attribute instance is created
    console.log('Custom attribute created');
  }

  binding(initiator: IHydratedController, parent: IHydratedController) {
    // Called before binding begins - good for setup
    console.log('Starting to bind');
  }

  bind() {
    // Simplified binding hook - most commonly used
    this.applyInitialValue();
  }

  bound(initiator: IHydratedController, parent: IHydratedController) {
    // Called after binding is complete
    console.log('Binding complete');
  }

  attaching(initiator: IHydratedController, parent: IHydratedController) {
    // Called before DOM attachment
    console.log('About to attach to DOM');
  }

  attached(initiator: IHydratedController) {
    // Called after DOM attachment - good for DOM manipulation
    this.initializeThirdPartyLibrary();
  }

  valueChanged(newValue: string, oldValue: string) {
    // Called whenever the value changes
    this.updateDisplay();
  }

  detaching(initiator: IHydratedController, parent: IHydratedController) {
    // Called before DOM detachment - good for cleanup
    this.cleanupEventListeners();
  }

  unbinding(initiator: IHydratedController, parent: IHydratedController) {
    // Called before unbinding
    console.log('About to unbind');
  }

  unbind() {
    // Simplified unbinding hook - good for final cleanup
    this.finalCleanup();
  }

  private applyInitialValue() {
    this.element.textContent = this.value;
  }

  private updateDisplay() {
    this.element.textContent = this.value;
  }

  private initializeThirdPartyLibrary() {
    // Initialize any third-party libraries that need DOM access
  }

  private cleanupEventListeners() {
    // Remove event listeners to prevent memory leaks
  }

  private finalCleanup() {
    // Final cleanup before the attribute is destroyed
  }
}

Aggregated Change Callbacks

Custom attributes can implement aggregated change detection that batches multiple property changes:

import { bindable, customAttribute } from 'aurelia';

@customAttribute('batch-processor')
export class BatchProcessorCustomAttribute {
  @bindable() prop1: string;
  @bindable() prop2: number;
  @bindable() prop3: boolean;

  // Called when any bindable property changes (batched until next microtask)
  propertiesChanged(changes: Record<string, { newValue: unknown; oldValue: unknown }>) {
    console.log('Properties changed:', changes);
    // Example output: { prop1: { newValue: 'new', oldValue: 'old' } }

    // Process all changes at once for better performance
    this.processBatchedChanges(changes);
  }

  // Called for every property change (immediate)
  propertyChanged(key: PropertyKey, newValue: unknown, oldValue: unknown) {
    console.log(`Property ${String(key)} changed from ${oldValue} to ${newValue}`);
  }

  private processBatchedChanges(changes: Record<string, any>) {
    // Efficiently handle multiple property changes
  }
}

Accessing the Host Element

A key aspect of custom attributes is that they work directly on DOM elements. To manipulate these elements (e.g., updating styles or initializing plugins), you need to access the host element. Aurelia provides a safe way to do this using dependency injection with INode.

import { INode, resolve } from 'aurelia';

export class RedSquareCustomAttribute {
  // Resolve the host element safely, even in Node.js environments
  private element: HTMLElement = resolve(INode) as HTMLElement;

  constructor() {
    // Now you can modify the host element directly
    this.element.style.width = this.element.style.height = '100px';
    this.element.style.backgroundColor = 'red';
  }
}

Note: While you can also use resolve(Element) or resolve(HTMLElement), using INode is safer in environments where global DOM constructors might not be available (such as Node.js).


Finding Related Custom Attributes

In complex UIs, you might have multiple custom attributes working together (for example, a dropdown with associated toggle buttons). Aurelia offers the CustomAttribute.closest function to traverse the DOM and locate a related custom attribute. This function can search by attribute name or by constructor.

Example: Searching by Attribute Name

<div foo="1">
  <center>
    <div foo="3">
      <div bar="2"></div>
    </div>
  </center>
</div>
import { CustomAttribute, resolve, INode, customAttribute } from 'aurelia';

@customAttribute('bar')
export class Bar {
  host: HTMLElement = resolve(INode) as HTMLElement;

  binding() {
    // Find the closest ancestor that has the 'foo' custom attribute
    const closestFoo = CustomAttribute.closest(this.host, 'foo');
    if (closestFoo) {
      console.log('Found foo attribute:', closestFoo.viewModel);
    }
  }
}

Example: Searching by Constructor

If you want to search based on the attribute's constructor (for stronger typing), you can do so:

import { CustomAttribute, resolve, INode, customAttribute } from 'aurelia';
import { Foo } from './foo';

@customAttribute('bar')
export class Bar {
  host: HTMLElement = resolve(INode) as HTMLElement;

  binding() {
    // Find the closest ancestor that is an instance of the Foo custom attribute
    const parentFoo = CustomAttribute.closest(this.host, Foo);
    if (parentFoo) {
      // parentFoo.viewModel is now strongly typed as Foo
      parentFoo.viewModel.someMethod();
    }
  }
}

Template Controller Custom Attributes

Custom attributes can also function as template controllers, which control the rendering of content. Template controllers are similar to built-in directives like if.bind and repeat.for.

Creating a Template Controller

import { templateController, IViewFactory, ISyntheticView, IRenderLocation, resolve, bindable, ICustomAttributeController } from 'aurelia';

@templateController('permission')
export class PermissionTemplateController {
  @bindable() userRole: string;
  @bindable() requiredRole: string;

  public readonly $controller!: ICustomAttributeController<this>;

  private view: ISyntheticView;
  private readonly factory = resolve(IViewFactory);
  private readonly location = resolve(IRenderLocation);

  bound() {
    this.updateView();
  }

  userRoleChanged() {
    if (this.$controller.isActive) {
      this.updateView();
    }
  }

  requiredRoleChanged() {
    if (this.$controller.isActive) {
      this.updateView();
    }
  }

  private updateView() {
    const hasPermission = this.userRole === this.requiredRole;

    if (hasPermission) {
      if (!this.view) {
        this.view = this.factory.create().setLocation(this.location);
      }
      if (!this.view.isActive) {
        this.view.activate(this.view, this.$controller, this.$controller.scope);
      }
    } else {
      if (this.view?.isActive) {
        this.view.deactivate(this.view, this.$controller);
      }
    }
  }

  unbind() {
    if (this.view?.isActive) {
      this.view.deactivate(this.view, this.$controller);
    }
  }
}

Usage:

<div permission="user-role.bind: currentUser.role; required-role: admin">
  <h2>Admin Panel</h2>
  <p>Only admins can see this content</p>
</div>

You can also use the static definition approach:

import { IViewFactory, ISyntheticView, IRenderLocation, resolve, type CustomAttributeStaticAuDefinition } from 'aurelia';

export class PermissionTemplateController {
  public static readonly $au: CustomAttributeStaticAuDefinition = {
    type: 'custom-attribute',
    name: 'permission',
    isTemplateController: true,
    bindables: ['userRole', 'requiredRole']
  };

  // ... implementation same as above
}

Advanced Configuration Options

Custom attributes support several advanced configuration options:

No Multi-Bindings

By default, custom attributes support multiple bindings (attr="prop1: value1; prop2: value2"). You can disable this:

import { customAttribute } from 'aurelia';

@customAttribute({
  name: 'simple-url',
  noMultiBindings: true
})
export class SimpleUrlCustomAttribute {
  value: string; // Will receive the entire attribute value as a string
}
<!-- With noMultiBindings: true, this won't be parsed as bindings -->
<a simple-url="https://example.com:8080/path">Link</a>

Dependencies

You can specify dependencies that should be registered when the attribute is used:

import { customAttribute } from 'aurelia';
import { SomeService } from './some-service';

@customAttribute({
  name: 'dependent-attr',
  dependencies: [SomeService]
})
export class DependentAttributeCustomAttribute {
  // SomeService will be registered when this attribute is used
}

Container Strategy (Template Controllers Only)

For template controller custom attributes, you can specify the container strategy:

import { templateController } from 'aurelia';

@templateController({
  name: 'isolated-scope',
  containerStrategy: 'new' // Creates a new container for the view factory
})
export class IsolatedScopeTemplateController {
  // Views created by this template controller will have their own container
}

Default Binding Mode

You can set a default binding mode for all bindable properties:

import { customAttribute, BindingMode } from 'aurelia';

@customAttribute({
  name: 'two-way-default',
  defaultBindingMode: BindingMode.twoWay
})
export class TwoWayDefaultCustomAttribute {
  @bindable() value1: string; // Will default to two-way binding
  @bindable() value2: string; // Will default to two-way binding
  @bindable({ mode: BindingMode.toView }) value3: string; // Explicitly one-way
}

Watch Integration

Custom attributes can integrate with Aurelia's @watch decorator for advanced property observation:

import { bindable, customAttribute, watch } from 'aurelia';

@customAttribute('data-processor')
export class DataProcessorCustomAttribute {
  @bindable() data: any[];
  @bindable() config: any;

  @watch('data', { immediate: true })
  @watch('config')
  onDataOrConfigChange(newValue: any, oldValue: any, propertyName: string) {
    console.log(`${propertyName} changed from`, oldValue, 'to', newValue);
    this.reprocessData();
  }

  private reprocessData() {
    // Process data based on current data and config
  }
}

Integrating Third-Party Libraries

Often, you'll want to incorporate functionality from third-party libraries—such as sliders, date pickers, or custom UI components—into your Aurelia applications. Custom attributes provide an excellent way to encapsulate the integration logic, ensuring that the third-party library initializes, updates, and cleans up properly within Aurelia's lifecycle.

When to Use Custom Attributes for Integration

  • DOM Manipulation: Many libraries require direct access to the DOM element for initialization.

  • Lifecycle Management: You can leverage Aurelia's lifecycle hooks (attached() and detached()) to manage resource allocation and cleanup.

  • Dynamic Updates: With bindable properties, you can pass configuration options to the library and update it reactively when those options change.

Example: Integrating a Hypothetical Slider Library

Consider a third-party slider library called AwesomeSlider that initializes a slider on a given DOM element. Below is an example of how to wrap it in a custom attribute.

import { customAttribute, bindable, INode, resolve, ILogger } from 'aurelia';
// Import the third-party slider library (this is a hypothetical example)
import AwesomeSlider from 'awesome-slider';

@customAttribute('awesome-slider')
export class AwesomeSliderCustomAttribute {
  // Allow dynamic options to be bound from the view
  @bindable() options: any = {};

  // The instance of the third-party slider
  private sliderInstance: any;

  // Safely resolve the host element
  private element: HTMLElement = resolve(INode) as HTMLElement;
  private logger = resolve(ILogger);

  attached() {
    // Initialize the slider when the element is attached to the DOM.
    // This ensures that the DOM is ready for manipulation.
    try {
      this.sliderInstance = new AwesomeSlider(this.element, this.options);
    } catch (error) {
      this.logger.error('Failed to initialize AwesomeSlider:', error);
    }
  }

  optionsChanged(newOptions: any, oldOptions: any) {
    // Update the slider if its configuration changes at runtime.
    // This callback is triggered when the bound `options` property changes.
    if (this.sliderInstance && typeof this.sliderInstance.updateOptions === 'function') {
      this.sliderInstance.updateOptions(newOptions);
    }
  }

  detached() {
    // Clean up the slider instance when the element is removed from the DOM.
    // This prevents memory leaks and removes event listeners.
    if (this.sliderInstance && typeof this.sliderInstance.destroy === 'function') {
      this.sliderInstance.destroy();
      this.sliderInstance = null;
    }
  }
}

In place of our hypothetical AwesomeSlider library, you can use any third-party library that requires DOM manipulation such as jQuery plugins, D3.js, or even custom UI components.


Best Practices

Separation of Concerns

Keep your custom attribute logic focused on enhancing the host element, and avoid heavy business logic.

Performance

  • Minimize DOM manipulations inside change handlers

  • If multiple properties change at once, consider batching style updates using propertiesChanged

  • Use lifecycle hooks appropriately - prefer attached() for DOM-dependent initialization

Memory Management

  • Always clean up event listeners in detached() or unbind()

  • Dispose of third-party library instances properly

  • Remove references to prevent memory leaks

Testing

Write unit tests for your custom attributes to ensure that lifecycle hooks and bindings work as expected.

Documentation

Comment your code and document the expected behavior of your custom attributes, especially if you provide aliases or multiple bindable properties.

Type Safety

  • Use TypeScript interfaces for complex bindable properties

  • Provide proper typing for change callbacks

  • Use generic constraints where appropriate

interface SliderOptions {
  min: number;
  max: number;
  step: number;
}

@customAttribute('typed-slider')
export class TypedSliderCustomAttribute {
  @bindable() options: SliderOptions = { min: 0, max: 100, step: 1 };
  @bindable() value: number = 0;

  optionsChanged(newOptions: SliderOptions, oldOptions: SliderOptions) {
    // Type-safe change handling
  }
}
PreviousGlobalsNextValue converters (pipes)

Last updated 4 days ago

Was this helpful?