Skip to content

Instantly share code, notes, and snippets.

@diegohb
Last active February 12, 2026 04:24
Show Gist options
  • Select an option

  • Save diegohb/b75e909fa3d55acaf2c5b66a4684c69a to your computer and use it in GitHub Desktop.

Select an option

Save diegohb/b75e909fa3d55acaf2c5b66a4684c69a to your computer and use it in GitHub Desktop.
To provide another AI with an expert-level understanding of Aurelia 2, the following prompt is structured in a logical progression—from foundational philosophy to internal execution mechanics—ensuring the model understands not just the "how," but the "why" behind the framework's architecture.

Aurelia v2 Application Guidelines

Last Updated: 2026-02-11
Purpose: Architectural patterns and conventions for Aurelia v2 applications


1. Global Architecture & Project Structure

Feature-First Architecture

Core Principle: Group files by business capability (e.g., /features/products, /features/orders) rather than technical layer (e.g., /views, /models).

  • Feature Slices: Group all components, templates, and domain-specific logic related to a business unit in a single directory.
  • Sub-Feature Nesting: Use nested features for complex modules (e.g., /features/administration/users, /features/administration/settings).
  • Encapsulation by Default: Each feature directory should contain its own localized custom elements, value converters, and styles to avoid global namespace pollution.
  • Declarative Routing: Define routes directly on components to mirror directory structure on disk, maintaining "vertical slice" architecture.
  • Hierarchical AI Documentation: Use a root Agents.md for global framework guardrails and directory-specific Agents.md files to provide just-in-time context.

Directory Structure (Feature-First)

src/
├── features/                 # Business capability slices
│   ├── products/            # Product management feature
│   │   ├── index.ts          # Feature entry point (exports components)
│   │   ├── product-list.ts   # ViewModel
│   │   ├── product-list.html # Template
│   │   ├── components/       # Feature-specific components
│   │   │   └── product-card/
│   │   └── services/         # Feature-specific services if needed
│   ├── administration/       # Admin feature (sub-features)
│   │   ├── users/
│   │   └── settings/
│   └── orders/
├── models/                   # DTOs (API contracts)
│   ├── contact-dto.ts
│   ├── target-dto.ts
│   └── program-dto.ts
├── entities/                 # Models (business logic)
│   ├── contact/
│   ├── target/
│   └── program/
├── services/                 # Infrastructure services
│   ├── api.service.ts
│   └── auth.service.ts
└── shared/                   # Cross-cutting utilities
    └── components/           # Reusable UI components

Terminology

Term Definition Location
DTO (Data Transfer Object) Response Model - API contract interfaces (PascalCase properties) src/models/
Model Internal business logic classes (camelCase properties) src/entities/
ViewModel View-specific state management (component classes) src/views/

Key Principle: Services return Models, not DTOs. DTOs are never exposed to components.


Architecture Overview

Separation of Concerns

