NexusCS

Aurelia

JavaScript
Aurelia: conventions-based framework with dependency injection, two-way binding, and web component support.
featured

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