Web Components

The basics of the web-component plugin for Aurelia.

Introduction

Web Components are part of an ever-evolving web specification that aims to allow developers to create native self-contained components without the need for additional libraries or transpilation steps. This guide will teach you how to use Aurelia in Web Components.

Installing The Plugin

To use the plugin, import the interface IWcElementRegistry interface from @aurelia/runtime-html module and start defining web-component custom elements by calling method define on the instance of IWcElementRegistry.

WC custom elements can be defined anytime, either at the application start or later. Applications are responsible for ensuring names are unique.

Extending built-in elements is supported via the 3rd parameter of the define call, like the define call on the global window.customElements.define call.

How it works

  • Each of WC custom element will be backed by a view model, like a normal Aurelia element component.

  • For each define call, a corresponding native custom element class will be created and defined.

  • Each bindable property on the backing Aurelia view model will be converted to a reactive attribute (via observedAttributes) and reactive property (on the prototype of the extended HTML Element class created).

  • Slot: [au-slot] is not supported when upgrading an existing element. slot can be used as a normal WC custom element.

Notes:

  • WC custom element works independently with the Aurelia component. This means the same class can be both a WC custom element and an Aurelia component. Though this should be avoided as it could result in double rendering.

  • containerless mode is not supported. Use extend-built-in instead if you want to avoid wrappers.

  • the defined WC custom elements will continue working even after the owning Aurelia application has stopped.

  • template info will be retrieved & compiled only once per define call. Changing it after this call won't have any effects.

  • bindables info will be retrieved & compiled only once per define call. Changing it after this call won't have any effects.

Examples

For simplicity, all the examples below define elements at the start of an application, but they can be defined at any time.

  1. Defining a tick-clock element

import { Aurelia, IWcElementRegistry } from 'aurelia';

Aurelia
  .register(
    AppTask.creating(IWcElementRegistry, registry => {
      registry.define('tick-clock', class TickClock {
        static template = '${message}';

        constructor() {
          this.time = Date.now();
        }

        attaching() {
          this.intervalId = setInterval(() => {
            this.message = `${Date.now() - this.time} seconds passed.`;
          }, 1000)
        }

        detaching() {
          clearInterval(this.intervalId);
        }
      })
    })
  )
  .app(class App {})
  .start();
  1. Defining a tick-clock element using shadow DOM with open mode

import { Aurelia, IWcElementRegistry } from 'aurelia';

Aurelia
  .register(
    AppTask.creating(IWcElementRegistry, registry => {
      registry.define('tick-clock', class TickClock {
        static template = '${message}';
        static shadowOptions = { mode: 'open' };

        constructor() {
          this.time = Date.now();
        }

        attaching() {
          this.intervalId = setInterval(() => {
            this.message = `${Date.now() - this.time} seconds passed.`;
          }, 1000)
        }

        detaching() {
          clearInterval(this.intervalId);
        }
      })
    })
  )
  .app(class App {})
  .start();
  1. Injecting the host element into the view model

import { INode, Aurelia, IWcElementRegistry } from 'aurelia';

Aurelia
  .register(
    AppTask.creating(IWcElementRegistry, registry => {
      registry.define('tick-clock', class TickClock {
        static template = '${message}';
        static shadowOptions = { mode: 'open' };

        // all these injections result in the same instance
        // listing them all here so that applications can use what they prefer
        // based on HTMLElement 
        static inject = [INode, Element, HTMLElement];

        constructor(node, element, htmlElement) {
          node === element;
          element === htmlElement;
          this.time = Date.now();
        }

        attaching() {
          this.intervalId = setInterval(() => {
            this.message = `${Date.now() - this.time} seconds passed.`;
          }, 1000)
        }

        detaching() {
          clearInterval(this.intervalId);
        }
      })
    })
  )
  .app(class App {})
  .start();
  1. Defining a tick-clock element with format bindable property for formatting

import { INode, Aurelia, IWcElementRegistry } from 'aurelia';

document.body.innerHTML = '<tick-clock format="short"></tick-clock>';

Aurelia
  .register(
    AppTask.creating(IWcElementRegistry, registry => {
      registry.define('tick-clock', class TickClock {
        static template = '${message}';
        static shadowOptions = { mode: 'open' };
        static bindables = ['format'];

        // all these injections result in the same instance
        // listing them all here so that applications can use what they prefer
        // based on HTMLElement 
        static inject = [INode, Element, HTMLElement];

        constructor(node, element, htmlElement) {
          node === element;
          element === htmlElement;
          this.time = Date.now();
        }

        attaching() {
          this.intervalId = setInterval(() => {
            this.message = `${(Date.now() - this.time)/1000} ${this.format === 'short' ? 's' : 'seconds'} passed.`;
          }, 1000)
        }

        detaching() {
          clearInterval(this.intervalId);
        }
      })
    })
  )
  .app(class App {})
  .start();
  1. Defining a tick-clock element extending built-in div element:

import { Aurelia, IWcElementRegistry } from 'aurelia';

document.body.innerHTML = '<div is="tick-clock"></div>'

Aurelia
  .register(
    AppTask.creating(IWcElementRegistry, registry => {
      registry.define('tick-clock', class TickClock {
        static template = '${message}';

        constructor() {
          this.time = Date.now();
        }

        attaching() {
          this.intervalId = setInterval(() => {
            this.message = `${Date.now() - this.time} seconds passed.`;
          }, 1000)
        }

        detaching() {
          clearInterval(this.intervalId);
        }
      })
    }, { extends: 'div' })
  )
  .app(class App {})
  .start();

Last updated