Get the Newsletter

Templating: Custom Elements Basics

An overview of the Aurelia templating engine's custom element functionality basics. Custom Elements are used to add custom components to Aurelia applications.

Introduction

Custom elements are the primary tool an Aurelia application developer will utilize for componentizing an application.

HTML Only Custom Element

The simplest way to create an Aurelia custom element is to create an Aurelia view template in an HTML file and then require it in to another Aurelia view template. HTML only custom elements are a highly useful strategy for dealing with functionality that has no need for ViewModel logic but is likely to be reused. The element name will be the same as the HTML file name, without the extension. When requiring an HTML only custom element in to a view, you must include the .html extension. It is even possible to create bindable properties for an HTML only custom element by putting a comma separated list of property names on the bindable attribute of the template element. The Aurelia convention of converting camelCase bindable properties to dash-case applies to properties provided to the bindable attribute, as shown in the following example.

    
  <template bindable="firstName, lastName">
    Hello, ${firstName} ${lastName}!
  </template>
  
  
    
  <template>
    <require from="./hello-world.html"></require>
  
    <hello-world first-name="Albert" last-name="Einstein"></hello-world>
  </template>
  
  

HTML only custom elements may require in other custom elements and attributes as well as utilizing any other view resource just like any other Aurelia component may. HTML only custom elements also support explicit two-way databinding for properties, though it is not possible to create properties that default to two-way databinding with HTML only custom elements. For that type of functionality, you will need to provide a ViewModel for your custom element.

The following example shows an Aurelia view utilizing two-way databinding to an example HTML only custom element. The example HTML only custom element itself requires in other custom elements, and utilizes two-way databinding to those custom elements. Note that it is possible to use the full power of Aurelia's templating engine from an HTML custom element, such as using the debounce binding behavior.

    
  <template>
    <require from="./example.html"></require>
  
    Hello, ${guest}!
    <example name.two-way="guest"></example>
  </template>
  
  
    
  <template bindable="name">
    <require from="./yes-or-no.html"></require>
    <require from="./say-goodbye.html"></require>
  
    <p>What is your name? <input type="text" value.bind="name & debounce"></p>
    <yes-or-no question="Are you leaving?" answer.two-way="sayGoodbye"></yes-or-no>
    <say-goodbye if.bind="sayGoodbye" name.bind="name"></say-goodbye>
  </template>
  
  
    
  <template bindable="question, answer">
    <p>
      ${question} <input type="checkbox" checked.bind="answer">
    </p>
  </template>
  
  
    
  <template bindable="name">
    Goodbye, ${name}!
  </template>
  
  

Custom Element Basics

Creating custom elements using Aurelia is extremely simple. Simply creating a JavaScript and HTML file pair with the same name is all that is necessary to create an Aurelia custom element. The HTML file must contain an Aurelia template wrapped in a template element. The JavaScript file must export a JavaScript class. Aurelia's standard naming convention for custom element VM classes is to append CustomElement to the end of the class name, e.g. SecretMessageCustomElement. Aurelia will take the JavaScript class name, strip CustomElement from the end, and convert it from InitCaps to dash-case for the custom element's name. Note that this means it is possible for the custom element name to not match the file name. Thus, it is recommended to name your custom element files to match the custom element name. It is acceptable to export more than one class from the JavaScript file for a custom element. Aurelia will use the first class exported from the file as the custom element's view-model (VM). Note that each instance of a custom element will receive its own separate VM instance.

Custom elements are not allowed to be self-closing. This means that <secret-message /> will not work. When using a custom element, you must provide a closing tag as shown in app.html below.

    
  export class SecretMessageCustomElement {
    secretMessage = 'Be sure to drink your Ovaltine!';
  }
  
  
    
  export class SecretMessageCustomElement {
    secretMessage: string = 'Be sure to drink your Ovaltine!';
  }
  
  
    
  <template>
    ${secretMessage}
  </template>
  
  
    
  <template>
    <require from="./secret-message"></require>
  
    And now, it's time for a secret message: <secret-message></secret-message>
  </template>
  
  

