Building plugins

Aurelia makes it easy to create your plugins. Learn how to create individual plugins, register them, and work with tasks to run code during certain parts of the lifecycle process.

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.

Aurelia plugins allow you to encapsulate functionality that can be reused across multiple applications. They can include custom elements, value converters, binding behaviors, and other resources. The goal is to create packaged, easily shared, ready-to-use functionalities that integrate seamlessly with Aurelia applications.

Understanding Plugin Architecture

At its core, an Aurelia plugin is an object with a register method that configures dependencies and sets up your functionality for use in an Aurelia application. This follows the dependency injection pattern that powers the entire Aurelia framework.

Minimal Plugin Example

Basic Plugin Structure

// my-simple-plugin.ts
import { IContainer } from '@aurelia/kernel';

export const MySimplePlugin = {
  register(container: IContainer): void {
    // Register your plugin resources here
    console.log('Plugin registered!');
  }
};

Registering the Plugin

Adding Components and Resources

Plugins typically provide custom elements, attributes, value converters, or other resources:

Creating Configurable Plugins

Modern Configuration Pattern with .customize()

The modern approach uses a .customize() method that follows the same pattern as Aurelia's built-in plugins:

Using the Configurable Plugin

Consuming Configuration in Components

Working with App Tasks (Lifecycle Hooks)

App tasks allow you to run code at specific points during the application lifecycle. This is useful for initialization, cleanup, or integration with external libraries.

Available Lifecycle Phases

Async App Tasks with DI

App tasks can be asynchronous and can inject dependencies:

Real-World Plugin Examples

Router-like Plugin Structure

Here's how a plugin similar to Aurelia's router might be structured:

Validation-like Plugin Structure

Here's how a validation plugin might be structured:

Template and Style Handling in Plugins

When building plugins with custom elements, you need to decide how to handle templates and styles. Aurelia provides multiple approaches, each suited for different scenarios.

Convention-Based Approach

The convention-based approach relies on file naming and automatic bundler processing. This is ideal for application development but requires specific bundler configuration.

File Structure

Component Definition

Template File

Styles File

Explicit Import Approach

The explicit import approach gives you full control over dependencies and is ideal for UI libraries and distribution packages.

Styling Strategies

Light DOM (Global Styles)

Regular CSS injection into the document head:

Shadow DOM with CSS

Encapsulated styles using Shadow DOM:

CSS Modules

Scoped class names for style isolation:

Multiple Template Support

For components with different template variants:

Inline Templates and Styles

For simple components, you can define templates and styles inline:

Or with CSS-in-JS approach:

When to Use Each Approach

Use Convention-Based When:

  • Building application-specific plugins

  • Working in a controlled bundler environment

  • Want minimal boilerplate code

  • Following standard Aurelia project structure

Use Explicit Imports When:

  • Building UI libraries for distribution

  • Need precise control over dependencies

  • Supporting multiple bundler configurations

  • Want explicit dependency management

  • Building plugins for npm distribution

TypeScript Support

For explicit imports, provide proper TypeScript declarations:

Plugin Template Recommendations

For UI library plugins, structure your components like this:

This approach provides maximum flexibility for consumers while maintaining clean plugin architecture.

Advanced Plugin Patterns

Conditional Resource Registration

Plugin with Dynamic Imports

Plugin Composition

Best Practices

Naming Conventions

  • Plugin Names: Use descriptive names ending with "Plugin" or "Configuration"

  • Interfaces: Prefix with "I" and use DI.createInterface()

  • Resources: Prefix with your plugin name to avoid collisions

Error Handling

TypeScript Integration

Testing Plugins

Unit Testing Plugin Registration

Packaging and Distribution

Package Structure

Package.json Configuration

Mono-Repository Setup

For complex plugins with multiple packages, consider using a workspace:

Directory structure:

This setup allows you to:

  • Share common dependencies

  • Cross-reference packages easily

  • Build and test everything together

  • Publish individual packages as needed

Extending the Rendering Pipeline

Public exports from @aurelia/runtime-html make it possible to hook into Aurelia’s renderer without forking the framework. This is essential for plugins that add new template syntax, hydrate components into foreign DOM environments, or need to precompile custom elements on the fly.

Register Custom Instruction Renderers

IRenderer implementations are discovered through the @renderer decorator. Each renderer receives the instruction at hydration time, so you can translate custom compiler output into DOM mutations.

Once registered (for example via Aurelia.register(TranslateRenderer) or StandardConfiguration.customize({ resources: [TranslateRenderer] })), any instruction whose type matches TranslateRenderer.target is routed through your plugin. Combine this with a binding command or template compiler hook to emit translate instructions from HTML like <span t="nav.home"></span>.

Use IRendering for Dynamic Definitions

Plugins that generate PartialCustomElementDefinition objects at runtime can inject IRendering to compile them, create node sequences, or materialize ViewFactory instances on demand.

IRendering.createNodes() returns reusable FragmentNodeSequence instances, letting you build headless renderers that project Aurelia content into other host libraries without going through standard component bootstrapping.

Provide Host Nodes via registerHostNode

If you hydrate Aurelia components into DOM nodes created by another framework (for example, inside a CMS widget or micro-frontend shell), call registerHostNode(container, host) so that DI can resolve HTMLElement, Element, or INode within that scope.

This mirrors what the runtime does for the default app host and keeps resource lookups such as resolve(IEventTarget) or resolve(IPlatform).HTMLElement working even when the DOM comes from an embedded document or shadow root.

Summary

Building Aurelia plugins involves:

  1. Basic Structure: Implement the register method pattern

  2. Configuration: Use DI.createInterface() and the .customize() pattern

  3. Lifecycle Integration: Leverage AppTask for initialization and cleanup

  4. Rendering Extensions: Reach for IRenderer, IRendering, and registerHostNode when you need custom instructions or alternate hosts

  5. Best Practices: Follow naming conventions, handle errors gracefully, and provide TypeScript support

  6. Testing: Write comprehensive tests for registration and functionality

  7. Distribution: Package properly for npm distribution

By following these patterns and practices, you can create robust, reusable plugins that integrate seamlessly with the Aurelia ecosystem.

Last updated

Was this helpful?