Extending the template compiler

The Aurelia template compiler is highly extensible, providing multiple hooks and extension points for advanced customization. This guide covers the advanced features and extension mechanisms available for developers who need to extend template compilation behavior.

Template Compiler Hooks

Registering Compilation Hooks

Template compiler hooks allow you to modify templates during the compilation process:

import { templateCompilerHooks, ITemplateCompilerHooks } from 'aurelia';

@templateCompilerHooks
class MyCompilerHook implements ITemplateCompilerHooks {
  compiling(template: HTMLElement): void {
    // Modify template before compilation
    this.addDefaultAttributes(template);
    this.injectDevelopmentHelpers(template);
  }

  private addDefaultAttributes(template: HTMLElement): void {
    // Add default attributes to form elements
    template.querySelectorAll('input[type="text"]').forEach(input => {
      if (!input.hasAttribute('autocomplete')) {
        input.setAttribute('autocomplete', 'off');
      }
    });
  }

  private injectDevelopmentHelpers(template: HTMLElement): void {
    if (__DEV__) {
      // Add development-only attributes
      template.querySelectorAll('[data-dev-hint]').forEach(el => {
        el.setAttribute('title', el.getAttribute('data-dev-hint')!);
      });
    }
  }
}

Global vs Component-Level Hooks

Hooks can be registered globally or at the component level:

// Global hook registration
container.register(MyCompilerHook);

// Component-level hook
@customElement({
  name: 'my-component',
  template: '<div>...</div>',
  hooks: [MyCompilerHook]
})
export class MyComponent { }

Advanced Attribute Pattern System

Creating Custom Attribute Syntax

The attribute pattern system allows you to create custom binding syntax:

import { attributePattern, AttrSyntax } from 'aurelia';

@attributePattern({ pattern: 'PART.vue:PART', symbols: '.:' })
class VueStyleAttributePattern {
  'PART.vue:PART'(rawName: string, rawValue: string, parts: string[]): AttrSyntax {
    const [target, event] = parts;
    return new AttrSyntax(rawName, rawValue, target, 'trigger', [event]);
  }
}

// Usage: <button click.vue:prevent="handleClick()">

Complex Pattern Matching

Support for multi-part patterns with custom symbols:

@attributePattern({ pattern: 'PART.PART.PART', symbols: '.' })
class NestedPropertyPattern {
  'PART.PART.PART'(rawName: string, rawValue: string, parts: string[]): AttrSyntax {
    const [obj, prop, command] = parts;
    return new AttrSyntax(rawName, rawValue, `${obj}.${prop}`, command, parts);
  }
}

// Usage: <input user.profile.bind="userProfile">

Custom Binding Commands

Advanced Binding Command Features

Binding commands can take full control of attribute processing:

import { bindingCommand, BindingCommandInstance, IInstruction } from 'aurelia';

@bindingCommand('throttle')
class ThrottleBindingCommand implements BindingCommandInstance {
  ignoreAttr = true; // Take full control of attribute processing

  build(info: ICommandBuildInfo, parser: IExpressionParser): IInstruction {
    const [delay = '250', event = 'input'] = info.attr.rawValue.split(':');
    
    return new ThrottleInstruction(
      parser.parse(info.attr.rawValue),
      parseInt(delay, 10),
      event
    );
  }
}

// Usage: <input value.throttle="500:input">

Multi-Attribute Processing

Commands can process multiple attributes for complex scenarios:

@bindingCommand('form')
class FormBindingCommand implements BindingCommandInstance {
  build(info: ICommandBuildInfo, parser: IExpressionParser): IInstruction {
    const formAttributes = this.collectFormAttributes(info.attr.syntax.target);
    
    return new FormInstruction(
      parser.parse(info.attr.rawValue),
      formAttributes
    );
  }

  private collectFormAttributes(element: Element): Record<string, string> {
    const attrs: Record<string, string> = {};
    for (const attr of element.attributes) {
      if (attr.name.startsWith('form-')) {
        attrs[attr.name.substring(5)] = attr.value;
      }
    }
    return attrs;
  }
}

Template Element Factory Customization

Custom Template Caching

The template element factory supports custom caching strategies:

import { ITemplateElementFactory, IMarkupCache } from 'aurelia';

class CustomTemplateElementFactory implements ITemplateElementFactory {
  private customCache = new Map<string, HTMLTemplateElement>();

  createTemplate(markup: string): HTMLTemplateElement {
    // Custom caching logic
    const cacheKey = this.generateCacheKey(markup);
    
    if (this.customCache.has(cacheKey)) {
      return this.customCache.get(cacheKey)!.cloneNode(true) as HTMLTemplateElement;
    }

    const template = this.createTemplateElement(markup);
    this.customCache.set(cacheKey, template);
    return template;
  }

  private generateCacheKey(markup: string): string {
    // Custom cache key generation
    return `${markup.length}-${this.hashCode(markup)}`;
  }
}

Template Wrapping Detection

Customize how templates are wrapped for proper compilation:

class SmartTemplateFactory implements ITemplateElementFactory {
  createTemplate(markup: string): HTMLTemplateElement {
    const wrapped = this.intelligentWrap(markup);
    return this.createTemplateElement(wrapped);
  }

  private intelligentWrap(markup: string): string {
    // Custom wrapping logic based on content
    if (markup.includes('<tr>')) {
      return `<table><tbody>${markup}</tbody></table>`;
    }
    if (markup.includes('<option>')) {
      return `<select>${markup}</select>`;
    }
    return markup;
  }
}

Advanced Resource Resolution

Custom Resource Discovery

Implement custom resource resolution for dynamic components:

import { IResourceResolver, IResourceDescriptions } from 'aurelia';

class DynamicResourceResolver implements IResourceResolver {
  resolve(name: string, context: IContainer): IResourceDescriptions | null {
    // Check if this is a dynamic component request
    if (name.startsWith('dynamic-')) {
      return this.resolveDynamicComponent(name, context);
    }
    
    return null; // Let default resolver handle it
  }

  private resolveDynamicComponent(name: string, context: IContainer): IResourceDescriptions {
    const componentType = this.loadDynamicComponent(name);
    return {
      [name]: {
        type: componentType,
        keyFrom: name,
        definition: componentType.definition
      }
    };
  }
}

Bindables Information Caching

Optimize bindable resolution with custom caching:

class OptimizedResourceResolver implements IResourceResolver {
  private bindablesCache = new Map<Function, Record<string, BindableDefinition>>();

  getBindables(Type: Function): Record<string, BindableDefinition> {
    if (this.bindablesCache.has(Type)) {
      return this.bindablesCache.get(Type)!;
    }

    const bindables = this.computeBindables(Type);
    this.bindablesCache.set(Type, bindables);
    return bindables;
  }
}

Local Template System

Advanced Local Element Definitions

Create complex local element hierarchies:

@customElement({
  name: 'dashboard',
  template: `
    <template as-custom-element="widget">
      <bindable property="title"></bindable>
      <bindable property="data"></bindable>
      <div class="widget">
        <h3>\${title}</h3>
        <div class="content" innerhtml.bind="data"></div>
      </div>
    </template>
    
    <template as-custom-element="chart-widget">
      <bindable property="chart-data"></bindable>
      <widget title="Chart" data.bind="renderChart(chartData)"></widget>
    </template>
    
    <div class="dashboard">
      <chart-widget chart-data.bind="metrics"></chart-widget>
    </div>
  `
})
export class Dashboard {
  renderChart(data: any): string {
    return `<canvas data-chart="${JSON.stringify(data)}"></canvas>`;
  }
}

Dynamic Local Template Creation

Create local templates programmatically:

@customElement({
  name: 'dynamic-layout',
  template: `<div ref="container"></div>`
})
export class DynamicLayout {
  @ViewSlot() container!: ViewSlot;

  attached(): void {
    this.createLocalTemplate();
  }

