Cheat Sheet

Bundler note: These examples import '.html' files as raw strings (showing '?raw' for Vite/esbuild). Configure your bundler as described in Importing external HTML templates with bundlers so the imports resolve to strings on Webpack, Parcel, etc.

Cheat Sheet

A quick reference for different aspects of Aurelia with minimal explanation.

Migration quick reference

Concern
Aurelia 1 idiom
Aurelia 2 equivalent
Notes

Bootstrapping

aurelia.use.standardConfiguration().feature(...).start().then(() => aurelia.setRoot())

Aurelia.register(...).app(AppRoot).start()

The aurelia package already registers StandardConfiguration plus a BrowserPlatform-backed IPlatform. Call app() with a component type or { host, component }.

Router

<router-view> + configureRouter

<au-viewport> + static routes/@route + RouterConfiguration

Router resources (viewport element, load/href attributes) live in @aurelia/router. Route lifecycle hooks are instance methods (canLoad, loading, canUnload, unloading).

Compose

<compose view-model="'./path'" ...>

<au-compose component.bind="ImportedType" template.bind="htmlString">

String-based module IDs and PLATFORM.moduleName are gone. Import classes directly or use () => import('./module') factories.

Task queue

TaskQueue from aurelia-task-queue

queueTask + queueAsyncTask + queueRecurringTask + runTasks

runTasks also runs recurring tasks, but wont stop it from running in the next interval

Compatibility gap

aurelia.use.plugin('@aurelia/compat-v1')

Aurelia.register(compatRegistration)

@aurelia/compat-v1 turns on delegate/call, .call bindings, BindingEngine, inlineView, and compose aliases so AU1 templates run while you migrate.

Bootstrapping

Simple

src/main.ts

import { Aurelia } from 'aurelia';
import { AppRoot } from './app-root';

const app = new Aurelia();
await app.app({
  component: AppRoot,
  host: document.querySelector('app-root'),
}).start();

Script tag (Vanilla JS)

Note: you can copy-paste the markup below into an html file and open it directly in the browser. There is no need for any tooling for this example.

index.html

Script tag (Vanilla JS - enhance)

Note: you can copy-paste the markup below into an html file and open it directly in the browser. There is no need for any tooling for this example.

index.html

Multi-root

Note: the sample below mainly demonstrates stopping an existing instance of Aurelia and starting a new one with a different root and host. Do not consider this a complete example of a proper way to separate a private app root from public view, which is a topic of its own.

src/main.ts

src/login-wall.ts

Advanced (low-level)

When you need more control over the wireup and/or want to override some of the defaults wrapped by the 'aurelia' package and/or maximize tree-shaking of unused parts of the framework:

AppTask lifecycle hooks and deterministic teardown

AppTask is implemented in packages/runtime-html/src/app-task.ts and exposes the same lifecycle slots that RouterConfiguration uses internally. tasksSettled waits for all tasks to be run first, so tests can flush work deterministically.

compat-v1 bridge while migrating

The compatRegistration bundle turns on the .call binding command, delegate syntax, BindingEngine, inlineView, and compose aliases so legacy Aurelia 1 templates keep rendering while you port them to native Aurelia 2 patterns.

Custom elements

With conventions

Without conventions

Vanilla JS

Custom attributes

With conventions

Without conventions

Vanilla JS

Template controllers

With conventions

Without conventions

Vanilla JS

Binding behaviors

With conventions

Without conventions

Vanilla JS

Value converters

With conventions

Without conventions

Vanilla JS

Attribute patterns

Binding commands / instruction renderer

CSS Classes & Styles

Observable Properties

Templating syntax

Built-in custom attributes & template controllers (AKA directives)

Lifecycle hooks

Migrating from v1

  • Rename bind to binding

    • If you had timing issues in bind and used the queueMicroTask to add some delay (or used attached for things that really should be in bind), you could try using bound instead (and remove the queueMicroTask). This hook was added to address some edge cases where you need information that's not yet available in bind, such as from-view bindings and refs.

    • If you used CompositionTransaction in the bind hook to await async work, you can remove that and simply return a promise (or use async/await) in binding instead. The promise will be awaited by the framework before rendering the component or binding and of its children.

  • Rename attached to attaching

    • If you had timing issues in attached and used queueMicroTask or even queueTask to add some delay, you can probably remove the queueMicroTask / queueTask and keep your logic in the attached hook. Where attaching is the new "called as soon as this thing is added to the DOM", attached now runs much later than it did in v1 and guarantees that all child components have been attached as well.

  • Rename unbind to unbinding (there is no unbound)

  • Rename detached to detaching (there is no more detached)

    • If you really need to run logic after the component is removed from the DOM, use unbinding instead.

  • If you need the owningView, consider the interface shown below: what was "view" in v1 is now called "controller", and what was called "owningView" in v1 is now called "parentController" (or simply parent in this case). You can inject it via DI using resolve(IController), therefore it's no longer passed-in as an argument to created.

The view model interfaces

You can import and implement these in your components as a type-checking aid, but this is optional.

Dependency Injection

Migrating from v1

Most stuff from v1 will still work as-is, but we do recommend that you consider using DI.createInterface liberally to create injection tokens, both within your app as well as when authoring plugins.

Consumers can use these as either parameter decorators or as direct values to .get(...) / static inject = [...].

The benefit of parameter decorators is that they also work in Vanilla JS with babel and will work natively in browsers (without any tooling) once they implement them.

They are, therefore, generally the more forward-compatible and consumer-friendly option.

Creating an interface

Note: this is a multi-purpose "injection token" that can be used as a plain value (also in VanillaJS) or as a parameter decorator (in TypeScript only)

Strongly-typed with default

Useful when you want the parameter decorator and don't need the interface itself.

No default (more loosely coupled)

Injecting an interface

Registration types

Creating resolvers explicitly

This is more loosely coupled (keys can be declared independently of their implementations) but results in more boilerplate. More typical for plugins that want to allow effective tree-shaking, and less typical in apps.

These can be provided directly to e.g. au.register(dep1, dep2) as global dependencies (available in all components) or to the static dependencies = [dep1, dep1] of components as local dependencies.

Decorating classes

Customizing injection

Using lifecycle hooks in a non-blocking fashion but keeping things awaitable

Example that blocks rendering (but is simplest to develop)

Example that does not block rendering and avoids race conditions (without task queue)

Example that does not block rendering and avoids race conditions (with task queue)

Apply the practices above consistently, and you'll reap the benefits:

Router & Navigation

Route Configuration

IRouter.isActive always needs a RouteContextLike. Inside routed components you can resolve(IRouteContext) as shown above; elsewhere pass the owning element/controller or an explicit IRouteContext so the router knows which viewport tree to compare against.

Router Viewports

Configure RouterConfiguration.customize({ activeClass: 'active' }) to have the load custom attribute toggle that class automatically, or bind active.two-way/active.class as above for fine-grained control. Because load resolves the element's href internally, these links continue to work even without JavaScript and honor the app's base URL.

Validation

Basic Validation Rules

Validation Controller

Template Validation Display

Animation

CSS Animations

Web Animations API

Router Transition Animations

Programmatic Animations

UI Composition

au-compose

Programmatic Composition

Synthetic Views

Creating Synthetic Views

Template Controllers with Synthetic Views

Shadow DOM & Slots

Shadow DOM Setup

Using Slots

Replaceable Parts

UI Virtualization

Virtual Repeat

Virtual Repeat Configuration

Testing

Component Testing

Integration Testing

Internationalization (i18n)

Basic Setup

Template Usage

Programmatic Usage

State Management

Observable State

Store Pattern

Dialog & Modal

Dialog Setup

Dialog Component

HTTP Client

Basic Setup

Service Usage

HTTP Client Configuration

Integration (plugins, shared components, etc)

Migrating from v1

One of the biggest differences compared to Aurelia v1 is the way integrations work.

In v1, you would have a configure function like so:

index.ts (producer)

Which would then be consumed as either a plugin or a feature like so:

main.ts (consumer)

consumer

In v2 the string-based conventions are no longer a thing. We use native ES modules now. And there are no more different APIs for resources, plugins and features. Instead, everything is a dependency that can be registered to a container, its behavior may be altered by specific metadata that's added by framework decorators.

The most literal translation from v1 to v2 of the above, would be as follows:

index.ts (producer)

main.ts (consumer)

consumer

The register method

In Aurelia v2, everything (including the framework itself) is glued together via DI. The concept is largely the same whether you're building a plugin, a shared component or a service class.

The producer (or the exporting side) exposes an object with a register method, and the consumer (the importing side) passes that object into its au.register call (for global registration) or into the dependencies array of a custom element (for local registration).

The DI container calls that register method and passes itself in as the only argument. The producer can then register resources / components / tasks to that container. Internally, things like resources and tasks have special metadata associated with them which allows the framework to discover and consume them at the appropriate times.

Below are some examples of how integrations can be produced and consumed:

1.1 Simple object literal with a register method

index.ts (producer)

main.ts (consumer)

1.2 A function that returns an object literal with a register method (to pass in e.g. plugin options)

index.ts (producer)

main.ts (consumer)

1.3 An interface

index.ts (producer)

Interfaces and classes do not need to be registered explicitly. They can immediately be injected. The container will "jit register" them the first time they are requested.

1.4 A class (typically a resource)

index.ts (producer)

main.ts (consumer)

To register it as a global resource (available in all components)

OR:

name-list.ts (consumer)

To register it as a local resource (available only in that specific custom element)

1.5 A (module-like) object with any of the above as its properties

resources/index.ts (producer)

main.ts (consumer)

Last updated

Was this helpful?