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
import au from'aurelia';import { LoginWall } from'./login-wall';awaitau.app({ component: LoginWall, host:document.querySelector('login-wall'),}).start();
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:
<!-- Render the 'firstName' and 'lastName' properties interpolated with some static text (reactive) --><div>Hello, ${firstName} ${lastName}</div><!-- Render the 'bgColor' property as a class name, interpolated with some static text (reactive) --><divclass="col-md-4 bg-${bgColor}"><!-- Bind the `someProp` property of the declaring component to the `prop` property of the declared component --><my-componentprop.bind="someProp"><!-- prop.bind is shorthand for prop.bind="prop" (when attr value is omitted, the left side of the dot is used as the value) -->
<my-componentprop.bind><!-- This is another flavor of interpolation but in normal JS syntax, using .bind --><divclass.bind="`col-md-4 bg-${bgColor}`"><!-- Conditionally add the 'active' class (the "old-fashioned" way) --><liclass="isActive ? 'active' : ''"><!-- Conditionally add the 'active' class (the cleaner way) --><liactive.class="isActive"><!-- Bind to the 'style' attribute (the "old-fashioned" way) --><listyle="background: ${bg}"><!-- Bind to the 'style' attribute (the cleaner way) --><libackground.style="bg"><!-- Listen to the 'blur' event using a direct event listener on the element --><inputblur.trigger="onBlur()"><!-- Listen to the 'click' event using a delegated event listener (only works with bubbling events) --><buttonclick.delegate="onClick()"><!-- Directly work with the event using the `$event` magic property --><formsubmit.trigger="$event.preventDefault()"><!-- Set this html element to the 'nameInput' property on the declaring component --><inputref="nameInput"><!-- Set the component instance of this <my-component> custom element to the 'myComponentViewModel' property on the declaring component --><my-componentcomponent.ref="myComponentViewModel"><!-- Automatic two-way binding to an input (convention, equivalent to value.two-way) --><inputvalue.bind="name"><!-- Manual two-way binding to an input --><inputvalue.to-view="name"change.trigger="handleNameChange($event.target.value)"><!-- Debounce the notification when input value change for 200ms --><inputvalue.bind="name & debounce"><!-- Debounce the notification when input value change for 500ms --><inputvalue.bind="name & debounce:500"><!-- Throttle the notification when input value change for 200ms --><inputvalue.bind="name & throttle"><!-- Throttle the notification when input value change for 500ms --><inputvalue.bind="name & throttle:500"><!-- Render the 'message' property (non-reactive) --><div>${message & oneTime}</div><!-- Render the 'message' property (when the 'update' signal is sent manually) --><div>${message & signal:'update'}</div><!-- Render the 'timestamp' property formatted by the 'formatDate' ValueConverter --><div>${timestamp | formatDate}</div>
<!-- Conditionally render this nav-link (element is not created and will not exist in DOM if false) --><nav-linkif.bind="isLoggedIn"><!-- Conditionally display this nav-link (element is only hidden via CSS if false, and will always be created and exist in DOM) -->
<nav-linkshow.bind="isLoggedIn"><!-- Conditionally hide this nav-link (element is only hidden via CSS if true, and will always be created and exist in DOM) -->
<nav-linkhide.bind="isAnonymous"><!-- Conditionally render one thing or the other --><pif.bind="product.stockQty > 0">${product.stockQty}</p><pelse>Out of stock!</p><!-- Conditionally render one or more out of several things based on a condition --><templateswitch.bind="status"> <spancase="received">Order received.</span> <spancase="dispatched">On the way.</span> <spancase="processing">Processing your order.</span> <spancase="delivered">Delivered.</span></template><!-- Render a list of items --><divrepeat.for="item of items">${item}</div><!-- Render something on a different location in the DOM --><modal-dialogportal="app-root"><!-- TODO: au-slot --><!-- TODO: with -->
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.
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.
exportclassApiClient {asyncgetProducts(filter) { /* ... */ }}exportinterfaceIApiClientextendsApiClient {}exportconstIApiClient=DI.createInterface<IApiClient>('IApiClient', x =>x.singleton(ApiClient));
No default (more loosely coupled)
exportinterfaceIApiClient {getProducts(filter):Promise<Product[]>;}exportconstIApiClient=DI.createInterface<IApiClient>('IApiClient');// Needs to be explicitly registered with the container when no default is providedcontainer.register(Registration.singleton(IApiClient, ApiClient));
Injecting an interface
// Note: in the future there will be a convention where the decorator is no longer necessary with interfacesexportclassMyComponent {private api:IApiClient=resolve(IApiClient);}// Once the convention is in place:exportclassMyComponent {constructor(private api:IApiClient) {}}
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.
Registration.singleton(key, SomeClass); // Single container-wide instanceRegistration.transient(key, SomeClass); // New instance per injectionRegistration.callback(key, (container) =>/* some factory fn */); // Callback invoked each timeRegistration.cachedCallback(key, (container) =>/* some factory fn */); // Callback invoked only once and then cachedRegistration.aliasTo(originalKey, aliasKey);Registration.instance(key, someInstance);
Decorating classes
// Registers in the root container@singletonexportclassSomeClass {}// Registers in the requesting container@singleton({ scoped:true })exportclassSomeClass {}@transientexportclassSomeClass {}
Customizing injection
exportclassMyLogger {// Resolve all dependencies associated with a key (zero to many)private sinks:ISink[] =resolve(all(ISink));}exportclassMyComponent {// Resolve a factory function that returns the dependency when calledprivategetFoo: () =>IFoo=resolve(lazy(IFoo));doStuff() {constfoo=this.getFoo(); }}exportclassMyComponent {// Explicitly opt-out of DI for one or more parameters (if default parameters must be respected)constructor(@ignore private name:string='my-component') {}}exportclassMyComponent {// Yield undefined (instead of throwing) if no registration existsprivate foo:IFoo=resolve(optional(IFoo));}exportclassMyComponent {// Explicitly create a new instance, even if the key is already registered as a singletonprivate foo:IFoo=resolve(newInstanceOf(IFoo));}// Extend Window type for custom added properties or e.g. third party libraries like Redux DevTools which do so, yet inject the actual window object
exportinterfaceIReduxDevToolsextendsWindow { devToolsExtension?:DevToolsExtension; __REDUX_DEVTOOLS_EXTENSION__?:DevToolsExtension;}exportclassMyComponent {// Note that the type itself is not responsible for resolving the proper key but the decoratorprivate window:IReduxDevTools=resolve(IWindow);}
Using lifecycle hooks in a non-blocking fashion but keeping things awaitable
Example that blocks rendering (but is simplest to develop)
exportclassMyComponent {loading(params) {this.loadDataTask =PLATFORM.taskQueue.queueTask(async () => {this.data =awaitloadData(params.id);this.loadDataTask =null; }, { suspend:true/* Rendering proceeds, but no new tasks are run until this one finishes */ }); }}
Apply the practices above consistently, and you'll reap the benefits:
// Await all pending tasks, sync or async (useful in unit, integration and e2e tests or generally figuring out when the app is "idle")
awaitPromise.all([PLATFORM.taskQueue.yield(),platform.domQueue.yield(),]);
In the future, time-slicing will be enabled via these TaskQueue APIs as well, which will allow you to easily chunk work that's been dispatched via the task queues.
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:
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:
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:
export const IStorageClient = DI.createInterface<IStorageClient>('IStorageClient', x => x.singleton(LocalStorageClient));
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.