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.
This is not a limitation of Aurelia. It is HTML5 who doesn't support self-closing syntax. Self-closing is a XML syntax, not HTML5. To be clear, you can use self-closing in SVG when it's embedded inside HTML page, because SVG is actually XML. When browser parses an embedded SVG inside HTML page, it uses XML parser to parse the SVG part.
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></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
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.
@child(selector)
- Decorates a property to create a reference to a single immediate child content element. Does not work with@containerless()
, see below.@children(selector)
- Decorates a property to create an array of content elements. The array is automatically synchronized to the the element's immediate child content by a DOM query selector. Does not work with@containerless()
, see below.@processContent(false|Function)
- Tells the compiler that the element's content requires special processing. If you providefalse
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 formfunction(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(options?: { mode: 'open' | 'closed' })
- Causes the view to be rendered in the ShadowDOM. When an element is rendered to ShadowDOM, a specialDOMBoundary
instance can optionally be injected into the constructor. This represents the shadow root. Does not work with@containerless()
, see below.@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 withmoduleId
s,Object
s withsrc
and optionallyas
properties, or classes of the module to be included.
Examples: Show me the code!!
The utility of some of these decorators is immediately evident from the description. However, that may not be the case for some of the more involved ones. Thus, in this section we would like to show some simple examples to portray the usefulness of some of these decorators. Some of these examples are even silly (why? for the sake of science!), but we trust you will find the proper use in your app.
Display a template fragment in your Aurelia app using @processContent
, and @noView
Suppose we want to display the following template fragment in our app.
<span>${message}</span>
A naive approach would be to use something like this:
<template>
<pre>
<code>
<span>${message}</span>
</code>
</pre>
</template>
However, with this code Aurelia will perform string interpolation for \${message}
, and depending on whether or not the view-model has a message
property, the app will display the value of message
or nothing. But that is not what was intended. To show such code fragments, we can create a no-op custom element to wrap them.
// reference: https://stackoverflow.com/a/50066210/2270340
import { noView, processContent } from "aurelia-framework";
@noView()
@processContent(false)
export class CodeFragment {}
<template>
<require from="./code-fragment"></require>
<code-fragment>
<pre>
<code>
<span>${message}</span>
</code>
</pre>
</code-fragment>
</template>
When Aurelia compiles the code-fragment
part of the view it does not look for a view for CodeFragment
as the class is decorated with @noView()
. Also it does not process any content that comes between <code-fragment>
and </code-fragment>
, as instructed by @processContent(false)
. You can see the result below.
So, go ahead and create the documentation app you always wanted to have, to show-case the usage of your own custom element library.
Pretty looking templates for your Tabs control
When we want to create our own custom element for a Tabs control, it is much more compact and intuitive if we can use the element as follows:
<tabs>
<tab header="IMDB's Top 250 Movies">
<ul>
<li>The Shawshank Redemption</li>
<li>The Godfather</li>
<li>The Godfather: Part II</li>
<li>The Dark Knight</li>
<li>12 Angry Men</li>
<li>...</li>
</ul>
</tab>
<tab header="IMDB's Top 250 TV Shows">
<ul>
<li>Planet Earth II</li>
<li>Band of Brothers</li>
<li>Game of Thrones</li>
<li>Planet Earth</li>
<li>Breaking Bad</li>
<li>...</li>
</ul>
</tab>
<tab header="Recently Added">
Nothing to see here.
</tab>
</tabs>
In this way, the tab header and the respective content stay together. As a result, reordering the tabs becomes easier, without accidentally showing the wrong content under an unrelated header. However, the rendered DOM for such tabs is usually (refer
Bootstrap Tabs
) like the following, where we have a separate section for the tab header in form of a ul>li
and a separate section for the content.
<ul>
<li><a>IMDB's Top 250 Movies</a></li>
<li><a>IMDB's Top 250 TV Shows</a></li>
<li><a>Recently Added</a></li>
</ul>
<div>
<div>...</div>
<div>...</div>
<div>...</div>
</div>
We can use the @processContent
decorator along with a function to achieve this. The following is a minimal implementation:
<template>
<require from="./tabs"></require>
<tabs>
<tab header="Top Movies">
<ul>
<li>The Shawshank Redemption</li>
<li>The Godfather</li>
<li>The Godfather: Part II</li>
<li>The Dark Knight</li>
<li>12 Angry Men</li>
</ul>
</tab>
<tab header="Top TV Shows">
<ul>
<li>Planet Earth II</li>
<li>Band of Brothers</li>
<li>Game of Thrones</li>
<li>Planet Earth</li>
<li>Breaking Bad</li>
</ul>
</tab>
<tab header="Recently Added"> Nothing to see here. </tab>
</tabs>
</template>
<template>
<!-- Here we have 2 replaceable templates for header and content -->
<div><template replaceable part="header"></template></div>
<div><template replaceable part="content"></template></div>
</template>
import { bindable, BindingEngine, Container, processContent } from "aurelia-framework";
function processTabs(compiler, resources, node, instruction) {
// first create 2 templates for the replaceable parts
const headerTemplate = document.createElement("template");
headerTemplate.setAttribute("replace-part", "header");
const contentTemplate = document.createElement("template");
contentTemplate.setAttribute("replace-part", "content");
// process all tabs
const tabs = Array.from(node.querySelectorAll("tab"));
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
// add header
const header = document.createElement("button");
header.classList.add("btn", "btn-link");
header.setAttribute("click.delegate", `showTab('${i}')`);
header.innerText = tab.getAttribute("header");
headerTemplate.content.appendChild(header);
// add content
const content = document.createElement("div");
content.setAttribute("show.bind", `activeTabId=='${i}'`);
content.append(...Array.from(tab.childNodes));
contentTemplate.content.appendChild(content);
node.removeChild(tab);
}
// Activate the first tab
const bindingEngine = Container.instance.get(BindingEngine);
instruction.attributes = {
...instruction.attributes,
"active-tab-id": bindingEngine.createBindingExpression("activeTabId", "'0'")
};
node.append(headerTemplate, contentTemplate);
return true;
}
@processContent(processTabs)
export class Tabs {
@bindable activeTabId;
showTab(tabId) {
this.activeTabId = tabId;
}
}
import { BehaviorInstruction, bindable, BindingEngine, Container, processContent } from "aurelia-framework";
@processContent(Tabs.processTabs)
export class Tabs {
// A static method in the custom element class itself is more compact than an external function as shown the JavaScript version.
// But both serves same purpose.
public static processTabs(_compiler, _resources, node: HTMLElement, instruction: BehaviorInstruction) {
// first create 2 templates for the replaceable parts
const headerTemplate = document.createElement("template");
headerTemplate.setAttribute("replace-part", "header");
const contentTemplate = document.createElement("template");
contentTemplate.setAttribute("replace-part", "content");
// process all tabs
const tabs = Array.from(node.querySelectorAll("tab"));
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
// add header
const header = document.createElement("button");
header.setAttribute("click.delegate", `showTab('${i}')`);
header.innerText = tab.getAttribute("header");
headerTemplate.content.appendChild(header);
// add content
const content = document.createElement("div");
content.setAttribute("show.bind", `activeTabId=='${i}'`);
content.append(...Array.from(tab.childNodes));
contentTemplate.content.appendChild(content);
node.removeChild(tab);
}
// Activate the first tab
const bindingEngine: BindingEngine = Container.instance.get(BindingEngine);
instruction.attributes = {
...instruction.attributes,
"active-tab-id": bindingEngine.createBindingExpression("activeTabId", "'0'")
};
node.append(headerTemplate, contentTemplate);
return true;
}
@bindable public activeTabId: string;
public showTab(tabId: string) { this.activeTabId = tabId; }
}
The processTabs
method used in @processContent
may look overwhelming, but it's mostly pure DOM manipulation. The function takes four arguments, out of which node
holds the DOM fragment for <tabs>...</tabs>
used in app.html
. And then in this function, we transforms the original DOM fragment to something like below.
<tabs>
<template replace-part="header">
<button click.delegate="showTab('0')">Top Movies</button>
<button click.delegate="showTab('1')">Top TV Shows</button>
<button click.delegate="showTab('2')">Recently Added</button>
</template>
<template replace-part="content">
<div show.bind="activeTabId=='0'">...</div>
<div show.bind="activeTabId=='1'">...</div>
<div show.bind="activeTabId=='2'">...</div>
</template>
</tabs>
You may have noticed that we have also used the some Aurelia syntax, such as click.delegate
, or show.bind
. We have even used instruction
to bind the @bindable activeTabId
of the Tabs
class, by createBindingExpression
using BindingEngine
. This enables us to set '0'
to activeTabId
which in turn activates the first tab by default. However, we need to instruct Aurelia to process these instructions further which we do by return
ing true
at the end of the function. To keep the example simple, we have removed some non-crucial parts of the code. However, you can see the result and source code below and play with it.
You may be thinking that "all that is okay, but how can I use my awesome custom controls?". You can definitely do that. In our example, we are packing all the child nodes of tabs
directly into the respective content div
. Therefore, you are absolutely free to use your awesome custom controls to compose the tab content. Let's see if we can use a custom element for the header. This seems like a good idea. As you start adding some styles to the header, it might start looking bulky and messy (as you might have seen in the code sandbox). Let's start with creating a custom element for the tab header.
<template>
<button class="btn btn-link" class.bind="isActive?'active':''" click.delegate="tabSelector()">
${name}
</button>
</template>
import { bindable } from "aurelia-framework";
export class TabHeader {
@bindable tabId;
@bindable isActive;
@bindable name;
@bindable tabSelector = id => {};
}
import { bindable } from "aurelia-framework";
export class TabHeader {
@bindable public tabId: string;
@bindable public isActive: boolean;
@bindable public name: string;
@bindable public tabSelector: (id: string) => void = id => {};
}
This is a pretty simple and straight forward custom element with couple of @bindable
s used to display and decorate the button
in the view. There is a @bindable
lambda/arrow function to select the tabs. We intend to bind this value from Tabs
so that it retains control over showing a particular tab (because you know we might want to compute the answer to life, the universe and everything there). To use this custom element in Tabs, we start by including that in tabs.html
and replacing the button
in processTabs
with tab-header
.
<template>
<require from="./tab-header"></require>
...
</template>
function processTabs(compiler, resources, node, instruction) {
...
for(let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
// add header
const header = document.createElement("tab-header");
header.setAttribute("tab-id", `${i}`);
header.setAttribute("name", tab.getAttribute("header"));
header.setAttribute("is-active.bind", `activeTabId=='${i}'`);
header.setAttribute("tab-selector.bind", `showTab('${i}')`);
...
}
...
return true;
}
...
export class Tabs {
public static processTabs(_compiler, resources: ViewResources, node: HTMLElement, instruction: BehaviorInstruction) {
...
for(let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
// add header
const header = document.createElement("tab-header");
header.setAttribute("tab-id", `${i}`);
header.setAttribute("name", tab.getAttribute("header"));
header.setAttribute("is-active.bind", `activeTabId=='${i}'`);
header.setAttribute("tab-selector.bind", `showTab('${i}')`);
...
}
...
return true;
}
...
}
You might expect this much to work. However, there is a caveat. When we do the DOM transformation in processContent
, we manipulate the DOM of the parent view, in this case that is app.html
. As we have not require
d the tab-header
in app.html
, it has no idea of how to process a tab-header
when we create a tab-header
element in processContent
, as it has no prior knowledge of the existence of a tab-header
custom element. A couple of solutions include require
ing tab-header
in app.html
, or registering it as a global resource. However, in both cases, the Tabs
control looses complete control over its view. Instead, we can register the additional resource (tab-header
) in processTabs
as follows:
function processTabs(compiler, resources, node, instruction) {
...
resources.registerElement("tab-header", instruction.type.viewFactory.resources.getElement("tab-header"));
for(let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
// add header
const header = document.createElement("tab-header");
header.setAttribute("tab-id", `${i}`);
header.setAttribute("name", tab.getAttribute("header"));
header.setAttribute("is-active.bind", `activeTabId=='${i}'`);
header.setAttribute("tab-selector.bind", `showTab('${i}')`);
...
}
...
return true;
}
...
import { BehaviorInstruction, bindable, BindingEngine, Container, processContent, ViewResources } from "aurelia-framework";
export class Tabs {
public static processTabs(_compiler, resources: ViewResources, node: HTMLElement, instruction: BehaviorInstruction) {
...
resources.registerElement("tab-header", instruction.type.viewFactory.resources.getElement("tab-header"));
for(let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
// add header
const header = document.createElement("tab-header");
header.setAttribute("tab-id", `${i}`);
header.setAttribute("name", tab.getAttribute("header"));
header.setAttribute("is-active.bind", `activeTabId=='${i}'`);
header.setAttribute("tab-selector.bind", `showTab('${i}')`);
...
}
...
return true;
}
...
}
This registration works because by the time Aurelia calls the processContent
function, the view of the custom element is already compiled. Thus, at this point Aurelia knows which view resources are needed by the custom element itself. In our case, it knows that Tabs
needs TabHeader
, as we require
d that in tabs.html
. Therefore, the last thing to do is to register the tab-header
element, so that app
also knows about this. You can check out the complete code and the result in the embedded sandbox below. The lines of code in this version might be more than the simpler version with button
, but now you know how to use your own custom element in processContent
.
Have you noted that you can register the control with a different name and use that while creating the element? So, it is possible to alias your tab-header
as zaphod-beeblebrox
. Fancy that!
Using shadow dom
A simple hello-world example showing how to scope your styles to your custom element using shadow dom.
<template>
<require from="./hello-world.css" as="scoped"></require>
<h1>${message}</h1>
</template>
h1 {
color: green;
}
import { useShadowDOM } from 'aurelia-framework';
@useShadowDOM()
export class HelloWorld {
message = 'Hello World!';
}
import { useShadowDOM } from 'aurelia-framework';
@useShadowDOM()
export class HelloWorld {
public message: string = 'Hello World!';
}
Using shadow dom in a HTML only custom component
<template use-shadow-dom>
<require from="./hello-world.css" as="scoped"></require>
<h1>Hello World!</h1>
</template>