It is also possible to explicitly name your custom element by using the customElement decorator on the VM class. Simply pass a string to this decorator with the exact name you wish to use for your custom element. Aurelia will not convert the string you pass it to dash-case. This means that @customElement('SecretMessage') is not converted to secret-message but to secretmessage. If any uppercase letters are passed to the decorator and development logging is enabled, Aurelia will log a message alerting you that it has lowercased the name. This is because the DOM is not case-sensitive. Thus you must be explicit about any dashes in the attribute name when using this decorator, e.g. @customElement('secret-message').

Aurelia custom elements do not need to follow the naming conventions for Web Components custom elements. Namely, Aurelia allows you to create custom elements that do not have a dash in their name. This is because the Web Components specs reserve all single-word element names for the browser. Thus, you are free to create a foo custom element with Aurelia; however, it is recommended to refrain from creating single-world custom elements to avoid any chance of a possible naming clash in the future. Also, any Aurelia custom elements that are intended to be used as standalone Web Components custom elements MUST have a dash in their name.

Before we move on, let's discuss just how easy it is to create a custom element in Aurelia and the impact it has on Aurelia's naming conventions for custom element view-model classes. One capability of the Aurelia framework is that it can take components that were originally created for use as a page in an application and use them as custom elements. When this happens, Aurelia will use the component's VM class name, dash-case it and use that as the custom element's name. Let's say there is an Aurelia application that provides various pages, one of which is the Contact page. All it takes to use the Contact page as a custom element on any page in the application is to require it in to the view. At that point, it is available as the contact custom element in that view. It is even possible to provide bindable properties for the page that can be used when using the page as a custom element. This means that, if you wish, you may ignore the Aurelia naming convention for your custom elements. In the example above, we could have simply named the class SecretMessage. The custom element would still be named secret-message. Given this capability, it might be considered wise to utilize Aurelia's naming convention for custom elements or use the customElement decorator to be explicit when creating a component that is only meant to be used as a custom element and not as a standalone page.

Bindable Properties

Any properties or functions of the VM class may be used for binding within the custom element's view; however, a custom element must specify the properties that will be bindable as attributes on the custom element. This is done by decorating each bindable property with the bindable decorator. The default binding mode for bindable properties is one-way. This means that a property value can be bound in to your custom element, but any changes the custom element makes to the property value will not be propogated out of the custom element. This default may be overridden, if needed, by passing a settings object to the bindable decorator with a property named defaultBindingMode set. This property should be set to one of the four bindingMode options: oneTime, fromView, toView / oneWay, or twoWay. Both bindable and bindingMode may be imported from the aurelia-framework module. Let's look at an example custom element with a bindable property that defaults to two-way binding.

    
  import {bindable, bindingMode} from 'aurelia-framework';
  
  export class SecretMessageCustomElement {
    @bindable({ defaultBindingMode: bindingMode.twoWay }) message;
    @bindable allowDestruction = false;
  
    constructor() {
      setInterval(() => this.deleteMessage(), 10000);
    }
  
    deleteMessage() {
      if(this.allowDestruction === true) {
        this.message = '';
      }
    }
  }
  
  
    
  import {bindable, bindingMode} from 'aurelia-framework';
  
  export class SecretMessageCustomElement {
    @bindable({ defaultBindingMode: bindingMode.twoWay }) message: string;
    @bindable allowDestruction: boolean = false;
  
    constructor() {
      setInterval(() => this.deleteMessage(), 10000);
    }
  
    deleteMessage() {
      if(this.allowDestruction === true) {
        this.message = '';
      }
    }
  }
  
  
    
  <template>
    <p>
      Urgent, secret message: ${message}
    </p>
    <p>
      This message will ${allowDestruction === false ? 'not ' : '' } self-destruct in less than 10 seconds!
    </p>
  </template>
  
  
    
  <template>
    <require from="./secret-message"></require>
  
    <p>
      Secret Message: <input type="text" value.bind="message">
    </p>
    <p>
      Allow Message to Destruct? <input type="checkbox" checked.bind="allowDestruction">
    </p>
    <secret-message message.bind="message" allow-destruction.bind="allowDestruction" ></secret-message>
  </template>
  
  

In this example, the secret-message custom element will check every ten seconds to see if it needs to destroy (set to an empty string) the message it receives via databinding. When told to destroy the message, Aurelia's databinding system will update the bound property of the component using the custom element, thanks to the custom element specifying that this property's default binding mode is two-way. Thus, the text box will be cleared when the message "self destructs." Of course, the component using the custom element is free to override this default by explicitly specifying the binding direction via the one-way, two-way, or one-time binding commands.

Whether a secret message that is only shown to the person who writes the message is very useful is for you to decide.

Declarative Computed Values

As your application grows, custom elements get complicated and often values that are computed based on other values start to appear. This can be handled either by creating getters in your custom element view model, or by using Aurelia's let element. You can think of let like a declaration in a JavaScript expression. For instance, a name tag form example might consist of two input fields with values bound to view model properties firstName and lastName, like the following:

    
  <div>
    First name:
    <input value.bind="firstName">
    Last name:
    <input value.bind="lastName">
  </div>
  Full name is: "${firstName} ${lastName}"
  
  

Notice the expression \${firstName} \${lastName}. What if we want to use it somewhere else, or give it a more meaningful name like fullName? We have the option to react on change of either firstName or lastName and re-compute fullName in view model, or declare a getter that returns the combination of those values like the following examples:

    
  export class App {
    @bindable firstName;
    @bindable lastName;
  
    // Aurelia convention, called after firstName has changed
    firstNameChanged(newFirstName: string) {
      this.fullName = `${newFirstName} ${this.lastName}`;
    }
  
    // Aurelia convention, called after firstName has changed
    lastNameChanged(newLastName: string) {
      this.fullName = `${this.firstName} ${newLastName}`;
    }
  }
  
  // Or with getter and `computedFrom` decorator
  export class App {
    @bindable firstName;
    @bindable lastName;
  
    @computedFrom('firstName', 'lastName')
    get fullName() {
      return `${this.firstName} ${this.lastName}`;
    }
  }
  
  
    
  export class App {
    @bindable firstName: string;
    @bindable lastName: string;
  
    fullName: string;
  
    // Aurelia convention, called after firstName has changed
    firstNameChanged(newFirstName: string) {
      this.fullName = `${newFirstName} ${this.lastName}`;
    }
  
    // Aurelia convention, called after firstName has changed
    lastNameChanged(newLastName: string) {
      this.fullName = `${this.firstName} ${newLastName}`;
    }
  }
  
  // Or with getter and `computedFrom` decorator
  export class App {
    @bindable firstName: string;
    @bindable lastName: string;
  
    @computedFrom('firstName', 'lastName')
    get fullName() {
      return `${this.firstName} ${this.lastName}`;
    }
  }
  
  

Aurelia provides a simpler way to achieve the above result, with the more declarative let element. Using the let element, the above example would be rewritten as:

Either using interpolation:

    
  <let full-name="${firstName} ${lastName}"></let>
  <div>
    First name:
    <input value.bind="firstName">
    Last name:
    <input value.bind="lastName">
  </div>
  Full name is: "${fullName}"
  
  

Or an expression:

    
  <let full-name.bind="firstName + ' ' + lastName"></let>
  <div>
    First name:
    <input value.bind="firstName">
    Last name:
    <input value.bind="lastName">
  </div>
  Full name is: "${fullName}"
  
  

And now after either firstName or lastName has changed, fullName is recomputed automatically and is ready to be used in other parts of the view.

Additionally, if there is a need to react to changes on both fullName, we can specify a special attribute to-binding-context on the <let/> element to notify bindings to assign the value to the binding context, which is your view model, instead of override context, which is your view.

    
  <let to-binding-context full-name="${firstName} ${lastName}"></let>
  <div>
    First name:
    <input value.bind="firstName">
    Last name:
    <input value.bind="lastName">
  </div>
  Full name is: "${fullName}"
  
  
    
  export class App {
    @bindable firstName;
    @bindable lastName;
  
    @observable fullName;
  
    // Aurelia convention, called after fullName has changed
    fullNameNameChanged(fullName) {
      // Do stuff with new full name
    }
  }
  
  
    
  export class App {
    @bindable firstName: string;
    @bindable lastName: string;
  
    @observable fullName: string;
  
    // Aurelia convention, called after fullName has changed
    fullNameNameChanged(fullName: string) {
      // Do stuff with new full name
    }
  }
  
  

