| 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
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: `
<div class="component-container">
@if (loading()) {
<nz-spin nzSimple />
} @else if (hasError()) {
<nz-alert
nzType="error"
[nzMessage]="errorMessage()!"
nzShowIcon
/>
} @else {
<div class="content">
@for (item of items(); track item.id) {
<app-item-card
[item]="item"
(itemChange)="handleItemChange($event)"
/>
} @empty {
<nz-empty nzNotFoundContent="No items found" />
}
</div>
}
</div>
`,
styles: [`
.component-container {
padding: 24px;
}
.content {
display: grid;
gap: 16px;
}
`]
})
export class YourComponent {
private yourService = inject(YourService);
blueprintId = input.required<string>();
readonly = ();
itemChange = output<>();
loading = ();
error = signal< | >();
items = signal<[]>([]);
hasError = ( .() !== );
errorMessage = ( .());
totalItems = ( .().);
() {
( {
id = .();
.(, id);
.(id);
});
}
(): {
.(.());
}
(: ): <> {
..();
..();
{
items = ..(blueprintId);
..(items);
} (err) {
..(err ? err. : );
} {
..();
}
}
(: ): {
..(
items.( i. === item. ? item : i)
);
..(item);
}
}
Key Patterns
1. Signal State Management
private _items = signal<Item[]>([]);
items = this._items.asReadonly();
filteredItems = computed(() =>
this._items().filter(item => item.status === 'active')
);
this._items.set([...]);
this._items.update(items => [...items, newItem]);
2. Control Flow Syntax
@if (condition()) {
<div>Content</div>
} @else if (otherCondition()) {
<div>Other</div>
} @else {
<div>Default</div>
}
@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
} @empty {
<p>No items</p>
}
@switch (status()) {
@case ('active') { <span class="badge-success">Active</span> }
@case ('inactive') { <span class="badge-danger">Inactive</span> }
@default { Unknown }
}
<div *ngIf=>...</div>
3. Input/Output Functions
task = input.required<Task>();
readonly = input(false);
taskChange = output<Task>();
@Input() task!: Task;
@Output() taskChange = new EventEmitter<Task>();
4. Dependency Injection
private taskService = inject(TaskService);
private router = inject(Router);
private destroyRef = inject(DestroyRef);
constructor(
private taskService: TaskService,
private router: Router
) {}
5. Subscriptions
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
this.service.data$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => this.items.set(data));
}
ngOnInit(): void {
this.service.data$.subscribe(data => this.items.set(data));
}
Component Types
Smart Component (Container)
@Component({
selector: 'app-task-list',
standalone: true,
imports: [SHARED_IMPORTS, TaskItemComponent],
template: `
@for (task of tasks(); track task.id) {
<app-task-item
[task]="task"
(taskChange)="updateTask($event)"
/>
}
`
})
export class TaskListComponent {
private taskService = inject(TaskService);
tasks = signal<Task[]>([]);
ngOnInit(): void {
this.loadTasks();
}
async loadTasks(): Promise<void> {
const tasks = await this.taskService.getTasks();
this.tasks.set(tasks);
}
async updateTask(task: Task): Promise<void> {
await this.taskService.updateTask(task.id, task);
this.tasks.update( =>
tasks.( t. === task. ? task : t)
);
}
}
Presentational Component (Pure)
@Component({
selector: 'app-task-item',
standalone: true,
imports: [SHARED_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<nz-card>
<h3>{{ task().title }}</h3>
<p>{{ task().description }}</p>
<button nz-button (click)="handleComplete()">
Complete
</button>
</nz-card>
`
})
export class TaskItemComponent {
task = input.required<Task>();
taskChange = output<Task>();
handleComplete(): void {
const updated = { ...this.task(), status: 'completed' };
this.taskChange.emit(updated);
}
}
Form Handling
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-task-form',
standalone: true,
imports: [SHARED_IMPORTS, ReactiveFormsModule],
template: `
<form nz-form [formGroup]="form" (ngSubmit)="handleSubmit()">
<nz-form-item>
<nz-form-label nzRequired>Title</nz-form-label>
<nz-form-control nzErrorTip="Please enter task title">
<input nz-input formControlName="title" />
</nz-form-control>
</nz-form-item>
<button nz-button nzType="primary" [disabled]="!form.valid">
Submit
</button>
</form>
`
})
export class TaskFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
title: ['', [Validators.required, Validators.maxLength(200)]],
description: [''],
status: ['pending']
});
taskSubmit = output<Partial<Task>>();
handleSubmit(): void {
if (this.form.valid) {
..(..);
..();
}
}
}
Checklist
When creating a component:
References