Skip to main content

angular-idioms

Angular components, signals, DI, RxJS, standalone architecture. For TypeScript see typescript-idioms.

Source facts

Repository
irahardianto/fun-chess
Last source activity
August 26, 2026 at 13:39
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
angular-idioms
description
Angular components, signals, DI, RxJS, standalone architecture. For TypeScript see typescript-idioms.
paths
["**/*.component.ts","**/*.service.ts","**/*.directive.ts","**/*.pipe.ts","**/*.guard.ts","**/angular.json"]
## Angular Idioms and Patterns ### Core Philosophy Angular (19+) rewards signals, standalone components, and reactive patterns. Idiomatic Angular = typed, modular, RxJS-aware, OnPush by default. > **Scope:** This file covers Angular-specific coding idioms for components, services, and patterns. For TypeScript type system patterns, see `@.agents/skills/typescript-idioms/SKILL.md`. For file and folder layout, see `references/project-structure.md`. > > **Loading guard:** Do NOT load this skill for non-Angular projects. Vue → `vue-idioms`; React → `react-idioms`; Next.js → `nextjs-idioms`. Angular file suffixes (`.component.ts`, `.service.ts`, etc.) and `angular.json` are the reliable triggers — `tsconfig*.json` alone is NOT an Angular signal (every TS project has one). ## When to Load References > Load these **before** writing code in the matching context — not after. | Situation | Reference to Load | |---|---| | Starting an Angular project or reviewing file layout | `references/project-structure.md` | | TypeScript type system, async, Zod, error types | `@.agents/skills/typescript-idioms/SKILL.md` (always co-load) | | Zod schemas / boundary validation | `@.agents/skills/typescript-idioms/references/zod-patterns.md` | | Async / I/O / coercion / RxJS pitfalls | `@.agents/skills/typescript-idioms/references/ts-patterns-and-anti-patterns.md` | ### Standalone Components (Default) 1. **Standalone components** — no NgModules for new components (standalone is the default since Angular 19): ```typescript @Component({ selector: 'app-task-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [TaskCardComponent], template: ` @for (task of filteredTasks(); track task.id) { <app-task-card [task]="task" /> } ` }) export class TaskListComponent { tasks = signal<Task[]>([]); } ``` 2. **Lazy-load routes** with `loadComponent`: ```typescript { path: 'tasks', loadComponent: () => import('./features/task/task-list.component') .then(m => m.TaskListComponent) } ``` ### Signals (17+) 1. **Signals for synchronous state** — prefer over BehaviorSubject for component state. 2. **`computed`** for derived state. **`effect`** for side effects. 3. **RxJS for async streams** — HTTP, WebSocket, complex event handling. ```typescript // ✅ Signal-based component state export class TaskListComponent { private readonly taskService = inject(TaskService); tasks = signal<Task[]>([]); filter = signal<string>(''); filteredTasks = computed(() => // ✅ Derived state this.tasks().filter(t => t.title.includes(this.filter())) ); constructor() { effect(() => console.debug('Tasks updated:', this.tasks().length)); // ✅ Side effect } } ``` ### Change Detection 1. **`OnPush` is the default strategy** — set it on every component: ```typescript // ✅ Always OnPush @Component({ changeDetection: ChangeDetectionStrategy.OnPush, // ... }) ``` 2. **OnPush works naturally with signals** — signal reads in templates automatically trigger change detection when the signal value changes. 3. **Use `Default` only when** wrapping third-party components that mutate state imperatively and cannot be refactored. 4. **Never call `ChangeDetectorRef.detectChanges()` manually** — if you need it, your data flow is wrong. Convert to signals or use `async` pipe. ### Component Design 1. **Signal-based inputs and outputs** (Angular 17.1+) — prefer over decorators: ```typescript // ✅ Signal input — reactive, no OnChanges needed task = input.required<Task>(); variant = input<'compact' | 'full'>('full'); // ✅ Signal output taskCompleted = output<string>(); // ❌ Decorator style — legacy @Input() task!: Task; @Output() taskCompleted = new EventEmitter<string>(); ``` 2. **Content projection** — use `<ng-content>` for composable UI, `select` attribute for named slots: ```typescript @Component({ template: ` <header><ng-content select="[card-header]" /></header> <main><ng-content /></main> ` }) ``` 3. **Signal-based view queries** (Angular 17.2+): ```typescript canvas = viewChild.required<ElementRef>('canvas'); // ✅ Reactive, no AfterViewInit items = contentChildren(TabItemComponent); // ✅ Content query ``` 4. **Lifecycle hooks guidance:** - `OnInit` — fetch initial data, set up subscriptions (use `inject(DestroyRef)` for cleanup) - `OnDestroy` — manual cleanup only if `takeUntilDestroyed` or `DestroyRef` cannot be used - **Avoid `OnChanges`** — use signal inputs with `computed()` or `effect()` instead ### Template Patterns 1. **Use new control flow syntax** (Angular 17+) — `@for`, `@if`, `@switch`, `@defer`: ```html <!-- ✅ New control flow with track expression --> @for (task of tasks(); track task.id) { <app-task-card [task]="task" /> } @empty { <p>No tasks found.</p> } @if (isLoading()) { <app-spinner /> } @else { <app-task-list [tasks]="tasks()" /> } @switch (task().priority) { @case ('high') { <span class="badge-high">High</span> } @case ('medium') { <span class="badge-med">Medium</span> } @default { <span class="badge-low">Low</span> } } ``` 2. **`@defer` for lazy-loaded template blocks:** ```html <!-- ✅ Lazy-load heavy component --> @defer (on viewport) { <app-task-analytics [tasks]="tasks()" /> } @placeholder { <div class="skeleton" /> } ``` 3. **Use `ng-container`** for grouping without extra DOM nodes — e.g., `<ng-container *ngTemplateOutlet="tpl" />`. 4. **Avoid complex expressions in templates** — extract to `computed()`: ```typescript // ❌ template: `{{ tasks().filter(t => t.done).length }} / {{ tasks().length }}` // ✅ Extract to computed summary = computed(() => `${this.doneTasks().length} / ${this.tasks().length}`); ``` ### Dependency Injection 1. **`inject()` function** over constructor injection in standalone components. 2. **Provide at appropriate level** — component, route, or root. 3. **Abstract services behind interfaces** for testability: ```typescript // ✅ Abstract class as interface (TypeScript has no runtime interfaces) export abstract class TaskStorage { abstract getById(id: string): Observable<Task>; abstract save(task: Task): Observable<void>; } // ✅ Implementation @Injectable() export class HttpTaskStorage extends TaskStorage { private readonly http = inject(HttpClient); getById(id: string) { return this.http.get<Task>(`/api/tasks/${id}`); } save(task: Task) { return this.http.post<void>('/api/tasks', task); } } // ✅ Wired at route or root level providers: [{ provide: TaskStorage, useClass: HttpTaskStorage }] ``` ### Reactive Forms 1. **Reactive forms over template-driven** for complex forms. 2. **Typed forms** (`FormControl<string>`) — always. 3. **Custom validators** as pure functions: ```typescript // ✅ Typed form group with nonNullable controls export class TaskFormComponent { form = new FormGroup({ title: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.maxLength(200)] }), priority: new FormControl<'low' | 'medium' | 'high'>('medium', { nonNullable: true }), }); } ``` ### Routing Patterns 1. **Functional guards** (Angular 15+) — no class-based guards: ```typescript // ✅ Functional guard export const authGuard: CanActivateFn = (route, state) => { const auth = inject(AuthService); return auth.isAuthenticated() || inject(Router).createUrlTree(['/login']); }; // ❌ Class-based guard — deprecated @Injectable() export class AuthGuard implements CanActivate { ... } ``` 2. **Functional resolvers** — same pattern, use `ResolveFn<T>`: ```typescript export const taskResolver: ResolveFn<Task> = (route) => inject(TaskService).getById(route.paramMap.get('id')!); ``` 3. **Lazy loading with `loadChildren`** for feature routes: ```typescript { path: 'tasks', loadChildren: () => import('./features/task/task.routes') .then(m => m.TASK_ROUTES), canActivate: [authGuard], } ``` 4. **Route parameter binding with `input()`** (Angular 16+): ```typescript // In app.config.ts: withComponentInputBinding() // ✅ Route params bound as signal inputs — no ActivatedRoute needed taskId = input.required<string>(); // ❌ Manual route param subscription this.route.paramMap.pipe(...).subscribe(...) ``` ### State Management 1. **Component signals first** — sufficient for most local UI state. 2. **NgRx Signal Store** for shared or complex state beyond a single component: ```typescript // ✅ NgRx Signal Store export const TaskStore = signalStore( { providedIn: 'root' }, withState<TaskState>({ tasks: [], isLoading: false, error: null }), withComputed(({ tasks }) => ({ completedTasks: computed(() => tasks().filter(t => t.done)), })), withMethods((store, taskService = inject(TaskService)) => ({ async loadTasks(): Promise<void> { patchState(store, { isLoading: true }); const tasks = await firstValueFrom(taskService.getAll()); patchState(store, { tasks, isLoading: false }); }, })), ); ``` 3. **When to use what:** - **`signal()`** — local component state, simple parent-child data flow - **NgRx Signal Store** — shared state across components, entity management (`withEntities()`) - **RxJS + services** — real-time streams, WebSocket data, complex async orchestration ### Error Handling > For universal error handling principles, see `.agents/rules/error-handling-principles.md`. Below: Angular-specific patterns only. 1. **Global error handler** for uncaught exceptions: ```typescript @Injectable() export class GlobalErrorHandler implements ErrorHandler { handleError(error: unknown): void { this.logger.error('unhandled_error', { error }); // Log to observability platform } } ``` 2. **HTTP interceptor** for centralized error handling: ```typescript export const errorInterceptor: HttpInterceptorFn = (req, next) => next(req).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { // Redirect to login } return throwError(() => error); }) ); ``` 3. **RxJS error handling** — never leave Observables unhandled. Always use `catchError` in `.pipe()` or handle in `subscribe()` error callback. ### Anti-Patterns - ❌ **NgModules for new components** — use standalone components (default since v19) - ❌ **BehaviorSubject for simple component state** — use signals - ❌ **Constructor injection in standalone components** — use `inject()` - ❌ **Manual subscriptions without cleanup** — use `takeUntilDestroyed()` or `async` pipe - ❌ **`any` in template bindings** — type everything - ❌ **Direct DOM manipulation** — use Angular's renderer or signals - ❌ **`subscribe()` in components without unsubscribe** — prefer `async` pipe or `toSignal()`
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub