home / skills / analogjs / angular-skills / angular-component

angular-component skill

/skills/angular-component

This skill helps you create modern Angular standalone components using v20+ best practices, enabling signal-based inputs/outputs, OnPush, host bindings, and

npx playbooks add skill analogjs/angular-skills --skill angular-component

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
6.7 KB
---
name: angular-component
description: Create modern Angular standalone components following v20+ best practices. Use for building UI components with signal-based inputs/outputs, OnPush change detection, host bindings, content projection, and lifecycle hooks. Triggers on component creation, refactoring class-based inputs to signals, adding host bindings, or implementing accessible interactive components.
---

# Angular Component

Create standalone components for Angular v20+. Components are standalone by default—do NOT set `standalone: true`.

## Component Structure

```typescript
import { Component, ChangeDetectionStrategy, input, output, computed } from '@angular/core';

@Component({
  selector: 'app-user-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    'class': 'user-card',
    '[class.active]': 'isActive()',
    '(click)': 'handleClick()',
  },
  template: `
    <img [src]="avatarUrl()" [alt]="name() + ' avatar'" />
    <h2>{{ name() }}</h2>
    @if (showEmail()) {
      <p>{{ email() }}</p>
    }
  `,
  styles: `
    :host { display: block; }
    :host.active { border: 2px solid blue; }
  `,
})
export class UserCardComponent {
  // Required input
  name = input.required<string>();
  
  // Optional input with default
  email = input<string>('');
  showEmail = input(false);
  
  // Input with transform
  isActive = input(false, { transform: booleanAttribute });
  
  // Computed from inputs
  avatarUrl = computed(() => `https://api.example.com/avatar/${this.name()}`);
  
  // Output
  selected = output<string>();
  
  handleClick() {
    this.selected.emit(this.name());
  }
}
```

## Signal Inputs

```typescript
// Required - must be provided by parent
name = input.required<string>();

// Optional with default value
count = input(0);

// Optional without default (undefined allowed)
label = input<string>();

// With alias for template binding
size = input('medium', { alias: 'buttonSize' });

// With transform function
disabled = input(false, { transform: booleanAttribute });
value = input(0, { transform: numberAttribute });
```

## Signal Outputs

```typescript
import { output, outputFromObservable } from '@angular/core';

// Basic output
clicked = output<void>();
selected = output<Item>();

// With alias
valueChange = output<number>({ alias: 'change' });

// From Observable (for RxJS interop)
scroll$ = new Subject<number>();
scrolled = outputFromObservable(this.scroll$);

// Emit values
this.clicked.emit();
this.selected.emit(item);
```

## Host Bindings

Use the `host` object in `@Component`—do NOT use `@HostBinding` or `@HostListener` decorators.

```typescript
@Component({
  selector: 'app-button',
  host: {
    // Static attributes
    'role': 'button',
    
    // Dynamic class bindings
    '[class.primary]': 'variant() === "primary"',
    '[class.disabled]': 'disabled()',
    
    // Dynamic style bindings
    '[style.--btn-color]': 'color()',
    
    // Attribute bindings
    '[attr.aria-disabled]': 'disabled()',
    '[attr.tabindex]': 'disabled() ? -1 : 0',
    
    // Event listeners
    '(click)': 'onClick($event)',
    '(keydown.enter)': 'onClick($event)',
    '(keydown.space)': 'onClick($event)',
  },
  template: `<ng-content />`,
})
export class ButtonComponent {
  variant = input<'primary' | 'secondary'>('primary');
  disabled = input(false, { transform: booleanAttribute });
  color = input('#007bff');
  
  clicked = output<void>();
  
  onClick(event: Event) {
    if (!this.disabled()) {
      this.clicked.emit();
    }
  }
}
```

## Content Projection

```typescript
@Component({
  selector: 'app-card',
  template: `
    <header>
      <ng-content select="[card-header]" />
    </header>
    <main>
      <ng-content />
    </main>
    <footer>
      <ng-content select="[card-footer]" />
    </footer>
  `,
})
export class CardComponent {}

// Usage:
// <app-card>
//   <h2 card-header>Title</h2>
//   <p>Main content</p>
//   <button card-footer>Action</button>
// </app-card>
```

## Lifecycle Hooks

```typescript
import { 
  AfterContentInit, AfterViewInit, OnDestroy, OnInit,
  afterNextRender, afterRender
} from '@angular/core';

export class MyComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy {
  constructor() {
    // For DOM manipulation after render (SSR-safe)
    afterNextRender(() => {
      // Runs once after first render
    });
    
    afterRender(() => {
      // Runs after every render
    });
  }
  
