Dynamic composition
Render components and templates dynamically with Aurelia's au-compose element.
Dynamic composition lets you decide what to render at runtime instead of at compile time. Think of <au-compose> as a placeholder that can become any component or template based on your application's state, user preferences, or data.
Use it for:
Dashboard widgets that change based on user configuration
Conditional components where you need to render different components based on data
Plugin architectures where components are loaded dynamically
Form builders that render different field types
Content management where layout components vary by content type
Before you start: Make sure you are comfortable with components and template controllers; both concepts show up throughout the examples.
Quick Reference
component
Registered element name (string), custom element class/definition, plain object/class, or Promise resolving to one of those values
undefined
Chooses what to render; strings must match a globally or locally registered custom element or Aurelia will throw.
template
Literal HTML string or Promise<string>
undefined
Provides markup for template-only composition or for a non-custom-element component object/class. Ignored when component resolves to a custom element.
model
Any value
undefined
Passed into the composed instance's activate(model) hook. Updating model re-runs activate without recreating the component; it does not spread model properties automatically.
scope-behavior
'auto' | 'scoped'
'auto'
Controls scope inheritance for non-custom-element compositions, including template-only compositions and templates backed by plain objects. Has no effect for custom elements.
tag
string | null
null (containerless)
For non-custom-element compositions, provide a tag name when you need a surrounding element; leave as null to keep the default comment boundaries. Ignored for custom elements.
composition
ICompositionController (from-view)
undefined
Exposes the current composition so you can read composition.controller.viewModel or call composition.update(model). <au-compose> remains responsible for deactivation and disposal.
composing
Promise<void> | void (from-view)
undefined
Exposes the Promise for the current asynchronous structural composition. Use it to drive loading UI or await component or template replacement.
flush-mode
'sync' | 'async'
'sync'
Controls whether composition updates are applied immediately or batched into the next change-processing turn.
Tip: Bindings placed on
<au-compose>that match bindables on a composed custom element are forwarded to that element. Other attributes are applied to the generated host element when one exists, unless the custom element captures them withcapture/...$attrs.
Component Composition
Composing with Custom Element Definitions
You can compose any custom element by passing its definition to the component property. Define those elements once at module scope so Aurelia reuses the same definition instead of recreating it for every view-model instance:
Composing with Component Names
If you have components registered globally or locally, you can reference them by name:
Template-Only Composition
Sometimes you need to render dynamic HTML without a full component. Template-only composition covers that case:
Basic Template Composition
Dynamic Templates with Data
Template with Component Object
You can combine a template with a simple object that provides data and methods:
Controlling the Host Element
Default Host Behavior
When you compose a custom element, Aurelia creates that element as the host. For template-only and plain-object compositions, Aurelia does not create a wrapper element by default:
The rendered DOM contains the two <span> elements plus Aurelia's render-location comments, not an extra <div> wrapper.
Creating a Host Element with tag
When you need a wrapper element around non-custom-element composed content, use the tag property:
This renders as:
For non-custom-element compositions, attributes you put on <au-compose> (like class, style, or event handlers) are transferred only when tag creates a real host element. Without tag, there is no host element to receive them.
Practical Host Element Example
Passing Data with Models and the Activate Method
Understanding the Activate Lifecycle
Composed components can implement an activate method that runs when the component is created and whenever the model changes. Use it for initialization and data updates:
Model-only changes invoke activate(model) directly. The component owns any Promise started there, including its cancellation and ordering policy.
Using Models for Data Passing
Model Updates vs Component Changes
Important distinction: changing the model doesn't recreate the component, it just calls activate() again. This is efficient for data updates:
Advanced Features
Promise Support
Both template and component properties can accept promises, which keeps lazy loading straightforward:
Scope Behavior Control
For non-custom-element compositions, you can control whether they inherit the parent scope:
Plain object components and missing properties
When component is a plain object and the composed template is not a custom element, the default scope-behavior="auto" keeps the parent scope visible. This is convenient when a template intentionally reads parent properties, but it can surprise you if the plain object is replaced with one that does not contain every property used by the template.
After replaceData(), y resolves on the new data object. The template also asks for x, but x is missing from data; with auto scoping Aurelia can resolve that failed lookup against the nearest parent scope instead. Later changes to data.x will not update a binding that was already connected to the parent.
Use scope-behavior="scoped" when the composed template should stay anchored to the plain object:
Another safe option is to keep the object shape complete whenever you replace it:
Custom-element composition does not have this fallback behavior because composed custom elements are scoped by default.
Accessing the Composition Controller
Use the composition property to access the composed component's controller:
Tracking Composition Progress
Bind composing when you want to show a loading state or await structural composition. Aurelia sets it while asynchronous component or template loading and the corresponding controller transition are underway.
Synchronous and Asynchronous Updates
By default, <au-compose> reacts to component, template, tag, and scope-behavior changes synchronously. Set flush-mode="async" when several inputs can change together and you want Aurelia to batch the composition update into the next change-processing turn:
A model-only update calls activate(model) on the current composition and retains that instance. Any returned Promise remains component-owned and does not enter composing.
Component and template changes use the structural composition queue. When that work becomes asynchronous, composing represents the queue until it settles. On successful detachment, Aurelia lets the current structural composition finish, disposes the controllers it created, and drops structural changes that are still waiting.
Real-World Examples
Form Builder with Dynamic Fields
Plugin Architecture with Dynamic Loading
Each plugin component should read plugin.config in its activate(model) hook, or expose explicit bindables and pass those bindables on <au-compose>.
Content Management with Dynamic Layouts
Only bind trusted or sanitized HTML into innerHTML; dynamic composition compiles templates, but it does not make unsafe content safe.
Migrating from Aurelia 1
If you're upgrading from Aurelia 1, here are the key changes you need to know:
Property Name Changes
Aurelia 1:
Aurelia 2:
Component Reference Changes
Aurelia 1:
Aurelia 2:
Note:
component.refis forwarded to a composed custom element. Usecomposition.bindwhen you need the composition controller, or when the composed value might be a plain object, plain class, or template-only composition.
String Handling Changes
Aurelia 1:
Aurelia 2:
Scope Inheritance Changes
Aurelia 1:
Aurelia 2:
Migration Examples
Before (Aurelia 1):
After (Aurelia 2):
Dynamic Module Loading Migration
Aurelia 1:
Aurelia 2:
Value Converter Pattern for Remote Templates
If you need to load templates from URLs (like in Aurelia 1), create a value converter:
Only compile template strings that you trust. A remote string passed to template becomes an Aurelia template, not inert display text.
Common Migration Gotchas
String components:
component="..."is a registered element name, not a module path.Dynamic imports: bind a promise that resolves to the component export, not the whole module object.
Model data:
modelis passed toactivate(model); it is not spread across component properties.Binding transfer: bindings matching custom element bindables go to the component. Other attributes go to the host element or to
...$attrswhen the custom element captures them.Scope behavior: template-only and plain-object compositions use
autoscope by default; custom elements are scoped by default.Lifecycle: custom elements get their normal lifecycle. Plain objects and plain classes can use the dynamic composition
activate(model)hook.
Best Practices
Use promises for lazy loading - Return the component export from
import().then(...).Use
activate(model)for model data - Keep model normalization in one place instead of relying on property names to line up.Choose scope behavior intentionally - Use
scopedwhen a template should not fall back to parent properties,autowhen fallback is desired.Cache component definitions - Call
CustomElement.defineonce per module and reuse the reference instead of redefining inside constructors.Handle loading states - Bind to
composingto show a spinner or disable UI while an asynchronous structural composition settles.Use models efficiently - Changing models is cheaper than switching components because
activate(model)re-runs without rehydration.
Next steps
Explore portalling elements to move DOM across layout boundaries.
Combine composition with enhance when progressively upgrading existing markup.
Review watching data to react to model changes that drive composition.
Last updated
Was this helpful?