ワンクリックで
angular-idioms
Angular components, signals, DI, RxJS, standalone architecture. For TypeScript see typescript-idioms.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Angular components, signals, DI, RxJS, standalone architecture. For TypeScript see typescript-idioms.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
| 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","**/tsconfig*.json"] |
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, seereferences/project-structure.md.
Standalone components — no NgModules for new components (standalone is the default since Angular 19):
@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[]>([]);
}
Lazy-load routes with loadComponent:
{ path: 'tasks', loadComponent: () => import('./features/task/task-list.component')
.then(m => m.TaskListComponent) }
computed for derived state. effect for side effects.// ✅ 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
}
}
OnPush is the default strategy — set it on every component:
// ✅ Always OnPush
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})
OnPush works naturally with signals — signal reads in templates automatically trigger change detection when the signal value changes.
Use Default only when wrapping third-party components that mutate state imperatively and cannot be refactored.
Never call ChangeDetectorRef.detectChanges() manually — if you need it, your data flow is wrong. Convert to signals or use async pipe.
Signal-based inputs and outputs (Angular 17.1+) — prefer over decorators:
// ✅ 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>();
Content projection — use <ng-content> for composable UI, select attribute for named slots:
@Component({
template: `
<header><ng-content select="[card-header]" /></header>
<main><ng-content /></main>
`
})
Signal-based view queries (Angular 17.2+):
canvas = viewChild.required<ElementRef>('canvas'); // ✅ Reactive, no AfterViewInit
items = contentChildren(TabItemComponent); // ✅ Content query
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 usedOnChanges — use signal inputs with computed() or effect() insteadUse new control flow syntax (Angular 17+) — @for, @if, @switch, @defer:
<!-- ✅ 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> }
}
@defer for lazy-loaded template blocks:
<!-- ✅ Lazy-load heavy component -->
@defer (on viewport) {
<app-task-analytics [tasks]="tasks()" />
} @placeholder {
<div class="skeleton" />
}
Use ng-container for grouping without extra DOM nodes — e.g., <ng-container *ngTemplateOutlet="tpl" />.
Avoid complex expressions in templates — extract to computed():
// ❌ template: `{{ tasks().filter(t => t.done).length }} / {{ tasks().length }}`
// ✅ Extract to computed
summary = computed(() => `${this.doneTasks().length} / ${this.tasks().length}`);
inject() function over constructor injection in standalone components.// ✅ 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 }]
FormControl<string>) — always.// ✅ 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 }),
});
}
Functional guards (Angular 15+) — no class-based guards:
// ✅ 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 { ... }
Functional resolvers — same pattern, use ResolveFn<T>:
export const taskResolver: ResolveFn<Task> = (route) =>
inject(TaskService).getById(route.paramMap.get('id')!);
Lazy loading with loadChildren for feature routes:
{
path: 'tasks',
loadChildren: () => import('./features/task/task.routes')
.then(m => m.TASK_ROUTES),
canActivate: [authGuard],
}
Route parameter binding with input() (Angular 16+):
// 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(...)
Component signals first — sufficient for most local UI state.
NgRx Signal Store for shared or complex state beyond a single component:
// ✅ 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 });
},
})),
);
When to use what:
signal() — local component state, simple parent-child data flowwithEntities())For universal error handling principles, see
.agents/rules/error-handling-principles.md. Below: Angular-specific patterns only.
Global error handler for uncaught exceptions:
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: unknown): void {
this.logger.error('unhandled_error', { error }); // Log to observability platform
}
}
HTTP interceptor for centralized error handling:
export const errorInterceptor: HttpInterceptorFn = (req, next) =>
next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Redirect to login
}
return throwError(() => error);
})
);
RxJS error handling — never leave Observables unhandled. Always use catchError in .pipe() or handle in subscribe() error callback.
inject()takeUntilDestroyed() or async pipeany in template bindings — type everythingsubscribe() in components without unsubscribe — prefer async pipe or toSignal()ChangeDetectionStrategy.Default without justification — always OnPush*ngFor / *ngIf in new code — use @for / @if control flow// ❌ Memory leak — subscription never cleaned up
ngOnInit() {
this.taskService.getTasks().subscribe(tasks => this.tasks = tasks);
}
// ✅ Auto-cleanup with takeUntilDestroyed
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.taskService.getTasks().pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(tasks => this.tasks.set(tasks));
}
// ✅ Even better — convert to signal
tasks = toSignal(this.taskService.getTasks(), { initialValue: [] });
File naming — dot-separated with type suffix:
task-list.component.ts, task.service.ts, task.pipe.ts, task.guard.ts, task.directive.tstask.routes.ts for feature route definitionstask-list.component.spec.ts for tests (co-located)Selector prefixes — use app- (or project-specific prefix from angular.json): selector: 'app-task-card'
Class naming — suffix matches file type: TaskListComponent, TaskService, HighlightDirective, DateFormatPipe
Route file exports — UPPER_SNAKE_CASE: export const TASK_ROUTES: Routes = [...]
For universal testing principles, see
.agents/rules/testing-strategy.md. Below: Angular-specific patterns only.
Angular Testing Library for component tests (preferred over TestBed):
import { render, screen } from '@testing-library/angular';
it('should display task title', async () => {
await render(TaskCardComponent, {
componentInputs: { task: mockTask },
});
expect(screen.getByText('Deploy fix')).toBeInTheDocument();
});
Spectator for service tests:
const spectator = createServiceFactory({
service: TaskService,
mocks: [TaskStorage],
});
HttpTestingController for HTTP service tests — no live backend.
Signal Store testing — test store methods directly, assert signal values:
it('should load tasks', async () => {
const store = TestBed.inject(TaskStore);
await store.loadTasks();
expect(store.tasks().length).toBeGreaterThan(0);
});
| Tool | Purpose | Command |
|---|---|---|
| Prettier | Formatting | npx prettier --write . |
| ESLint + angular-eslint | Linting | npx ng lint |
strict mode | Type checking | "strict": true in tsconfig.json |
| Angular compiler | Template checking | npx ng build (checks templates) |
Structured fault tolerance for coordinator agents. 5-level escalation ladder (Retry → Replace → Skip → Redistribute → Degrade), dead-man timers, degraded completion protocol, and cross-level escalation format. Load when orchestrating agents that may fail.
Structured code review protocol for inspecting code quality against the full rule set. Use when auditing code written by yourself or another agent, during the /audit workflow, or when the user asks for a code review.
Reusable convergence protocol for coordinator agents. Defines the BRIEFING → ITERATE → GATE → CONVERGE loop, context hygiene rules, self-succession protocol, turn budget, and handoff compression. Load when orchestrating multi-iteration workflows.
Pre-flight checklist and post-implementation self-review protocol. Use before generating any code (pre-flight) and after writing code but before verification (self-review) to catch issues early.
MECE task decomposition, file ownership enforcement, DAG-based execution, and safe merge protocol for intra-domain parallel dispatch. The safety invariants that prevent merge chaos when multiple agents write in parallel. Applies recursively at every nesting depth.
Shared protocols for all agents in the multi-agent pipeline: recursive nesting, pre-implementation restatement, parallel dispatch format, and agent definition cascade. Load this skill instead of inlining these protocols in every agent file.