Files
fog/.agents/skills/frontend-angular/SKILL.md
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

191 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: frontend-angular
description: 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.
trigger-terms: 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
```typescript
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
```html
<!-- 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
```typescript
// 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+)
```typescript
@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:
```typescript
@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:**
```typescript
@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.