  ngOnInit() { /* Component initialized */ }
  ngAfterContentInit() { /* Projected content ready */ }
  ngAfterViewInit() { /* View children ready */ }
  ngOnDestroy() { /* Cleanup */ }
}
```

## Accessibility Requirements

Components MUST:
- Pass AXE accessibility checks
- Meet WCAG AA standards
- Include proper ARIA attributes for interactive elements
- Support keyboard navigation
- Maintain visible focus indicators

```typescript
@Component({
  selector: 'app-toggle',
  host: {
    'role': 'switch',
    '[attr.aria-checked]': 'checked()',
    '[attr.aria-label]': 'label()',
    'tabindex': '0',
    '(click)': 'toggle()',
    '(keydown.enter)': 'toggle()',
    '(keydown.space)': 'toggle(); $event.preventDefault()',
  },
  template: `<span class="toggle-track"><span class="toggle-thumb"></span></span>`,
})
export class ToggleComponent {
  label = input.required<string>();
  checked = input(false, { transform: booleanAttribute });
  checkedChange = output<boolean>();
  
  toggle() {
    this.checkedChange.emit(!this.checked());
  }
}
```

## Template Syntax

Use native control flow—do NOT use `*ngIf`, `*ngFor`, `*ngSwitch`.

```html
<!-- Conditionals -->
@if (isLoading()) {
  <app-spinner />
} @else if (error()) {
  <app-error [message]="error()" />
} @else {
  <app-content [data]="data()" />
}

<!-- Loops -->
@for (item of items(); track item.id) {
  <app-item [item]="item" />
} @empty {
  <p>No items found</p>
}

<!-- Switch -->
@switch (status()) {
  @case ('pending') { <span>Pending</span> }
  @case ('active') { <span>Active</span> }
  @default { <span>Unknown</span> }
}
```

## Class and Style Bindings

Do NOT use `ngClass` or `ngStyle`. Use direct bindings:

```html
<!-- Class bindings -->
<div [class.active]="isActive()">Single class</div>
<div [class]="classString()">Class string</div>

<!-- Style bindings -->
<div [style.color]="textColor()">Styled text</div>
<div [style.width.px]="width()">With unit</div>
```

## Images

Use `NgOptimizedImage` for static images:

```typescript
import { NgOptimizedImage } from '@angular/common';

@Component({
  imports: [NgOptimizedImage],
  template: `
    <img ngSrc="/assets/hero.jpg" width="800" height="600" priority />
    <img [ngSrc]="imageUrl()" width="200" height="200" />
  `,
})
export class HeroComponent {
  imageUrl = input.required<string>();
}
```

For detailed patterns, see [references/component-patterns.md](references/component-patterns.md).

Overview

This skill creates modern Angular standalone components aligned with Angular v20+ signal-based APIs and OnPush change detection. It focuses on signal inputs/outputs, host bindings, content projection, lifecycle hooks, and accessibility to produce fast, testable UI components. Use it to generate or refactor components to current best practices and SSR-safe rendering patterns.

How this skill works

The skill inspects component code and generates or refactors TypeScript + template snippets that use signal-based input() and output(), computed signals, and outputFromObservable where appropriate. It replaces decorator-based host bindings with the host object in @Component, converts class-based inputs to signals, enforces template control-flow syntax (@if/@for/@switch), and wires lifecycle helpers like afterNextRender and afterRender for DOM-safe work. It also ensures ARIA attributes, keyboard handlers, and focus management for accessibility.

When to use it

  • Creating a new standalone UI component for Angular v20+
  • Refactoring legacy class-based inputs/outputs to signal-based APIs
  • Adding host attribute/class/style bindings or event listeners
  • Implementing accessible interactive components (buttons, toggles, menus)
  • Converting templates to the native control-flow syntax and optimized image usage

Best practices

  • Prefer input.required<T>() for required inputs and input(default) for optional values
  • Use computed() for derived values and output()/outputFromObservable() for events
  • Declare host bindings via the host object in @Component instead of decorators
  • Use afterNextRender/afterRender for DOM work to remain SSR-safe
  • Ensure AXE and WCAG AA compliance: ARIA attributes, keyboard handlers, and visible focus styles

Example use cases

  • Generate a user-card component with signal inputs, a computed avatar URL, and a selected output event
  • Refactor a button to use host bindings for role, tabindex, class toggles, and keyboard activation
  • Build a toggle/switch component that exposes checkedChange output with proper aria-checked and keyboard support
  • Create a card component that uses content projection slots for header, main, and footer regions
  • Replace ngClass/ngStyle and structural directives with direct class/style bindings and native control-flow

FAQ

Should components be marked standalone: true?

No. Components are standalone by default in this pattern; do not set standalone: true.

How do I handle DOM work safely for SSR?

Use afterNextRender for one-time post-render tasks and afterRender for repeated post-render operations; avoid direct DOM access in constructors.