Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/aiskillstore-angular-20-standalone-component/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-22 | ✗→✓ | ▲ Improved | 126% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 32% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 43% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 44% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 55% | 0% |
This skill helps create Angular 20 components following modern patterns and project standards.
signal(), computed(), effect() for stateinput(), output(), @if, @for, @switchtypescriptimport { 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); } }
typescript// 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
typescript// ✅ 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>
typescript// ✅ 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>();
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 ) {}
typescriptimport { 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)); }
typescript@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) ); } }
typescript@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); } }
typescriptimport { 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(); } } }
When creating a component:
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-17 | pass→pass | 19,896 | 17,894 | -10% | 1 | 1 | 0% | 4,103 | 6,467 | +58% | 0 | 0 | — |
case-22 | fail→pass | 16,464 | 22,493 | +37% | 1 | 1 | 0% | 3,360 | 7,577 | +126% | 0 | 0 | — |
case-01 | fail→pass | 19,765 | 14,436 | -27% | 1 | 1 | 0% | 4,376 | 5,755 | +32% | 0 | 0 | — |
case-02 | fail→pass | 20,041 | 19,911 | -1% | 1 | 1 | 0% | 4,814 | 6,905 | +43% | 0 | 0 | — |
case-07 | fail→pass | 19,696 | 17,037 | -14% | 1 | 1 | 0% | 4,254 | 6,136 | +44% | 0 | 0 | — |
case-03 | fail→pass | 17,755 | 16,234 | -9% | 1 | 1 | 0% | 4,022 | 6,226 | +55% | 0 | 0 | — |
case-04 | pass→pass | 10,509 | 7,836 | -25% | 1 | 1 | 0% | 2,048 | 4,141 | +102% | 0 | 0 | — |
case-05 | pass→pass | 8,624 | 5,734 | -34% | 1 | 1 | 0% | 1,700 | 3,630 | +114% | 0 | 0 | — |
case-06 | fail→fail | 7,460 | 6,965 | -7% | 1 | 1 | 0% | 1,311 | 3,775 | +188% | 0 | 0 | — |
case-08 | pass→pass | 12,013 | 11,629 | -3% | 1 | 1 | 0% | 2,332 | 4,966 | +113% | 0 | 0 | — |
case-09 | pass→pass | 14,366 | 21,584 | +50% | 1 | 1 | 0% | 3,168 | 6,015 | +90% | 0 | 0 | — |
case-10 | pass→pass | 15,820 | 13,471 | -15% | 1 | 1 | 0% | 3,284 | 5,640 | +72% | 0 | 0 | — |
case-11 | pass→pass | 14,618 | 12,969 | -11% | 1 | 1 | 0% | 2,674 | 5,204 | +95% | 0 | 0 | — |
case-12 | fail→pass | 16,971 | 15,332 | -10% | 1 | 1 | 0% | 3,520 | 5,810 | +65% | 0 | 0 | — |
case-13 | fail→fail | 14,297 | 9,699 | -32% | 1 | 1 | 0% | 2,735 | 4,566 | +67% | 0 | 0 | — |
case-14 | fail→pass | 35,122 | 13,010 | -63% | 1 | 1 | 0% | 2,925 | 5,103 | +74% | 0 | 0 | — |
case-15 | pass→pass | 9,826 | 6,382 | -35% | 1 | 1 | 0% | 1,717 | 3,630 | +111% | 0 | 0 | — |
case-16 | pass→pass | 13,027 | 12,880 | -1% | 1 | 1 | 0% | 2,251 | 5,087 | +126% | 0 | 0 | — |
case-18 | fail→pass | 7,230 | 5,375 | -26% | 1 | 1 | 0% | 1,288 | 3,509 | +172% | 0 | 0 | — |
case-19 | pass→pass | 16,799 | 17,643 | +5% | 1 | 1 | 0% | 3,420 | 6,435 | +88% | 0 | 0 | — |
case-20 | pass→pass | 8,372 | 9,704 | +16% | 1 | 1 | 0% | 1,471 | 4,546 | +209% | 0 | 0 | — |
case-21 | pass→pass | 21,636 | 22,254 | +3% | 1 | 1 | 0% | 3,358 | 7,650 | +128% | 0 | 0 | — |
case-23 | fail→pass | 12,866 | 21,529 | +67% | 1 | 1 | 0% | 1,942 | 5,273 | +172% | 0 | 0 | — |
case-24 | pass→pass | 10,408 | 14,715 | +41% | 1 | 1 | 0% | 1,975 | 5,541 | +181% | 0 | 0 | — |
case-25 | pass→pass | 19,214 | 20,373 | +6% | 1 | 1 | 0% | 4,014 | 7,276 | +81% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +36 percentage points is the difference between those two pass rates over the 25 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.