Skip to content

Aurelia Composition — Dynamic Component Loading

DodaTech Updated 2026-06-28 5 min read

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

  1. Not handling component load errors. If the compose path is wrong, the component silently fails. Provide error handling with a fallback component.
  2. Passing mutable data without cloning. If the composed component mutates the model, the original data changes. Pass clones for read-only data.
  3. Forgetting to handle empty/null models. Check that model exists before accessing properties in the composed component.
  4. Overusing compose for everything. Static includes are faster. Only use compose when the component type is truly dynamic.
  5. Not unsubscribing from events in composed components. Composed components follow the same lifecycle. Clean up in detached.

Practice Questions

  1. What does the <compose> element do?
  2. How do you pass data to a dynamically loaded component?
  3. How do you choose which component to load based on data?
  4. What lifecycle hooks does a composed component receive?
  5. 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\"" >}}

Does compose support lazy loading?

Yes. Components are loaded when compose renders. No loaders are needed.

Can I use compose without a view-model?

Yes. Provide a view attribute for a template-only composition.

How do I handle errors in compose?

Wrap compose in a try-catch or register a global error handler.

Can compose access the parent scope?

Yes. The composed component inherits the parent binding scope unless you provide a model.

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