Get the FREE Ultimate OpenClaw Setup Guide →

angular-20-standalone-component

npx machina-cli add skill aiskillstore/marketplace/angular-20-standalone-component --openclaw
Files (1)
SKILL.md
9.2 KB

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

Source

git clone https://github.com/aiskillstore/marketplace/blob/main/skills/7spade/angular-20-standalone-component/SKILL.mdView on GitHub

Overview

Build Angular 20 standalone components using modern patterns: signals for reactive state, input()/output() for props and events, and inject() for DI, all with OnPush change detection. This aligns UI components with a three-layer architecture: presentation, services, and EventBus-driven communication.

How This Skill Works

State is managed with signal(), derived with computed(), and side effects with effect(). Inputs and events are declared via input() and output() (no decorators). The template uses @if/@for/@switch for flow control, while dependencies are resolved with inject(). Components run with ChangeDetectionStrategy.OnPush for performance.

When to Use It

  • Scaffolding a new UI component that needs reactive state and local form handling
  • Wiring services via inject() without using constructors
  • Building components that follow a three-layer architecture (presentation, services, EventBus)
  • Rendering dynamic lists with @for and handling item-level events
  • Optimizing performance in high-frequency UI by using OnPush and signals

Quick Start

  1. Step 1: Scaffold a standalone component with 100% standalone, imports, and ChangeDetectionStrategy.OnPush
  2. Step 2: Add signals (e.g., loading, items) and inject() dependencies; declare input() and output() props/events
  3. Step 3: Implement the template using @if/@for/@switch and wire up itemChange events with an effect() to load data on input changes

Best Practices

  • Use signal() for all mutable component state (loading, items, errors) instead of class fields
  • Declare inputs and outputs with input() and output() to avoid decorators
  • Compute derived state with computed() to minimize re-renders
  • Inject services with inject(); never inject repositories directly
  • Enable ChangeDetectionStrategy.OnPush on all standalone components and rely on signals for updates

Example Use Cases

  • A dashboard widget that loads data via a service, shows a loading spinner with loading()
  • A list component rendering items with @for and emitting item changes through itemChange
  • A form-like component using signals for field values and validation messages
  • A component reacting to global events through an EventBus integration
  • A parent-child communication scenario using input signals and output events

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers