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.

Stars 232
Forks 15

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

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: `
    <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

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

2. Control Flow Syntax

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>

3. Input/Output Functions

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>();

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) {
      <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)

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);
  }
}

Form Handling

typescript
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

Expand your agent's capabilities with these related and highly-rated skills.

aiskillstore/marketplace

perigon-backend

Perigon ASP.NET Core + EF Core + Aspire conventions

232 15
Explore
aiskillstore/marketplace

perigon-agent

Pointers for Copilot/agents to apply Perigon conventions

232 15
Explore
aiskillstore/marketplace

perigon-angular

Angular 21+ standalone/Material/signal conventions for Perigon WebApp

232 15
Explore
aiskillstore/marketplace

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.

232 15
Explore
aiskillstore/marketplace

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.

232 15
Explore
aiskillstore/marketplace

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.

232 15
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results