Custom attributes

Using built-in custom attributes and building your own.

A custom attribute allows you to create special properties to enhance and decorate existing HTML elements and components. Natively attributes exist in the form of things such as disabled on form inputs or aria text labels. Where custom attributes can be especially useful is wrapping existing HTML plugins that generate their own markup.

Creating custom attributes

On a simplistic level, custom attributes resemble components quite a lot. They can have bindable properties, and they use classes for their definitions.

A basic custom attribute looks something like this:

export class CustomPropertyCustomAttribute {
}

If you were to replace CustomAttribute with CustomElement, it would be a component. On a core level, custom attributes are a more primitive component form.

Let's create a custom attribute that adds a red background and height to any dom element it is used on:

  import { INode, resolve } from 'aurelia';
  
  export class RedSquareCustomAttribute {
    private element = resolve(INode) as HTMLElement;
    constructor(){
        this.element.style.width = this.element.style.height = '100px';
        this.element.style.backgroundColor = 'red';
    }
  }

Now, let's use our custom attribute:

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

<div red-square></div>

We import our custom attribute so the DI knows about it and then use it on an empty DIV. We'll have a red background element with a height of one hundred pixels.

Explicit custom attributes

The customAttribute decorator allows you to create custom attributes, including the name explicitly. Other configuration options include the ability to create aliases.

Explicit attribute naming

You can explicitly name the custom attribute using the name configuration property.

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

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

Attribute aliases

The customAttribute allows you to create one or more aliases that this attribute can go by.

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

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

We can now use our custom attribute using the registered name red-square as well as redify and redbox the following example highlights using both aliases on an element.

<div redify></div>

<div redbox></div>

Single value binding

Sometimes, you want a custom attribute with only one bindable property. You don't need to define the bindable property explicitly to do this, as Aurelia supports custom attributes with single-value bindings.

  import { INode, resolve } from 'aurelia';

  export class RedSquareCustomAttribute {
    private element = resolve(INode) as HTMLElement;
    private value;
    
    constructor() {
        this.element.style.width = this.element.style.height = '100px';
        this.element.style.backgroundColor = 'red';
    }
    
    bind() {
        this.element.style.backgroundColor = this.value;
    }
  }

The value property is automatically populated if a value is supplied to a custom attribute, however, requires you to define the value property as a bindable explicitly.

When the value is changed, we can access it like this:

  import { bindable, INode, resolve } from 'aurelia';
  
  export class RedSquareCustomAttribute {
    private element = resolve(INode) as HTMLElement;

    @bindable() private value;
    
    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;
    }
  }

Accessing the element

When using the custom attribute on a dom element, there are instances where you want to be able to access the element itself. To do this, you can use the INode decorator and HTMLElement interface to inject and target the element.

  import { INode } from 'aurelia';
  
  export class RedSquareCustomAttribute {
    private element = resolve(INode) as HTMLElement;
  }

The code above was lifted from the first example, allowing us to access the element itself on the class using this.element This is how we can set CSS values and perform other modifications, such as initialising third-party libraries like jQuery and other libraries.

You can also use resolve(Element) or resolve(HTMLElement) to get the host element, just like INode. However, using INode makes it safer when used in Nodejs environment since there will not be a global Element or HTMLElement. If you are not planning to run your application in Nodejs environment, you can also just use Element or HTMLElement, like this: resolve(Element) or resolve(HTMLElement).

Custom attributes with bindable properties

In many cases, you might only need custom attributes without user-configurable properties. However, in some cases, you want the user to be able to pass in one or more properties to change the behavior of the custom attribute (like a plugin).

Using bindable properties, you can create a configurable custom attribute. Taking our example from above, let's make the background color configurable instead of always red. We will rename the attribute for this.

  import { bindable, INode } from 'aurelia';
  
  export class ColorSquareCustomAttribute {
    @bindable() color: string = 'red';
  
    constructor(@INode private element: HTMLElement){
        this.element.style.width = this.element.style.height = '100px';
        this.element.style.backgroundColor = this.color;
    }
    
    bound() {
      this.element.style.backgroundColor = this.color;
    }
  }

We can now provide a color on a per-use basis. Let's go one step further and allow the size to be set too.

  import { bindable, INode } from 'aurelia';
  
  export class ColorSquareCustomAttribute {
    @bindable() color: string = 'red';
    @bindable() size: string = '100px';
  
    constructor(@INode private element: HTMLElement){
        this.element.style.width = this.element.style.height = this.size;
        this.element.style.backgroundColor = this.color;
    }
    
    bound() {
      this.element.style.width = this.element.style.height = this.size;
      this.element.style.backgroundColor = this.color;
    }
}