┌─────────────────────────────────────────────────────────────┐
│                     Components (Views)                    │
│                   src/views/*.ts (ViewModels)           │
│                        ↓ uses                            │
│                  ┌────────────┐                          │
│                  │   Models   │                          │
│          src/entities/{entity}/ (Business Logic)          │
│                  ↓ converts                           │
│                  ┌────────────┐                          │
│                  │   DTOs     │                          │
│          src/models/ (API Contracts)                   │
│                  ↓ calls                            │
│               ┌─────────────┐                          │
│               │  Backend API │                          │
└─────────────────────────────────────────────────────────────┘

Data Flow

  1. API Response → DTO (interface, PascalCase)
  2. DTO → Model (class, fromDTO() static factory)
  3. Model → Component (typed, camelCase)
  4. Component → Model (via bindings)
  5. Model → DTO (toDTO() instance method)
  6. DTO → API Request (PascalCase)

Directory Structure (Traditional vs Feature-First)

Traditional Structure (for reference)

src/
├── models/              # DTOs (API contracts)
│   ├── entity-dto.ts
│   ├── collection-dto.ts
│   └── configuration-dto.ts
├── entities/           # Models (business logic)
│   ├── entity/
│   │   ├── entity.model.ts
│   │   ├── entity.service.ts
│   │   └── nested-model.model.ts
│   ├── collection/
│   └── configuration/
├── services/            # Infrastructure services
│   ├── api.service.ts       # Generic HTTP client
│   └── auth.service.ts
└── views/              # ViewModels
    └── entity-page.ts

Feature-First Structure (Recommended)

src/
├── features/                 # Business capability slices
│   ├── products/            # Product management feature
│   │   ├── index.ts          # Feature entry (exports all public components)
│   │   ├── product-list.ts   # ViewModel
│   │   ├── product-list.html # Template
│   │   ├── components/       # Feature-specific components
│   │   │   └── product-card/
│   │   └── services/         # Feature-specific services if needed
│   ├── contacts/            # Contact management feature
│   │   ├── contact-list.ts
│   │   ├── contact-list.html
│   │   └── contact-detail.ts
│   ├── targets/
│   ├── programs/
│   └── administration/       # Complex features with sub-features
│       ├── users/
│       └── settings/
├── models/                   # DTOs (API contracts) - SHARED ACROSS ALL FEATURES
│   ├── contact-dto.ts
│   ├── target-dto.ts
│   ├── program-dto.ts
│   ├── collection-dto.ts
│   └── configuration-dto.ts
├── entities/                 # Models (business logic) - SHARED ACROSS ALL FEATURES
│   ├── contact/
│   ├── target/
│   ├── program/
│   ├── collection/
│   └── configuration/
├── services/                 # Infrastructure services - SHARED ACROSS ALL FEATURES
│   ├── api.service.ts
│   └── auth.service.ts
├── shared/                   # Cross-cutting utilities
│   └── components/           # Reusable UI components
└── app.ts                    # Application entry point

File Naming Conventions

Type Pattern Example
DTO interface {entity}-dto.ts contact-dto.ts
Model class {entity}.model.ts contact.model.ts
Service class {entity}.service.ts contact.service.ts
Service interface I{Entity}Service IContactService
ViewModel {entity}-page.ts contacts-page.ts

Service Layer Patterns

Service-as-Store Pattern

Core Principle: Keep state "close to where it is used" by leveraging DI-based services as the primary state management strategy. Avoid cross-cutting state propagation patterns like Event Aggregator.

Benefits of Service-as-Store:

  • Explicit Dependencies: Services declare their dependencies explicitly via DI, making relationships clear
  • Testable State: State is encapsulated in services with clear interfaces, easier to test
  • Type Safety: TypeScript interfaces define what state and methods are available
  • No Magic: Components know exactly what services they use, no implicit event subscriptions

When to Use Service-as-Store:

  • Entity-specific state (e.g., contact list, form data)
  • Cached data with TTL
  • User preferences and settings
  • Feature flags and configuration

When NOT to Use Service-as-Store:

  • Cross-component UI state not tied to business logic
  • Complex global state with multiple reducers (consider @aurelia/state)
  • Transient, per-request data

Repository Pattern

Abstract data access using a repository layer (e.g., ContactRepository) to keep services agnostic of data source.

export interface IContactRepository {
  findById(id: string): Promise<ContactModel | null>;
  findAll(params: QueryParams): Promise<ContactModel[]>;
  save(model: ContactModel): Promise<ContactModel>;
  delete(id: string): Promise<void>;
}

export const IContactRepository = DI.createInterface<IContactRepository>(
  'IContactRepository',
  x => x.singleton(ContactRepository)
);

export class ContactService {
  private repository = resolve(IContactRepository);
  private apiService = resolve(IApiService);
  
  async getContacts(): Promise<ContactModel[]> {
    return this.repository.findAll({ page: 1, pageSize: 50 });
  }
}

Service Definition with DI.createInterface()

Aurelia v2 recommends DI.createInterface() for service tokens:

import { DI } from 'aurelia';
import { Registration } from 'aurelia';

// 1. Define interface
export interface IContactService {
  getContacts(page: number, pageSize: number): Promise<{
    data: ContactModel[];
    total: number;
    page: number;
  }>;
  getContact(id: string): Promise<ContactModel>;
}

// 2. Create interface token with default implementation
export const IContactService = DI.createInterface<IContactService>(
  'IContactService',
  x => x.singleton(ContactService)  // Auto-register as singleton
);

// 3. Export type for convenience
export type IContactService = IContactService;

Benefits of Interfaces:

  • Loose coupling (depend on abstraction, not concrete)
  • Better testability (inject mocks easily)
  • Flexible (swap implementations without changing consumers)

Dependency Injection

Prefer resolve() for property injection:

import { resolve } from 'aurelia';

export class ContactPage implements IRouteViewModel {
  // ✅ Property injection (recommended)
  private contactService = resolve(IContactService);
  private apiService = resolve(IApiService);
  
  // ❌ Constructor injection (still works but less preferred)
  constructor(private contactService: IContactService) {}
}

Service Registration Strategies

Strategy Usage Example
singleton() Shared state, API clients x => x.singleton(ApiService)
transient() Stateless, disposable x => x.transient(ValidatorService)
instance() Pre-configured object x => x.instance(config)

Advanced DI Resolvers

  • lazy(Token): Defer resolution until demand for better startup performance
  • newInstanceForScope(Token): Create a scoped instance for a specific component branch (e.g., a validation controller for a form)
  • optional(Token): Provide a graceful fallback if a service is not registered
import { lazy, optional } from 'aurelia';

export class MyComponent {
  // Defer creation until first use
  private heavyService = lazy(IHeavyService);
  
  // Graceful fallback if not registered
  private optionalService = optional(IOptionalService);
}

Manual Registration (if no default)

Strategy Usage Example
singleton() Shared state, API clients x => x.singleton(ApiService)
transient() Stateless, disposable x => x.transient(ValidatorService)
instance() Pre-configured object x => x.instance(config)

Manual Registration (if no default):

Aurelia.register(
  Registration.singleton(ICustomService, CustomServiceImplementation)
);

Model/DTO Conversion Patterns

Model Class Structure

import { ContactDTO } from '../../models/contact-dto';

export class ContactModel {
  constructor(
    public id: string,
    public fullName: string,
    public email: string,
    public status: ContactStatus = 'active',
    public createdAt: string = new Date().toISOString()
  ) {}

  // Static factory: DTO → Model
  public static fromDTO(dto: ContactDTO): ContactModel {
    return new ContactModel(
      dto.Id.toString(),
      dto.FullName,
      dto.Email,
      'active'
    );
  }

  // Instance method: Model → DTO
  public toDTO(): ContactDTO {
    return {
      Id: parseInt(this.id, 10),
      Email: this.email,
      FullName: this.fullName
    };
  }
}

export type ContactStatus = 'active' | 'inactive' | 'archived';

Dynamic Mapping for Case Conversion

Handle PascalCase (DTO) → camelCase (Model) conversions:

public static fromDTO(dto: ContactDTO): ContactModel {
  return new ContactModel(
    dto.Id.toString(),      // PascalCase → camelCase
    dto.FullName,
    dto.Email
  );
}

public toDTO(): ContactDTO {
  return {
    Id: parseInt(this.id, 10),  // camelCase → PascalCase
    Email: this.email,
    FullName: this.fullName
  };
}

Nested Models

For complex entities with nested objects, create separate model classes:

// TargetModel with nested AddressModel
export class TargetModel {
  constructor(
    public id: string,
    public name: string,
    public address?: AddressModel  // Nested model
  ) {}

  static fromDTO(dto: TargetDTO): TargetModel {
    return new TargetModel(
      dto.ID.toString(),
      dto.Target,
      dto.AddressRef ? AddressModel.fromDTO(dto.AddressRef) : undefined
    );
  }

  toDTO(): TargetDTO {
    return {
      ID: parseInt(this.id, 10),
      Target: this.name,
      AddressRef: this.address?.toDTO()
    };
  }
}

export class AddressModel {
  constructor(
    public line1?: string,
    public city?: string
  ) {}

  static fromDTO(dto: AddressRefDTO): AddressModel {
    return new AddressModel(dto.Line1, dto.City);
  }

  toDTO(): AddressRefDTO {
    return { Line1: this.line1, City: this.city };
  }
}

Dynamic Schema Handling

For Collections and Configuration entities with dynamic fields, use any:

export class ProgramTypeModel {
  constructor(
    public id: string,
    public name: string,
    public customFields: Record<string, any> = {}  // Dynamic fields
  ) {}

  static fromDTO(dto: ProgramTypeDTO): ProgramTypeModel {
    return new ProgramTypeModel(dto.Id.toString(), dto.Name);
  }

  // No toDTO - read-only entities don't send to server
}

5. Data & State Management

Request Deduplication

Implement pending request maps in state services to prevent redundant simultaneous API calls.

export class ContactListStateService {
  private pendingRequests = new Map<string, Promise<ContactModel[]>>();
  
  async getContacts(forceRefresh = false): Promise<ContactModel[]> {
    const cacheKey = 'contacts:all';
    
    if (!forceRefresh && this.pendingRequests.has(cacheKey)) {
      return this.pendingRequests.get(cacheKey)!;
    }
    
    const promise = this.loadContacts();
    this.pendingRequests.set(cacheKey, promise);
    
    try {
      return await promise;
    } finally {
      this.pendingRequests.delete(cacheKey);
    }
  }
}

State-as-Store Preference

For most cases, a singleton DI service is the preferred state management strategy. Use @aurelia/state only for complex, truly global state.

When to use @aurelia/state:

  • Cross-component state not tied to services
  • Complex state with multiple reducers
  • State that needs time-travel debugging

When to use singleton services:

  • Entity-specific state (e.g., contact list)
  • Form state (e.g., validation, submission status)
  • Cached data with TTL

Event Aggregator: Discouraged

Avoid Cross-Cutting Event Propagation: The Event Aggregator pattern (publishing and subscribing to application-wide events) is identified as a potential source of inconsistency and should be avoided in favor of DI-based services.

Why Avoid Event Aggregator:

  • Implicit Dependencies: Hard to track which components subscribe to which events
  • Type Safety: Event payloads are often untyped (any), breaking type safety
  • Debugging: Difficult to trace event flow through the application
  • Coupling: Creates implicit dependencies that aren't declared in constructors

Use Instead: Service-as-Store Pattern

// ❌ AVOID: Event Aggregator
export class ContactList {
  private eventAggregator;
  
  constructor() {
    this.eventAggregator = resolve(IEventAggregator);
    this.eventAggregator.subscribe('contact:selected', this.handleContactSelected);
  }
  
  handleContactSelected(event) {
    // What type is event.payload?
    // Where does this event come from?
  }
}

// ✅ PREFER: DI-based service
export class ContactList {
  private contactState = resolve(IContactStateService); // Explicit dependency
  
  selectContact(contactId: string) {
    this.contactState.setSelectedContact(contactId); // Typed method
    // Type-safe, explicit dependencies
  }
}

6. Component Usage Guidelines

Local Dependencies Property

Use the dependencies property in @customElement decorator to declare only resources (value converters, attributes) needed by that component, enabling efficient tree-shaking.

@customElement('my-component', {
  name: 'my-component',
  dependencies: [MyValueConverter, MyCustomAttribute]
})
export class MyComponent {}

Resource Management Patterns

Local Resource Registration: Use the dependencies property in @customElement decorator to declare only resources needed by that component, enabling efficient tree-shaking.

Import vs Require:

Pattern Syntax Use Case
Import <import from="./value-converter"></import> Feature-specific resources
Require <require from="./value-converter"></require> Discouraged in Aurelia 2
Global Registered in main.ts Cross-cutting shared resources

Example: Feature-Specific Resource Registration

<!-- product-card.html -->
<template>
  <import from="./price-formatter"></import>
  
  <div class="card">
    <span>${price | priceFormatter}</span>
  </div>
</template>

<customElement('product-card', {
  name: 'product-card',
  dependencies: [PriceFormatter]  // Only what this component needs
})
export class ProductCard {}

Direct Component Imports: Use direct component imports instead of string-based module paths and PLATFORM.moduleName for better tree-shaking.

// ✅ Preferred: Direct import
import { ProductList } from './product-list';
import { ProductDetail } from './product-detail';

@route({
  routes: [
    { path: '', component: ProductList },
    { path: ':id', component: ProductDetail }
  ]
})

// ❌ Discouraged: String module path
import { ProductList } from './product-list';
import { ProductDetail } from './product-detail';

@route({
  routes: [
    { path: '', component: () => import('./product-list').get('default') },
    { path: ':id', component: () => import('./product-detail').get('default') }
  ]
})

Au-Compose vs Component Composition

Use @au-compose decorator for component composition instead of the deprecated <compose> element.

import { compose } from 'aurelia';

@compose({
  template: `<div class="loading"><slot></slot></div>`
})
export class LoadingIndicator {}

Extend Existing Component Library

Before creating new components, check src/components/:

  • Button, Alert, Card, Modal, Input, Navigation, DataTable, FileUpload, Spinner
  • Layout: Header, Footer, Grid, Flex, Sidebar, CardGrid
  • Utility: Search, Pagination, ListView

Component Type Safety

import { bindable } from 'aurelia';

export type ButtonVariant = 'primary' | 'secondary' | 'danger';
export type ButtonSize = 'sm' | 'md' | 'lg';

@customElement('ui-button')
export class Button {
  @bindable public variant: ButtonVariant = 'primary';
  @bindable public size: ButtonSize = 'md';
  @bindable public disabled: boolean = false;

  public get classes(): string {
    const variantClasses: Record<ButtonVariant, string> = {
      primary: 'bg-blue-500 text-white',
      secondary: 'bg-gray-500 text-white',
      danger: 'bg-red-500 text-white'
    };
    return `rounded ${variantClasses[this.variant]} ${this.getSizeClasses()}`;
  }

  private getSizeClasses(): string {
    const sizeMap: Record<ButtonSize, string> = {
      sm: 'px-2 py-1 text-sm',
      md: 'px-4 py-2 text-base',
      lg: 'px-6 py-3 text-lg'
    };
    return sizeMap[this.size];
  }
}

Binding Modes

Use appropriate binding commands for different scenarios:

Mode Syntax Direction Use Case
Two-way value.bind="property" model ↔ view Forms
To view value.to-view="property" model → view Read-only
From view value.from-view="property" view → model User input
One-time value.one-time="property" model → view once Static values

Default to .bind for most cases, .two-way for forms, and .to-view for performance-critical data.

Event Handling

Replace .delegate with .trigger: Use .trigger for almost all event handling in v2, as it efficiently handles event bubbling for dynamic content and feels more natural with Shadow DOM.

export class Button {
  @bindable public onClick?: (event: MouseEvent) => void;
  
  public handleClick(event: MouseEvent): void {
    if (this.disabled) {
      event.preventDefault();
      return;
    }
    
    // Emit custom event with .trigger
    this.dispatchEvent(
      new CustomEvent('click', { 
        detail: { value: 'clicked' },
        bubbles: true 
      })
    );
    
    this.onClick?.(event);
  }
}

Event Modifiers: Use Aurelia event modifiers to declaratively handle conditions and prevent default behaviors directly in the template.

Modifier Example Purpose
@click:capture @click.capture Handle in capture phase
@click:delegate @click.delegate Use delegate (native events)
@click:self @click.self Only on element itself
@click:stop @click.stop Stop propagation
@click:prevent @click.prevent Prevent default behavior
@click:ctrl+enter @click.ctrl+enter.prevent Handle key combinations
<!-- Declarative event handling with modifiers -->
<input type="text" 
       @keydown.enter.prevent="onEnter()"
       @click:ctrl+enter="onCtrlEnter()"
       value.bind="userInput" />

Example - Form Submission with Validation:

<form submit.trigger="handleSubmit($event)">
  <input type="text" name="username" value.bind="credentials.username">
  <input type="password" name="password" value.bind="credentials.password">
  <button type="submit" @click:ctrl+enter.trigger="handleSubmit($event)">
    Login
  </button>
</form>
<button class.bind="classes" 
        click.trigger="handleClick($event)"
        disabled.bind="disabled">
  <slot></slot>
</button>

Binding Modes

Mode Syntax Direction
Two-way (default) value.bind="property" model ↔ view
To view value.to-view="property" model → view
From view value.from-view="property" view → model
One-time value.one-time="property" model → view once

Quick Reference

Common Imports

// Core Aurelia
import { resolve, DI, Registration } from 'aurelia';
import { route } from '@aurelia/router';
import { IRouteContext, ICurrentRoute, IRouterEvents, IDisposable } from '@aurelia/router';

// Custom element
import { customElement, bindable, useShadowDOM, slotted } from 'aurelia';

// App types
import type { IContactService } from '../entities/contact/contact.service';
import { ContactModel } from '../entities/contact/contact.model';
import { ContactDTO } from '../models/contact-dto';

Service Injection Pattern

export class MyViewModel {
  // Property injection (recommended)
  private contactService = resolve(IContactService);
  private authService = resolve(IAuthService);
  
  // Constructor injection (alternative)
  constructor(private apiService = resolve(IApiService)) {}
}

Component Definition Pattern

@customElement('my-element')
export class MyElement {
  @bindable public input: string = '';
  @bindable public options: string[] = [];
  
  public get computedValue(): string {
    return this.input.toUpperCase();
  }
}

Routing Pattern

@route({
  path: 'parent',
  routes: [
    { path: 'child', component: () => import('./child'), title: 'Child' },
    { path: '', redirectTo: 'child' }
  ]
})
export class ParentLayout {
  private navModel = resolve(IRouteContext)().routeConfigContext.navigationModel;
}

Resources

Quick Reference: @docs/Aurelia.md (framework API details)

Aurelia DeepWiki: https://deepwiki.com/aurelia/aurelia

Aurelia Documentation: https://docs.aurelia.io

Aurelia GitHub: https://github.com/aurelia/aurelia

Aurelia 2 Technical Reference

Framework Version: Aurelia 2.x
Last Updated: 2026-02-11
Purpose: Quick technical reference for agents


Overview

Aurelia 2 is a TypeScript-first web framework with direct DOM manipulation and integrated DI container.


Component Definition

Components pair view model (.ts) with template (.html):

import { customElement, bindable } from 'aurelia';

@customElement('my-component')
export class MyComponent {
  @bindable public property: string = '';
}

Lifecycle Hooks

Hook Transition Purpose
constructor() Instance creation Initialize component
binding() activating → binding Before bindings activate
bound() binding → bound After bindings subscribe to observers
attaching() bound → attaching Before DOM insertion
attached() attaching → activated After DOM insertion, fully active
detaching() deactivating → detaching Before DOM removal
unbinding() detaching → deactivated Before bindings unsubscribe

Binding Commands

Binding Modes

Mode Syntax Direction Use Case
Two-way value.bind="property" model ↔ view Forms
To view value.to-view="property" model → view Read-only
From view value.from-view="property" view → model User input
One-time value.one-time="property" model → view once Static values

Event Binding Syntax

Event Binding Syntax

Type Syntax Use Case
.trigger event.trigger="handler($event)" Custom events
.delegate event.delegate="handler($event)" Native DOM events
.call event.call="handler()" Call without event object

Critical Rule

Custom events MUST use .trigger, not .delegate

<!-- ✅ CORRECT -->
<nav nav-click.trigger="handleNav($event)"></nav>

<!-- ❌ INCORRECT - throws AUR0009 -->
<nav nav-click.delegate="handleNav($event)"></nav>

Event Modifiers

Use event modifiers to declaratively handle conditions and prevent default behaviors.

Modifier Example Purpose
@click:capture @click.capture Handle in capture phase
@click:delegate @click.delegate Use delegate (native DOM events)
@click:self @click.self Only on element itself
@click:stop @click.stop Stop propagation
@click:prevent @click.prevent Prevent default behavior
@click:ctrl+enter @click.ctrl+enter.prevent Handle key combinations
<input type="text" 
       @keydown.enter.prevent="onEnter()"
       @click:ctrl+enter="onCtrlEnter()"
       value.bind="userInput" />

Routing: Parent-Child Pattern

Use layout component with <au-viewport> for nested routes.

Use layout component with <au-viewport> for nested routes.

import { route } from '@aurelia/router';

@route({
  path: 'entity',
  routes: [
    { path: 'contacts', component: () => import('./contacts'), title: 'Contacts' },
    { path: '', redirectTo: 'contacts' }
  ]
})
export class EntityLayout {}
<div class="layout">
  <nav><a load="contacts">Contacts</a></nav>
  <main><au-viewport></au-viewport></main>
  </div>

Lifecycle Guards

Use canLoad and canUnload for asynchronous navigation guards.

@route(...)
export class ProtectedPage {
  async canLoad(params, config) {
    const authService = resolve(IAuthService);
    if (!authService.isAuthenticated()) {
      config.redirect = 'login';
      return false;
    }
    return true;
  }
}

Instruction Handling

Prefer load custom attribute over href when you need structured parameter binding.

<!-- ✅ Preferred: Custom instruction -->
<a load.bind="contact.id">View</a>

<!-- ❌ Less preferred: Manual href construction -->
<a href="/contacts/${contact.id}">View</a>

Dependency Injection

Aurelia v2 prefers resolve() over @inject decorator:

import { resolve } from 'aurelia';
import { IMyService } from './services/my.service';

export class MyComponent {
  private myService = resolve(IMyService);
}

For services, use DI.createInterface() to define injection tokens:

import { DI } from 'aurelia';

export interface IMyService {
  getData(): Promise<any>;
}

export const IMyService = DI.createInterface<IMyService>('IMyService', x => x.singleton(MyService));
export type IMyService = IMyService;

Advanced DI Resolvers

import { lazy, optional, newInstanceForScope } from 'aurelia';

export class MyComponent {
  // Defer creation until first use
  private heavyService = lazy(IHeavyService);
  
  // Graceful fallback if not registered
  private optionalService = optional(IOptionalService);
  
  // Scoped instance for this component tree
  private scopedService = newInstanceForScope(IScopedService);
}

Batch Operations

Use batch() when making multiple property or array changes to combine them into a single change notification and prevent unnecessary UI updates.

import { batch } from 'aurelia';

export class ContactList {
  contacts: ContactModel[] = [];
  selectedContacts: Set<string> = new Set();
  
  selectContact(id: string) {
    this.selectedContacts.add(id);
  }
  
  deselectContact(id: string) {
    this.selectedContacts.delete(id);
  }
  
  // Group related DOM updates into single batch
  selectAll() {
    batch(() => {
      this.contacts.forEach(c => {
        this.selectedContacts.add(c.id);
      });
    });
  }
}

Au-Compose Decorator

Use @au-compose for component composition instead of the deprecated <compose> element.

import { compose } from 'aurelia';

@compose({
  template: `<div class="loading-indicator"><slot></slot></div>`
})
export class LoadingIndicator {}

Key Constraints

Issue Description Solution
containerless + Shadow DOM Mutually exclusive Use one or the other
<slot> without Shadow DOM Throws AUR0717 Enable Shadow DOM first
Custom event with .delegate Throws AUR0009 Use .trigger for custom events
Element naming Must have hyphen Use kebab-case (e.g., my-component)

Best Practices

  • ✅ Use .trigger for custom events
  • ✅ Use @aurelia/router for centralized routing
  • ✅ Use parent-child routing with <au-viewport>
  • ✅ Share data via singleton services, not base classes
  • ✅ Use resolve() for property injection
  • ✅ Use DI.createInterface() for service definitions
  • ✅ Follow kebab-case naming for elements
  • ✅ Use Light DOM with TailwindCSS (current approach)
  • ✅ Use queueAsyncTask for controllable async work
  • ✅ Use batch() for grouping DOM updates
  • ✅ Use createStateMemoizer for expensive computations

Avoid: Event Aggregator

The Event Aggregator pattern (publishing and subscribing to application-wide events) is identified as a potential source of inconsistency and should be avoided in favor of DI-based services.

Why Avoid Event Aggregator:

  • Implicit dependencies make tracking difficult
  • Event payloads are often untyped (any)
  • Hard to trace event flow through the application
  • Creates coupling that isn't declared in constructors

Quick Reference

Binding Modes

Mode Syntax Direction Use Case
Two-way value.bind="property" model ↔ view Forms
To view value.to-view="property" model → view Read-only
From view value.from-view="property" view → model User input
One-time value.one-time="property" model → view once Static values

References

Full Guidelines: @docs/Aurelia_Guidelines.md (architectural patterns, service patterns, DTO/Model separation)

Testing: @docs/testing.md

UI Component Library: src/components/

Aurelia DeepWiki: https://deepwiki.com/aurelia/aurelia

Aurelia Github: https://github.com/aurelia/aurelia

Aurelia Docs: https://docs.aurelia.io

AI Expert Prompt: Aurelia 2 Mastery

Role: You are a Lead Software Architect specializing in Aurelia 2. Your goal is to ingest the following 24 documentation modules in a specific sequence to build a high-fidelity mental model of the framework.

Core Directive: Aurelia 2 prioritizes Web Standards and Convention over Configuration1.... When interpreting the following content, prioritize the modern resolve() function over legacy decorators and emphasize reactive observation without a Virtual DOM4....

Phase 1: Foundations & Mindset

URL 1 (The Aurelia Philosophy): [https://docs.aurelia.io/introduction/the-aurelia-philosophy\]

    ◦ Instruction: Focus on the "fighting words" regarding framework churn and the commitment to Web Standards, Enhanced27. Internalize the "stability over hype" mindset89.

URL 2 (Essentials): [https://docs.aurelia.io/essentials\]

    ◦ Instruction: Identify the four pillars: Components, Templates, DI, and Reactivity610.

URL 3 (Getting Started): [https://docs.aurelia.io/getting-to-know-aurelia/routing/getting-started\]

    ◦ Instruction: Observe the bootstrapping phase and how the router is registered as a global dependency1112.

Phase 2: The Component Model & Templating

URL 4 (Component Basics): [https://docs.aurelia.io/components/component-basics\]

    ◦ Instruction: Note that components must have a hyphen in their name to align with Web Component standards13. Study the pairing of .ts and .html files14.

URL 5 (Component Lifecycles): [https://docs.aurelia.io/components/component-lifecycles\]

    ◦ Instruction: Technical Map—Distinguish between Top -> Down (parent before child) and Bottom -> Up (child before parent) execution orders1516. Pay special attention to the binding hook's ability to block children via Promises17.

URL 6 (Event Binding): [https://docs.aurelia.io/templates/template-syntax/event-binding\]

    ◦ Instruction: Learn the difference between .trigger (bubbling) and .capture (capturing)18. Mastery of event modifiers (e.g., :ctrl+enter.prevent) is essential19....

URLs 7-8 (Cheat Sheets): [https://docs.aurelia.io/templates/cheat-sheet\]

    ◦ Instruction: Use these for a syntax "Rosetta Stone," focusing on binding modes (.bind, .two-way, .one-time)2223.

Phase 3: Service Architecture & DI

URL 9 (DI Overview): [https://docs.aurelia.io/getting-to-know-aurelia/dependency-injection/di-overview\]

    ◦ Instruction: Internalize that services are singletons by default24. Study Constructor Injection using resolve()5.

URL 10 (Advanced DI Patterns): [https://docs.aurelia.io/developer-guides/advanced-di-patterns-and-recipes\]

    ◦ Instruction: Focus on Interface-based DI using DI.createInterface25. Memorize the use cases for resolvers like lazy(), all(), and newInstanceOf()2627.

URL 11 (Creating Services): [https://docs.aurelia.io/getting-to-know-aurelia/dependency-injection/creating-services\]

    ◦ Instruction: Learn how to structure stateless vs. stateful services using @singleton or @transient28....

Phase 4: Navigation, Communication, & Asynchrony

URL 12 (Routing Overview): [https://docs.aurelia.io/getting-to-know-aurelia/routing\]

    ◦ Instruction: Understand the viewport-first layout principle using <au-viewport>3132.

URL 13 (Child Routing Playbook): [https://docs.aurelia.io/getting-to-know-aurelia/routing/child-routing-playbook\]

    ◦ Instruction: Master relative navigation using ../ and the IRouteContext33.

URL 14 (Event Aggregator): [https://docs.aurelia.io/getting-to-know-aurelia/services-and-runtime-hooks/event-aggregator\]

    ◦ Instruction: Use this for decoupled pub/sub communication; always emphasize disposing of subscriptions to prevent memory leaks3435.

URL 15 (Task Queue): [https://docs.aurelia.io/getting-to-know-aurelia/services-and-runtime-hooks/task-queue\]

    ◦ Instruction: This is the "air-traffic controller"36. Prioritize queueAsyncTask() for app logic and tasksSettled() for deterministic testing3738.

URL 16 (App Tasks): [https://docs.aurelia.io/getting-to-know-aurelia/services-and-runtime-hooks/app-tasks\]

    ◦ Instruction: Understand these as higher-level framework hooks (e.g., creating, hydrating, activating) that run before component-level hooks3940.

Phase 5: Reliability & Advanced Engineering

URL 17 (Quick Reference - Testing): [https://docs.aurelia.io/developer-guides/testing/quick-reference-how-do-i\]

    ◦ Instruction: Focus on the createFixture pattern and how to mock dependencies during test setup4142.

URL 18 (Modern Build Tools): [https://docs.aurelia.io/developer-guides/modern-build-tools\]

    ◦ Instruction: Prioritize Vite configuration and HMR (Hot Module Replacement) setup4344.

URL 19 (Organizing Large-Scale Projects): [https://docs.aurelia.io/developer-guides/organizing-large-scale-projects\]

    ◦ Instruction: Study Feature-Based Architecture and the Monorepo pattern using Turbo4546.

URL 20 (Framework Internals): [https://docs.aurelia.io/getting-to-know-aurelia/advanced/framework-internals\]

    ◦ Instruction: Analyze how templates compile into Instruction objects (e.g., HydrateElementInstruction) and how renderers interpret them4748.

URL 21 (Migrating to Aurelia 2): [https://docs.aurelia.io/migrating-to-aurelia-2\]

    ◦ Instruction: Note critical breaking changes: .delegate is gone (use .trigger), and <compose> is now <au-compose>4950.

Phase 6: Ecosystem & Auxiliary Guides

URL 22 (Web Components): [https://docs.aurelia.io/developer-guides/web-components\]

    ◦ Instruction: Understand how to produce framework-agnostic elements5152.

URL 23 (Developing with AI): [https://docs.aurelia.io/developer-guides/developing-with-ai\]

    ◦ Instruction: Apply context-aware rules like .cursorrules or CLAUDE.md to maintain Aurelia 2 standards in AI generation5354.

URL 24 (Debugging & Troubleshooting): [https://docs.aurelia.io/developer-guides/debugging-and-troubleshooting\]

    ◦ Instruction: Learn to inspect component state via $0.au.controller.viewModel in the browser console.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment