Files
Maurycy e8523d270e Refactor API and enhance Angular integration
- Removed `ciTargetName` from `nx.json`.
- Updated `package.json` to include new dependencies: `@types/seedrandom`, `fast-check`, `happy-dom`, and `@nestjs/schedule`.
- Modified `pnpm-lock.yaml` to reflect the addition of new packages and their versions.
- Improved project documentation in `PROJECT_CONTEXT.md` to clarify the use of Zod schemas and Angular framework decisions.
- Introduced new Angular components and patterns in the `.agents/skills/frontend-angular` directory, including examples and reference materials for Angular 21+ features.
2026-05-07 14:26:16 +00:00

4.3 KiB
Raw Permalink Blame History

name, description, trigger-terms
name description trigger-terms
frontend-angular Modern Angular 21+ patterns including signals, standalone components, zoneless change detection, and new control flow syntax. Use for Angular architecture decisions or implementing components with latest APIs. Angular 21+, signals, standalone components, zoneless, control flow, @if, @for

Angular Production Patterns

Production-ready patterns for Angular 21+ applications with zoneless change detection.

Core Concepts

Signal-Based Reactivity

import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="increment()">
      {{ count() }} × 2 = {{ doubled() }}
    </button>
  `
})
export class CounterComponent {
  count = signal(0);
  doubled = computed(() => this.count() * 2);

  constructor() {
    effect(() => console.log('Count changed:', this.count()));
  }

  increment() {
    this.count.update(n => n + 1);
  }
}

Key APIs

API Purpose
signal() Reactive state declaration
computed() Derived values (auto-tracked)
effect() Side effects on signal changes
input() Component inputs (replaces @Input)
output() Component outputs (replaces @Output)
model() Two-way bindable signals

Modern Control Flow

<!-- Conditionals -->
@if (isLoading()) {
  <app-spinner />
} @else if (hasError()) {
  <app-error [message]="error()" />
} @else {
  <app-content [data]="data()" />
}

<!-- Loops with tracking -->
@for (item of items(); track item.id) {
  <app-item [data]="item" />
} @empty {
  <p>No items found</p>
}

<!-- Switch -->
@switch (status()) {
  @case ('pending') { <app-pending /> }
  @case ('active') { <app-active /> }
  @default { <app-unknown /> }
}

Standalone Architecture

// No NgModules - components declare their dependencies
@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [CommonModule, RouterModule, MetricsComponent],
  template: `...`
})
export class DashboardComponent {}

// Bootstrapping
bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
});

Component I/O (Angular 21+)

@Component({...})
export class UserCardComponent {
  // Input signal (required)
  user = input.required<User>();

  // Input with default
  showAvatar = input(true);

  // Output
  selected = output<User>();

  // Two-way binding
  isExpanded = model(false);

  onSelect() {
    this.selected.emit(this.user());
  }
}

Performance Targets

Metric Target
LCP < 2.5s
INP < 200ms
CLS < 0.1
Initial Bundle < 250KB

When NOT to Use Angular

  • Simple interactivity → Vanilla JS
  • Static marketing site → Astro/11ty
  • < 100KB JS budget → Svelte or Web Components
  • React ecosystem dependency → React

Signal Testing Patterns

Pattern Use Case
PLATFORM_ID mocking Prevent constructor side effects in SSR tests
WritableSignal with .set() Control signal state without reassignment
NG0100 prevention Initialize signals before detectChanges()

Signal-Based Service Pattern

Private writable signal → public readonly → update in tap() → component injects and reads:

@Injectable({ providedIn: 'root' })
export class AnalyticsService {
  private _summary = signal<Summary | null>(null);
  readonly summary = this._summary.asReadonly();

  private _loading = signal(false);
  readonly loading = this._loading.asReadonly();

  getSummary() {
    this._loading.set(true);
    return this.http.get<Summary>('/api/analytics/summary').pipe(
      tap(data => {
        this._summary.set(data);
        this._loading.set(false);
      }),
      catchError(err => {
        this._loading.set(false);
        throw err;
      })
    );
  }
}

Component usage:

@Component({...})
export class DashboardComponent {
  private analytics = inject(AnalyticsService);
  summary = this.analytics.summary;
  loading = this.analytics.loading;
}

Pattern: Matches AuthService pattern for consistency across services.

See examples.md for full testing code patterns.

See reference.md for component libraries.