  private createLocalTemplate(): void {
    const template = `
      <template as-custom-element="dynamic-item">
        <bindable property="item"></bindable>
        <div class="item">\${item.name}</div>
      </template>
    `;
    
    this.container.add(this.viewFactory.create(template));
  }
}

Compilation Context System

Hierarchical Resource Resolution

Work with compilation contexts for advanced scenarios:

class CustomCompiler {
  compileWithContext(template: string, parentContext?: ICompilationContext): ICompiledTemplate {
    const context = this.createCompilationContext(parentContext);
    
    // Add custom resources to context
    context.addResource('custom-element', MyCustomElement);
    context.addResource('value-converter', MyConverter);
    
    return this.compile(template, context);
  }

  private createCompilationContext(parent?: ICompilationContext): ICompilationContext {
    const context = new CompilationContext(parent);
    
    // Configure context for specific compilation needs
    context.resolveResources = true;
    context.debug = __DEV__;
    
    return context;
  }
}

Custom Dependency Injection

Customize DI container behavior during compilation:

class ScopedCompiler {
  compileWithScope(template: string, scope: Record<string, any>): ICompiledTemplate {
    const container = this.createScopedContainer(scope);
    const context = new CompilationContext(container);
    
    return this.compile(template, context);
  }

  private createScopedContainer(scope: Record<string, any>): IContainer {
    const container = DI.createContainer();
    
    // Register scope variables as services
    Object.entries(scope).forEach(([key, value]) => {
      container.register(Registration.instance(key, value));
    });
    
    return container;
  }
}

Performance Optimization

Template Compilation Caching

Implement aggressive template caching for performance:

class CachedTemplateCompiler {
  private compilationCache = new Map<string, ICompiledTemplate>();
  private templateHashCache = new Map<string, string>();

  compile(template: string, context: ICompilationContext): ICompiledTemplate {
    const hash = this.getTemplateHash(template, context);
    
    if (this.compilationCache.has(hash)) {
      return this.compilationCache.get(hash)!;
    }

    const compiled = this.performCompilation(template, context);
    this.compilationCache.set(hash, compiled);
    return compiled;
  }

  private getTemplateHash(template: string, context: ICompilationContext): string {
    const contextHash = this.getContextHash(context);
    return `${template.length}-${contextHash}`;
  }
}

Compilation Mode Optimization

Configure compilation for different environments:

interface CompilationOptions {
  resolveResources?: boolean;
  debug?: boolean;
  enhance?: boolean;
  aot?: boolean;
}

class OptimizedCompiler {
  compile(template: string, options: CompilationOptions = {}): ICompiledTemplate {
    const context = this.createOptimizedContext(options);
    
    if (options.aot) {
      return this.compileAOT(template, context);
    }
    
    return this.compileJIT(template, context);
  }

  private createOptimizedContext(options: CompilationOptions): ICompilationContext {
    const context = new CompilationContext();
    
    context.resolveResources = options.resolveResources ?? true;
    context.debug = options.debug ?? __DEV__;
    context.enhance = options.enhance ?? false;
    
    return context;
  }
}

Best Practices

1. Hook Registration

  • Register global hooks early in application bootstrap

  • Use component-level hooks for specific customizations

  • Keep hooks lightweight to avoid compilation performance impact

2. Pattern and Command Design

  • Design patterns to be intuitive and consistent with Aurelia conventions

  • Use descriptive names and clear syntax

  • Provide good error messages for invalid usage

3. Resource Resolution

  • Cache expensive resource lookups

  • Implement fallback mechanisms for missing resources

  • Use lazy loading for dynamic components

4. Performance Considerations

  • Profile template compilation in development

  • Use AOT compilation for production builds

  • Implement smart caching strategies

  • Monitor memory usage with large template caches

5. Testing Extensions

  • Create unit tests for custom hooks and commands

  • Test compilation output for correctness

  • Verify performance impact of extensions

  • Test edge cases and error handling

The template compiler's extensibility allows for powerful customizations while maintaining framework performance and consistency. Use these extension points judiciously to enhance your application's template processing capabilities.

Last updated

Was this helpful?