Agent skill
angular-20-standalone-component
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.
Install this agent skill to your Project
npx add-skill https://github.com/aiskillstore/marketplace/tree/main/skills/7spade/angular-20-standalone-component
SKILL.md
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 {
// ✅ Inject services with inject()
private yourService = inject(YourService);
// ✅ Use input() for properties (NOT @Input())
blueprintId = input.required<string>();
readonly = input(false);
// ✅ Use output() for events (NOT @Output())
itemChange = output<Item>();
// ✅ Use signal() for mutable state
loading = signal(false);
error = signal<string | null>(null);
items = signal<Item[]>([]);
// ✅ 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<void> {
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
// Writable signals
private _items = signal<Item[]>([]);
// 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
// ✅ CORRECT: New @if syntax
@if (condition()) {
<div>Content</div>
} @else if (otherCondition()) {
<div>Other</div>
} @else {
<div>Default</div>
}
// ✅ CORRECT: New @for syntax with track
@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
} @empty {
<p>No items</p>
}
// ✅ CORRECT: New @switch syntax
@switch (status()) {
@case ('active') { <span class="badge-success">Active</span> }
@case ('inactive') { <span class="badge-danger">Inactive</span> }
@default { <span class="badge-default">Unknown</span> }
}
// ❌ WRONG: Old syntax (forbidden)
<div *ngIf="condition">...</div>
<div *ngFor="let item of items">...</div>
<div [ngSwitch]="status">...</div>
3. Input/Output Functions
// ✅ CORRECT: Use input()/output() functions
task = input.required<Task>();
readonly = input(false);
taskChange = output<Task>();
// ❌ WRONG: Decorators (forbidden)
@Input() task!: Task;
@Output() taskChange = new EventEmitter<Task>();
4. Dependency Injection
// ✅ 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
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)
@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 =>
tasks.map(t => t.id === task.id ? 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) {
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
- Signals State Management
- Architecture Guide
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
perigon-backend
Perigon ASP.NET Core + EF Core + Aspire conventions
perigon-agent
Pointers for Copilot/agents to apply Perigon conventions
perigon-angular
Angular 21+ standalone/Material/signal conventions for Perigon WebApp
fastapi-mastery
Comprehensive FastAPI development skill covering REST API creation, routing, request/response handling, validation, authentication, database integration, middleware, and deployment. Use when working with FastAPI projects, building APIs, implementing CRUD operations, setting up authentication/authorization, integrating databases (SQL/NoSQL), adding middleware, handling WebSockets, or deploying FastAPI applications. Triggered by requests involving .py files with FastAPI code, API endpoint creation, Pydantic models, or FastAPI-specific features.
context7-efficient
Token-efficient library documentation fetcher using Context7 MCP with 86.8% token savings through intelligent shell pipeline filtering. Fetches code examples, API references, and best practices for JavaScript, Python, Go, Rust, and other libraries. Use when users ask about library documentation, need code examples, want API usage patterns, are learning a new framework, need syntax reference, or troubleshooting with library-specific information. Triggers include questions like "Show me React hooks", "How do I use Prisma", "What's the Next.js routing syntax", or any request for library/framework documentation.
browser-use
Browser automation using Playwright MCP. Navigate websites, fill forms, click elements, take screenshots, and extract data. Use when tasks require web browsing, form submission, web scraping, UI testing, or any browser interaction.
Didn't find tool you were looking for?