Framework internals
Aurelia's instruction system is the bridge between template compilation and runtime execution. Templates compile to instruction objects that describe exactly what bindings and components to create, then renderers interpret these instructions to build the actual DOM bindings and component instances.
From Template to Runtime
Let's follow a simple template through the complete pipeline:
Template
<my-element value.bind="name" click.trigger="doSomething()">
<p>${message}</p>
</my-element>
Compilation Phase
The template compiler processes this into instruction arrays:
const instructions = [
[ // Instructions for <my-element>
new HydrateElementInstruction(
'my-element', // Element name/definition
[ // Property instructions
new PropertyBindingInstruction('name', 'value', BindingMode.toView),
new ListenerBindingInstruction('doSomething()', 'click', false, null)
],
null, // Projections
false, // Containerless
[], // Captures
{} // Data
)
],
[ // Instructions for <p> inside my-element
new InterpolationInstruction('message', 'textContent')
]
];
Runtime Phase
At runtime, renderers execute these instructions:
CustomElementRenderer creates the
my-element
component instancePropertyBindingRenderer creates a binding from
name
to thevalue
propertyListenerBindingRenderer creates a click event listener
InterpolationRenderer creates a text binding for
${message}
All bindings are added to the component's controller and activated during the binding lifecycle.
Instruction Types and What They Do
Component Creation
hydrateElement
- Creates custom element instanceshydrateAttribute
- Creates custom attribute instanceshydrateTemplateController
- Creates template controllers (if
,repeat
, etc.)
Data Binding
propertyBinding
- Property bindings (.bind
,.two-way
, etc.)interpolation
- Text interpolation${...}
attributeBinding
- Attribute bindings (.attr
)stylePropertyBinding
- Style bindings (.style
)
Event Handling
listenerBinding
- Event listeners (.trigger
,.delegate
,.capture
)
Static Values
setProperty
- Static property valuessetAttribute
- Static attribute valuessetClassAttribute
- Static class values
Advanced Features
refBinding
- Reference bindings (ref
attribute)letBinding
- Let bindings (let
element)iteratorBinding
- Iterator bindings (forrepeat
)
Debugging with Instructions
Inspecting Compiled Instructions
You can examine compiled instructions in the browser devtools:
// In a component's constructor or attached() lifecycle
export class MyComponent {
constructor() {
// Access the compiled definition
const definition = CustomElement.getDefinition(this.constructor);
console.log('Compiled instructions:', definition.instructions);
}
}
Understanding Instruction Arrays
Instructions are organized as arrays where each array corresponds to a DOM target:
// instructions[0] = instructions for the first target element
// instructions[1] = instructions for the second target element
// etc.
Debugging Binding Issues
When bindings don't work as expected:
Check what instructions were generated
Verify the instruction properties match your template
Look for missing or incorrect binding modes
Check if custom elements/attributes were resolved properly
// Example: Debug why a binding isn't working
const instructions = definition.instructions[0]; // First target
const propertyBinding = instructions.find(i => i.type === 'rg'); // propertyBinding type
console.log('Binding from:', propertyBinding.from);
console.log('Binding to:', propertyBinding.to);
console.log('Binding mode:', propertyBinding.mode);
Common Template Patterns
Basic Property Binding
<input value.bind="name">
Compiles to:
new PropertyBindingInstruction('name', 'value', BindingMode.twoWay)
Event Binding
<button click.trigger="save()">Save</button>
Compiles to:
new ListenerBindingInstruction('save()', 'click', false, null)
String Interpolation
<span>${firstName} ${lastName}</span>
Compiles to:
new InterpolationInstruction('`${firstName} ${lastName}`', 'textContent')
Custom Element with Bindables
<user-card name.bind="user.name" age.bind="user.age"></user-card>
Compiles to:
new HydrateElementInstruction(
'user-card',
[
new PropertyBindingInstruction('user.name', 'name', BindingMode.toView),
new PropertyBindingInstruction('user.age', 'age', BindingMode.toView)
],
null, false, [], {}
)
Template Controller (Repeater)
<div repeat.for="item of items">${item.name}</div>
Compiles to:
new HydrateTemplateController(
definition, // Template controller definition
'repeat', // Resource name or definition
undefined, // Alias
[new IteratorBindingInstruction('item of items', 'items', [])]
)
Extending the System
Creating Custom Instructions
For advanced scenarios, you can create custom instruction types:
export class CustomInstruction {
public static readonly type = 'my-custom-instruction';
constructor(
public readonly config: any,
public readonly target: string
) {}
}
Creating Custom Renderers
Custom renderers interpret your custom instructions:
import { renderer } from '@aurelia/runtime-html';
export const CustomRenderer = renderer(class {
public readonly target = 'my-custom-instruction';
public render(
renderingCtrl: IHydratableController,
target: unknown,
instruction: CustomInstruction,
platform: IPlatform,
exprParser: IExpressionParser,
observerLocator: IObserverLocator,
): void {
// Your custom rendering logic
const binding = new CustomBinding(/* ... */);
renderingCtrl.addBinding(binding);
}
});
Registering Custom Renderers
Register your renderer during app startup:
import Aurelia from 'aurelia';
import { CustomRenderer } from './custom-renderer';
Aurelia
.register(CustomRenderer)
.app(App)
.start();
Resource Registration and Discovery
Before instructions can reference resources like custom elements, Aurelia needs to discover and register them. The framework supports multiple registration patterns.
Resource Registration Patterns
Decorator-Based Registration
@customElement('user-card')
export class UserCard {
@bindable name: string;
@bindable age: number;
}
The decorator automatically:
Creates a resource definition with metadata
Registers the resource in the DI container with key:
"au:resource:custom-element:user-card"
Stores the definition for later compilation use
Static $au
Property Registration
$au
Property Registrationexport class UserCard {
static readonly $au: CustomElementStaticAuDefinition = {
type: 'custom-element',
name: 'user-card',
bindables: {
name: { mode: BindingMode.toView },
age: { mode: BindingMode.toView }
}
};
}
Convention-Based Registration
With the conventions plugin, file names automatically determine resource names:
src/components/user-card.ts → resource name: "user-card"
src/components/userName.ts → resource name: "user-name"
Resource Resolution During Compilation
When the template compiler encounters <user-card>
, it:
Looks up the resource using
CustomElement.find(container, 'user-card')
Resolves the definition from the DI container key
"au:resource:custom-element:user-card"
Creates instruction with either the resource name string or the resolved definition
Caches the result to avoid repeated lookups
// Template compiler resource resolution
export class ResourceResolver {
public el(container: IContainer, name: string): CustomElementDefinition | null {
// Check cache first, then resolve from container
return this._cache[name] ?? (this._cache[name] = CustomElement.find(container, name));
}
}
Resource Resolution During Runtime
At runtime, the CustomElementRenderer processes HydrateElementInstruction
:
export const CustomElementRenderer = renderer(class {
public render(/* ... */, instruction: HydrateElementInstruction) {
// Resolve resource definition if not already resolved
const definition = instruction.def ??
renderingCtrl.container.find(CustomElement, instruction.res);
// Create component instance using DI
const component = renderingCtrl.container.invoke(definition.Type);
// Create controller and add to hierarchy
const controller = Controller.createForCustomElement(/* ... */);
renderingCtrl.addChild(controller);
}
});
Container Hierarchy and Resource Scope
Resources are registered in DI containers, which form hierarchies:
// Root container - global resources
const rootContainer = DI.createContainer();
rootContainer.register(GlobalButton, GlobalModal);
// Child container - page-specific resources
const pageContainer = rootContainer.createChild();
pageContainer.register(PageSpecificCard);
// Component container - component-specific resources
const componentContainer = pageContainer.createChild();
Resource resolution follows the hierarchy:
Check current container
Check parent containers up to root
Return null if not found
Debugging Resource Registration
Check what resources are registered:
// In a component
export class MyComponent {
constructor(private container: IContainer) {
// Check if a resource is registered
const definition = this.container.find(CustomElement, 'user-card');
console.log('UserCard registered:', definition !== null);
// Access registration details
if (definition) {
console.log('Resource key:', definition.key);
console.log('Resource type:', definition.Type);
console.log('Bindables:', definition.bindables);
}
}
}
Resource Registration Best Practices
Use decorators for explicit control over resource configuration
Use static
$au
properties when decorators aren't suitable (e.g., plain classes)Use conventions for rapid development with consistent naming
Register global resources at app startup in
main.ts
Register page-specific resources in route components or modules
Understanding Aurelia's instruction system and resource registration gives you deeper insight into how templates become living, reactive UIs and provides the foundation for advanced framework extensions.
Last updated
Was this helpful?