--- name: angular-20-standalone-component description: "Create Angular 20 standalone components using modern patterns: Signals for state management, input()/output() functions (not decorators), @if/@for/@switch control flow (not *ngIf/*ngFor), inject() dependency injection (not constructor), and OnPush change detection. Use this skill when scaffolding new UI components that need reactive state, form handling, or integration with services following the three-layer architecture." license: "MIT" --- # Angular 20 Standalone Component Skill This skill helps create Angular 20 components following modern patterns and project standards. ## Core Principles ### Modern Angular 20 Patterns - **Standalone Components**: 100% standalone, zero NgModules - **Signals**: Use `signal()`, `computed()`, `effect()` for state - **New Syntax**: `input()`, `output()`, `@if`, `@for`, `@switch` - **inject()**: Function-based dependency injection - **OnPush**: Change detection strategy for performance ### Architecture Integration - **Presentation Layer**: Components handle UI only - **Service Integration**: Inject services for business logic - **No Direct Repository**: Never inject repositories directly - **Event-Driven**: Use EventBus for cross-module communication ## Component Template ```typescript import { Component, signal, computed, effect, input, output, inject, ChangeDetectionStrategy } from '@angular/core'; import { SHARED_IMPORTS } from '@shared'; import { YourService } from '@core/services/your.service'; @Component({ selector: 'app-your-component', standalone: true, imports: [SHARED_IMPORTS], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@if (loading()) { } @else if (hasError()) { } @else {
@for (item of items(); track item.id) { } @empty { }
}
`, styles: [` .component-container { padding: 24px; } .content { display: grid; gap: 16px; } `] }) export class YourComponent { // ✅ Inject services with inject() private yourService = inject(YourService); // ✅ Use input() for properties (NOT @Input()) blueprintId = input.required(); readonly = input(false); // ✅ Use output() for events (NOT @Output()) itemChange = output(); // ✅ Use signal() for mutable state loading = signal(false); error = signal(null); items = signal([]); // ✅ Use computed() for derived state hasError = computed(() => this.error() !== null); errorMessage = computed(() => this.error()); totalItems = computed(() => this.items().length); // ✅ Use effect() for side effects constructor() { effect(() => { const id = this.blueprintId(); console.log('Blueprint ID changed:', id); this.loadItems(id); }); } ngOnInit(): void { this.loadItems(this.blueprintId()); } async loadItems(blueprintId: string): Promise { this.loading.set(true); this.error.set(null); try { const items = await this.yourService.getItems(blueprintId); this.items.set(items); } catch (err) { this.error.set(err instanceof Error ? err.message : 'Unknown error'); } finally { this.loading.set(false); } } handleItemChange(item: Item): void { // Update local state this.items.update(items => items.map(i => i.id === item.id ? item : i) ); // Emit to parent this.itemChange.emit(item); } } ``` ## Key Patterns ### 1. Signal State Management ```typescript // Writable signals private _items = signal([]); // Read-only public access items = this._items.asReadonly(); // Computed derived state filteredItems = computed(() => this._items().filter(item => item.status === 'active') ); // Update signals this._items.set([...]); // Replace this._items.update(items => [...items, newItem]); // Transform ``` ### 2. Control Flow Syntax ```typescript // ✅ CORRECT: New @if syntax @if (condition()) {
Content
} @else if (otherCondition()) {
Other
} @else {
Default
} // ✅ CORRECT: New @for syntax with track @for (item of items(); track item.id) {
{{ item.name }}
} @empty {

No items

} // ✅ CORRECT: New @switch syntax @switch (status()) { @case ('active') { Active } @case ('inactive') { Inactive } @default { Unknown } } // ❌ WRONG: Old syntax (forbidden)
...
...
...
``` ### 3. Input/Output Functions ```typescript // ✅ CORRECT: Use input()/output() functions task = input.required(); readonly = input(false); taskChange = output(); // ❌ WRONG: Decorators (forbidden) @Input() task!: Task; @Output() taskChange = new EventEmitter(); ``` ### 4. Dependency Injection ```typescript // ✅ CORRECT: Use inject() private taskService = inject(TaskService); private router = inject(Router); private destroyRef = inject(DestroyRef); // ❌ WRONG: Constructor injection (forbidden) constructor( private taskService: TaskService, private router: Router ) {} ``` ### 5. Subscriptions ```typescript import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; // ✅ CORRECT: Auto-cleanup with takeUntilDestroyed private destroyRef = inject(DestroyRef); ngOnInit(): void { this.service.data$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(data => this.items.set(data)); } // ❌ WRONG: Manual subscriptions without cleanup ngOnInit(): void { this.service.data$.subscribe(data => this.items.set(data)); } ``` ## Component Types ### Smart Component (Container) ```typescript @Component({ selector: 'app-task-list', standalone: true, imports: [SHARED_IMPORTS, TaskItemComponent], template: ` @for (task of tasks(); track task.id) { } ` }) export class TaskListComponent { private taskService = inject(TaskService); tasks = signal([]); ngOnInit(): void { this.loadTasks(); } async loadTasks(): Promise { const tasks = await this.taskService.getTasks(); this.tasks.set(tasks); } async updateTask(task: Task): Promise { await this.taskService.updateTask(task.id, task); this.tasks.update(tasks => tasks.map(t => t.id === task.id ? task : t) ); } } ``` ### Presentational Component (Pure) ```typescript @Component({ selector: 'app-task-item', standalone: true, imports: [SHARED_IMPORTS], changeDetection: ChangeDetectionStrategy.OnPush, template: `

{{ task().title }}

{{ task().description }}

` }) export class TaskItemComponent { task = input.required(); taskChange = output(); handleComplete(): void { const updated = { ...this.task(), status: 'completed' }; this.taskChange.emit(updated); } } ``` ## Form Handling ```typescript import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-task-form', standalone: true, imports: [SHARED_IMPORTS, ReactiveFormsModule], template: `
Title
` }) export class TaskFormComponent { private fb = inject(FormBuilder); form = this.fb.group({ title: ['', [Validators.required, Validators.maxLength(200)]], description: [''], status: ['pending'] }); taskSubmit = output>(); handleSubmit(): void { if (this.form.valid) { this.taskSubmit.emit(this.form.value); this.form.reset(); } } } ``` ## Checklist When creating a component: - [ ] Standalone component with imports - [ ] Uses signal() for state - [ ] Uses computed() for derived state - [ ] Uses input()/output() functions - [ ] Uses @if/@for/@switch syntax - [ ] Uses inject() for dependencies - [ ] OnPush change detection - [ ] No business logic in component - [ ] Proper error handling - [ ] Loading states - [ ] Empty states - [ ] TypeScript strict typing ## References - [Angular Instructions](.github/instructions/angular.instructions.md) - [Signals State Management](.github/instructions/ng-ng-events(GigHub)-signals-state.instructions.md) - [Architecture Guide](.github/instructions/ng-ng-events(GigHub)-architecture.instructions.md)