Surrogate behaviors allow you to add attributes, event handlers, and bindings on the template element for a custom element. This can be extremely useful in many cases, but one particular area that it is helpful is with dealing with aria attributes to help add accessibility to your custom elements. When using surrogate behaviors, you add attributes to the template element for your custom element. These attributes will be placed on the custom element itself at runtime. For example, consider the view for a my-button custom element:

    
  <template role="button">
    <div>My Button</div>
  </template>
  
  
    
  <template>
    <require from="my-button"></require>
  
    <my-button></my-button>
  </template>
  
  

The role="button" attribute will automatically be set on the my-button element whenever it used in an Aurelia application. If you were to check your browser's Dev Tools while running a template that used the my-buttom custom element, you will see something that looks like the below

    
  <my-button class="au-target" au-target-id="1" role="button">
    <div>My Button</div>
  </my-button>
  
  

It is important to note that Surrogate Behaviors cannot be used with a custom element that is using the @containerless decorator discussed below as this decorator removes the wrapping custom element from the DOM, and thus there is nowhere for the Surrogate Behaviors to be placed.

Basic Content Projection

So far, we've only talked about custom elements that look like <custom-element attr.bind="vmProp"></custom-element>. Now it's time to look at creating custom elements that have content inside them. Let's create a name tag custom element. When the name-tag element is used, it will take the name it will display as content in the element.

    
  <name-tag>
    Ralphie
  </name-tag>
  
  

Aurelia custom elements utilize the "slot based" content projection standard from the Web Component specifications. Let's look at how this will work with our name-tag element. This custom element utilizes a single slot, so we simply need to add a <slot></slot> element in our template where we would like content to be projected.

    
  <template>
    <div class="header">
      Hello, my name is
    </div>
    <div class="name">
      <slot></slot>
    </div>
  </template>
  
  

Aurelia will project the element's content in to the template where the <slot></slot> element is located.

Decorators for Customizing Aurelia Custom Element Processing

There are lots of options that allow you to change how custom elements work. These are expressed by decorators added to the custom element's viewmodel or properties on the viewmodel.

  • @children(selector) - Decorates a property to create an array on your class that has its items automatically synchronized based on a query selector against the element's immediate child content.
  • @child(selector) - Decorates a property to create a reference to a single immediate child content element.
  • @processContent(false|Function) - Tells the compiler that the element's content requires special processing. If you provide false to the decorator, the compiler will not process the content of your custom element. It is expected that you will do custom processing yourself. But, you can also supply a custom function that lets you process the content during the view's compilation. That function can then return true/false to indicate whether or not the compiler should also process the content. The function takes the following form function(compiler, resources, node, instruction):boolean
  • @useView(path) - Specifies a different view to use.
  • @noView(dependencies?) - Indicates that this custom element does not have a view and that the author intends for the element to handle its own rendering internally. This is extremely useful when "wrapping" legacy JavaScript widgets that programatically create their markup
  • @inlineView(markup, dependencies?) - Allows the developer to provide a string that will be compiled into the view.
  • @useShadowDOM() - Causes the view to be rendered in the ShadowDOM. When an element is rendered to ShadowDOM, a special DOMBoundary instance can optionally be injected into the constructor. This represents the shadow root.
  • @containerless() - Causes the element's view to be rendered without the custom element container wrapping it. This cannot be used in conjunction with @child, @children or @useShadowDOM decorators. It also cannot be used with surrogate behaviors. Use sparingly.
  • @viewResources(...dependencies) - Adds dependencies to the underlying View. Same as: <require from="..."></require>, but declared in the ViewModel. Arguments can either be strings with moduleIds, Objects with src and optionally as properties, or classes of the module to be included.