Responding to bindable property change events

We have code that will work on the first initialization of our custom property, but if the property is changed after rendering, nothing else will happen. We need to use the change detection functionality to update the element when any bindable properties change.

  import { bindable, INode } from 'aurelia';
  
  export class ColorSquareCustomAttribute {
    @bindable() color: string = 'red';
    @bindable() size: string = '100px';
  
    constructor(@INode private element: HTMLElement){
        this.element.style.width = this.element.style.height = this.size;
        this.element.style.backgroundColor = this.color;
    }
    
    bound() {
      this.element.style.width = this.element.style.height = this.size;
      this.element.style.backgroundColor = this.color;
    }
    
    colorChanged(newColor, oldColor) {
      this.element.style.backgroundColor = newColor;
    }
    
    sizeChanged(newSize: string, oldSize: string) {
      this.element.style.width = this.element.style.height = newSize;
    }
}  

As a default convention, bindable property change callbacks will use the bindable property name followed by a suffix of Changed at the end. The change callback gets two parameters, the new value and the existing value.

Want to learn more about bindable properties and how to configure them? Please reference the bindable properties section.

Whenever our size or color bindable properties change, our element will be updated accordingly instead of only at render.

Options binding

Options binding provides a custom attribute with the ability to have multiple bindable properties. Each bindable property must be specified using the bindable decorator. The attribute view model may implement an optional ${propertyName}Changed(newValue, oldValue) callback function for each bindable property.

When binding to these options, separate each option with a semicolon and supply a binding command or literal value, as in the example below. It is important to note that bindable properties are converted to dash-case when used in the DOM, while the view model property they are bound to is kept with their original casing.

  import { bindable, INode } from 'aurelia';
  
  export class ColorSquareCustomAttribute {
    @bindable() color: string = 'red';
    @bindable() size: string = '100px';
  
    constructor(@INode private element: HTMLElement){
        this.element.style.width = this.element.style.height = this.size;
        this.element.style.backgroundColor = this.color;
    }
    
    bound() {
      this.element.style.width = this.element.style.height = this.size;
      this.element.style.backgroundColor = this.color;
    }
    
    colorChanged(newColor, oldColor) {
      this.element.style.backgroundColor = newColor;
    }
    
    sizeChanged(newSize: string, oldSize: string) {
      this.element.style.width = this.element.style.height = newSize;
    }
}  

To use options binding, here is how you might configure those properties:

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

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

Specifying a primary property

When you have more than one bindable property, you might want to specify which property is the primary one (if any). If you mostly expect the user only to configure one property most of the time, you can specify it is the primary property through the bindable configuration.

  import { bindable, INode } from 'aurelia';
   
  export class ColorSquareCustomAttribute {
    @bindable( {primary: true} ) color: string = 'red';
    @bindable() size: string = '100px';
  
    private element = resolve(INode) as HTMLElement;

    constructor() {
        this.element.style.width = this.element.style.height = this.size;
        this.element.style.backgroundColor = this.color;
    }
    
    bound() {
      this.element.style.width = this.element.style.height = this.size;
      this.element.style.backgroundColor = this.color;
    }
    
    colorChanged(newColor, oldColor) {
      this.element.style.backgroundColor = newColor;
    }
    
    sizeChanged(newSize, oldSize) {
      this.element.style.width = this.element.style.height = newSize;
    }
}  

The above example specifies that color is the primary bindable property. Our code actually doesn't change at all. The way we consume the custom attribute changes slightly.

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

<div color-square="blue"></div>

Or, you can bind the value itself to the attribute:

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

<div color-square.bind="myColour"></div>

Finding a custom attribute from the DOM

Sometimes, custom attributes are developed in a group of 2 or more, e.g attributes for a dropdown + toggle button inside it, and it's necessary to retrieve the "parent" attribute from a child attribute. One way to do this is to use CustomAttribute.closest function for walking the DOM and finding a particular custom attribute, based on either name, or the constructor. The name closest mimics the DOM API Element.prototype.closest.

An example of the CustomAttribute.closest is as follow:

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

@customAttribute('bar')
export class Bar {
  host = resolve(INode);
  parent = CustomAttribute.closest(this.host, 'foo');
}

Or the following also works if you want to search using the constructor:

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

@customAttribute('bar')
export class Bar {
  host = resolve(INode);
  parent = CustomAttribute.closest(this.host, Foo); // <--- use constructor instead of string 'foo'
}

Last updated