Getting started
Installation
npx makes aurelia
cd your-project-name
npm start
Convention-based component
// user-card.ts
export class UserCard {
name = "John Doe";
email = "john@example.com";
}
<!-- user-card.html -->
<div class="user-card">
<h3>${name}</h3>
<p>${email}</p>
</div>
Pair creates <user-card> element.
Decorator-based component
import { customElement } from "aurelia";
import template from "./user-card.html?raw";
@customElement({
name: "user-card",
template,
dependencies: [ChildComponent],
})
export class UserCard {
name = "John Doe";
}
Explicit configuration with inline template.
Components
HTML-only components
<!-- status-badge.html -->
<bindable name="status"></bindable>
<bindable name="message"></bindable>
<span class="badge badge-${status}"> ${message} </span>
<!-- Usage -->
<import from="./status-badge.html"></import>
<status-badge status="success" message="Done"> </status-badge>
No TypeScript required.
@bindable properties
import { bindable, BindingMode } from "aurelia";
export class UserCard {
@bindable user: User;
@bindable isActive: boolean = false;
@bindable({ mode: BindingMode.twoWay }) selectedId: string;
@bindable({ mode: BindingMode.oneTime }) config: any;
// Change callback
userChanged(newUser: User, oldUser: User) {
// Handle user change
}
}
<!-- Usage -->
<user-card user.bind="currentUser" is-active.bind="true"> </user-card>
Type coercion
@customElement("my-el")
export class MyEl {
@bindable({ type: Number }) count: number;
@bindable({ type: Boolean }) isActive: boolean;
}
<!-- Strings auto-converted -->
<my-el count="42" is-active="true"></my-el>
Binding
Text interpolation
<h1>Hello, ${name}!</h1>
<p>Total: ${price * quantity}</p>
<span>${user.firstName} ${user.lastName}</span>
Property binding
<!-- Auto two-way on forms -->
<input value.bind="searchTerm" />
<!-- Explicit one-way -->
<span text.to-view="readOnlyData"></span>
<!-- Set once -->
<span text.one-time="initialValue"></span>
<!-- Explicit two-way -->
<input value.two-way="userName" />
Event binding
<!-- Basic events -->
<button click.trigger="save()">Save</button>
<input keydown.trigger:enter="submit()" />
<!-- Prevent default -->
<form submit.trigger:prevent="handleSubmit()">
<!-- Debounce and throttle -->
<input input.trigger="search() & debounce:300" />
<button click.trigger="update() & throttle:1000"></button>
</form>
Ref binding
<input ref="emailInput" />
<button click.trigger="emailInput.focus()">Focus</button>
Access DOM element directly.
Templating
Conditionals
<!-- if.bind -->
<div if.bind="isLoading">Loading...</div>
<!-- if-else -->
<div if.bind="isAuth">Welcome!</div>
<div else>Please log in</div>
<!-- Toggle visibility (keeps in DOM) -->
<div show.bind="isVisible">Toggle</div>
switch.bind
<template switch.bind="status">
<span case="received">Received</span>
<span case="processing">Processing</span>
<span case="shipped">Shipped</span>
<span default-case>Unknown</span>
</template>
repeat.for
<!-- Basic loop -->
<li repeat.for="item of items">${$index}: ${item.name}</li>
<!-- With key (recommended) -->
<div repeat.for="user of users; key.bind: user.id">${user.name}</div>
<!-- Destructuring -->
<div repeat.for="{ id, name } of users">${id}: ${name}</div>
Context variables
| Variable | Description |
|---|---|
$index |
Current index |
$first |
First item |
$last |
Last item |
$even |
Even index |
$odd |
Odd index |
$length |
Array length |
$previous |
Previous item |
Styling
Class binding
<!-- Conditional class -->
<p selected.class="isSelected">Item</p>
<!-- Multiple classes (no spaces) -->
<div alert,alert-danger.class="hasError">Error!</div>
Style binding
<!-- Single property -->
<p background.style="bgColor">Styled</p>
<!-- Multiple properties -->
<div style.bind="{ background: 'red', color: '#FFF' }">Styled</div>
Lifecycle hooks
Execution order
export class UserProfile {
binding() {
// Before bindings processed
}
bound() {
// Data available
}
attached() {
// In DOM, init third-party libs
}
detaching() {
// Cleanup event listeners
}
unbinding() {
// Final cleanup
}
}
Order: constructor → define → hydrating → hydrated → created → binding → bound → attaching → attached → detaching → unbinding → dispose
Parent-child flow
Top-down: define, hydrating, hydrated, binding, attaching
Bottom-up: created, bound, attached, detaching, unbinding
Value converters
Creating converters
import { valueConverter } from "aurelia";
@valueConverter("capitalize")
export class CapitalizeConverter {
toView(value: string): string {
return value?.charAt(0).toUpperCase() + value?.slice(1).toLowerCase();
}
fromView(value: string): string {
return value?.toLowerCase();
}
}
Usage
<!-- Single converter -->
<p>${name | capitalize}</p>
<!-- With parameters -->
<p>${price | currency:'USD'}</p>
<!-- Chained converters -->
<p>${text | sanitize | capitalize}</p>
Dependency injection
Basic injection
import { resolve } from "@aurelia/kernel";
export class UserService {
private http = resolve(IHttpClient);
private logger = resolve(ILogger);
}
Interface tokens
import { DI } from "@aurelia/kernel";
export const IApiClient = DI.createInterface<ApiClient>("IApiClient", (x) =>
x.singleton(ApiClient),
);
// Usage
export class UserService {
private api = resolve(IApiClient);
}
Scopes
import { singleton, transient } from "@aurelia/kernel";
@singleton
export class ConfigService {} // One instance
@transient
export class RequestHandler {} // New every time
Routing
Route configuration
import { route } from "@aurelia/router";
@route({
routes: [
{
path: ["", "home"],
component: Home,
title: "Home",
},
{
path: "users/:id",
component: UserDetail,
title: "User",
},
],
})
export class MyApp {}
Navigation
<nav>
<a href="home">Home</a>
<a href="users/123">User 123</a>
</nav>
<au-viewport></au-viewport>
Custom attributes
import { customAttribute, INode, bindable } from "@aurelia/runtime-html";
import { resolve } from "@aurelia/kernel";
@customAttribute("tooltip")
export class TooltipCustomAttribute {
@bindable text: string;
private element: HTMLElement = resolve(INode) as HTMLElement;
attached() {
this.initTooltip();
}
detaching() {
this.destroyTooltip();
}
private initTooltip() {
// Initialize third-party tooltip
}
private destroyTooltip() {
// Cleanup
}
}
<!-- Usage -->
<button tooltip.bind="tooltipText">Hover me</button>
Gotchas
Change callbacks
⚠️ Change callbacks don't fire on initial bind in Aurelia 2. Call manually in bound() if needed.
export class MyComponent {
@bindable user: User;
bound() {
this.userChanged(this.user, undefined);
}
userChanged(newUser: User, oldUser: User) {
// Handle change
}
}
Array mutations
⚠️ Array index assignment (items[0] = x) won't update DOM. Use splice() or replace array.
// ✗ Won't trigger update
this.items[0] = newItem;
// ✓ Triggers update
this.items.splice(0, 1, newItem);
this.items = [...this.items];
Repeat keys
⚠️ repeat.for without key.bind recreates DOM on reorder. Always use keys for dynamic lists.
<!-- ✗ Recreates DOM -->
<div repeat.for="user of users">${user.name}</div>
<!-- ✓ Preserves DOM -->
<div repeat.for="user of users; key.bind: user.id">${user.name}</div>
Template imports
⚠️ HTML template imports need bundler config: ?raw for Vite, asset/source for Webpack.
// Vite
import template from "./user-card.html?raw";
// Webpack
import template from "./user-card.html";
Attribute types
⚠️ Attribute values are strings. Use .bind or enable type coercion for numbers/booleans.
<!-- ✗ Passes string "42" -->
<my-el count="42"></my-el>
<!-- ✓ Passes number 42 -->
<my-el count.bind="42"></my-el>
Class binding syntax
⚠️ Comma-separated classes must have NO spaces.
<!-- ✗ Invalid -->
<div class1, class2.class="condition"></div>
<!-- ✓ Valid -->
<div class1,class2.class="condition"></div>
Component naming
⚠️ Component names must have hyphens (Web Components standard).
// ✗ Invalid
@customElement('usercard')
// ✓ Valid
@customElement('user-card')
Also see
- Aurelia 2 Docs (docs.aurelia.io)
- Components (docs.aurelia.io)
- Bindable Properties (docs.aurelia.io)
- Lifecycle Hooks (docs.aurelia.io)
- Templates (docs.aurelia.io)
- Value Converters (docs.aurelia.io)
- DI Overview (docs.aurelia.io)