Aurelia Composition — Dynamic Component Loading
In this tutorial, you will learn about Aurelia Composition. We cover key concepts, practical examples, and best practices to help you master this topic.
Aurelia's <compose> element dynamically loads and renders components at runtime. It accepts a view-model path and model data, instantiating the component and passing data. This enables dynamic dashboards, plugin systems, and content-driven layouts.
What You'll Learn
You will learn how to use <compose> for dynamic component loading, pass model data, bind context, handle composition lifecycle, and build configurable dashboards.
Why It Matters
Static templates cannot handle every scenario. Dynamic composition lets you load components based on data, user roles, or configuration. A dashboard widget grid is impossible without dynamic composition.
Real-World Use
A reporting dashboard lets users add, remove, and rearrange widgets. Each widget type has its own component. The dashboard configuration (which widgets, what data) is stored in user preferences. <compose> renders each widget dynamically.
flowchart LR
A[Compose element] --> B[view-model.bind]
A --> C[model.bind]
B --> D[Component Loader]
D --> E[Dynamic Component]
E --> F[View + ViewModel]
C --> F
F --> G[Rendered Output]
Basic Compose
<template>
<!-- Compose a component by module path -->
<compose view-model="./shared/user-card"></compose>
<!-- Compose with model data -->
<compose view-model="./shared/user-card"
model.bind="selectedUser">
</compose>
<!-- Compose with inline view -->
<compose view="./shared/partial-view.html"></compose>
</template>
Dynamic Component Selection
<template>
<!-- Component type determined by data -->
<div repeat.for="widget of dashboardWidgets">
<compose view-model.bind="widget.component"
model.bind="widget.data">
</compose>
</div>
</template>
export class Dashboard {
dashboardWidgets = [
{
type: 'chart',
component: './widgets/line-chart',
data: { title: 'Revenue', labels: [...], values: [...] }
},
{
type: 'table',
component: './widgets/data-table',
data: { columns: [...], rows: [...] }
},
{
type: 'metric',
component: './widgets/metric-card',
data: { label: 'Active Users', value: 1243, trend: '+12%' }
}
];
addWidget(type) {
const widgetConfigs = {
chart: { component: './widgets/line-chart', data: { title: 'New Chart' } },
table: { component: './widgets/data-table', data: { columns: [], rows: [] } },
metric: { component: './widgets/metric-card', data: { label: 'New Metric', value: 0 } }
};
this.dashboardWidgets.push({ ...widgetConfigs[type] });
}
removeWidget(index) {
this.dashboardWidgets.splice(index, 1);
}
}
Passing Complex Models
// src/widgets/line-chart.ts
import { bindable } from 'aurelia-framework';
export class LineChart {
@bindable data;
dataChanged(newValue) {
if (newValue) {
this.renderChart(newValue);
}
}
renderChart(data) {
console.log('Rendering chart:', data.title);
// Chart rendering logic
}
}
Compose with Binding Context
<template>
<!-- Bind to current context -->
<compose view-model="./shared/header"
model.bind="$this">
</compose>
<!-- Pass binding context object -->
<compose view-model="./shared/footer"
model.bind="{ company: 'Acme', year: 2026 }">
</compose>
</template>
Dynamic Form Builder
export class FormBuilder {
formFields = [
{ type: 'text', component: './fields/text-field', config: { label: 'Name', required: true } },
{ type: 'email', component: './fields/email-field', config: { label: 'Email' } },
{ type: 'select', component: './fields/select-field', config: { label: 'Country', options: [...] } },
{ type: 'checkbox', component: './fields/checkbox-field', config: { label: 'Subscribe' } },
{ type: 'date', component: './fields/date-field', config: { label: 'Birth Date' } }
];
addField(type) {
const fieldTemplates = {
text: { component: './fields/text-field', config: { label: 'New Field' } },
textarea: { component: './fields/textarea-field', config: { label: 'Description' } },
number: { component: './fields/number-field', config: { label: 'Quantity' } }
};
this.formFields.push({ ...fieldTemplates[type] });
}
getFormData() {
// Collect values from all field components
return this.formFields.reduce((data, field) => {
data[field.config.label] = field.value;
return data;
}, {});
}
}
Layout Switching with Compose
<template>
<!-- Switch layout based on screen size or preference -->
<compose view-model.bind="layoutComponent"
model.bind="pageData">
</compose>
<button click.delegate="toggleLayout()">
Switch to ${currentLayout === 'grid' ? 'list' : 'grid'} view
</button>
</template>
export class ProductPage {
currentLayout = 'grid';
pageData = { products: [...] };
get layoutComponent() {
return this.currentLayout === 'grid'
? './layouts/grid-layout'
: './layouts/list-layout';
}
toggleLayout() {
this.currentLayout = this.currentLayout === 'grid' ? 'list' : 'grid';
}
}
Plugin System with Compose
// src/plugins/plugin-manager.ts
export class PluginManager {
plugins = [];
registerPlugin(plugin) {
this.plugins.push(plugin);
}
getActivePlugins(context) {
return this.plugins.filter(p => p.isActive && p.matchesContext(context));
}
}
<template>
<div class="plugin-area">
<div repeat.for="plugin of pluginManager.getActivePlugins(currentContext)">
<compose view-model.bind="plugin.component"
model.bind="plugin.getData(currentContext)">
</compose>
</div>
</div>
</template>
Compose Lifecycle
// src/widgets/example-widget.ts
import { bindable } from 'aurelia-framework';
export class ExampleWidget {
@bindable data;
constructor() {
console.log('Widget constructor');
}
bind(bindingContext) {
console.log('Widget bind', bindingContext);
}
attached() {
console.log('Widget attached to DOM');
}
dataChanged(newValue, oldValue) {
console.log('Widget data changed', newValue);
}
detached() {
console.log('Widget detached from DOM');
}
unbind() {
console.log('Widget unbound');
}
}
Tabbed Interface with Compose
<template>
<div class="tabs">
<div class="tab-headers">
<button repeat.for="tab of tabs"
click.delegate="activeTab = tab"
class="${activeTab === tab ? 'active' : ''}">
${tab.label}
</button>
</div>
<div class="tab-content">
<compose view-model.bind="activeTab.component"
model.bind="activeTab.data">
</compose>
</div>
</div>
</template>
Common Mistakes
- Not handling component load errors. If the compose path is wrong, the component silently fails. Provide error handling with a fallback component.
- Passing mutable data without cloning. If the composed component mutates the model, the original data changes. Pass clones for read-only data.
- Forgetting to handle empty/null models. Check that model exists before accessing properties in the composed component.
- Overusing compose for everything. Static includes are faster. Only use compose when the component type is truly dynamic.
- Not unsubscribing from events in composed components. Composed components follow the same lifecycle. Clean up in
detached.
Practice Questions
- What does the
<compose>element do? - How do you pass data to a dynamically loaded component?
- How do you choose which component to load based on data?
- What lifecycle hooks does a composed component receive?
- Challenge: Build a plugin-based dashboard where users can add, remove, and configure widgets. Each widget type has its own configuration component. Use compose to render widgets and their configuration forms. Store the dashboard layout in localStorage.
FAQ
{{< faq "Can compose load components from external packages?" "Yes. Use the npm package path as the view-model: view-model=\"my-package/my-component\"" >}}
Mini Project
Build a configurable dashboard with these features: (1) Default layout with chart, table, metric, and activity feed widgets. (2) Add widget dropdown with type selection. (3) Remove widget button on each widget. (4) Drag-and-drop widget reordering. (5) Save/load layout from localStorage. (6) Each widget component is loaded via <compose>.
What's Next
Now that you understand composition, learn Aurelia Dependency Injection for service management. Then explore Aurelia Routing for navigation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro