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

{{ name() }}

@if (showEmail()) {

{{ email() }}

} `, styles: ` :host { display: block; } :host.active { border: 2px solid blue; } `, }) export class UserCardComponent { // Required input name = input.required(); // Optional input with default email = input(''); 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(); handleClick() { this.selected.emit(this.name()); } } ``` ## Signal Inputs ```typescript // Required - must be provided by parent name = input.required(); // Optional with default value count = input(0); // Optional without default (undefined allowed) label = input(); // 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(); selected = output(); // With alias valueChange = output({ alias: 'change' }); // From Observable (for RxJS interop) scroll$ = new Subject(); 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: ``, }) export class ButtonComponent { variant = input<'primary' | 'secondary'>('primary'); disabled = input(false, { transform: booleanAttribute }); color = input('#007bff'); clicked = output(); onClick(event: Event) { if (!this.disabled()) { this.clicked.emit(); } } } ``` ## Content Projection ```typescript @Component({ selector: 'app-card', template: `
`, }) export class CardComponent {} // Usage: // //

Title

//

Main content

// //
``` ## 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: ``, }) export class ToggleComponent { label = input.required(); checked = input(false, { transform: booleanAttribute }); checkedChange = output(); toggle() { this.checkedChange.emit(!this.checked()); } } ``` ## Template Syntax Use native control flow—do NOT use `*ngIf`, `*ngFor`, `*ngSwitch`. ```html @if (isLoading()) { } @else if (error()) { } @else { } @for (item of items(); track item.id) { } @empty {

No items found

} @switch (status()) { @case ('pending') { Pending } @case ('active') { Active } @default { Unknown } } ``` ## Class and Style Bindings Do NOT use `ngClass` or `ngStyle`. Use direct bindings: ```html
Single class
Class string
Styled text
With unit
``` ## Images Use `NgOptimizedImage` for static images: ```typescript import { NgOptimizedImage } from '@angular/common'; @Component({ imports: [NgOptimizedImage], template: ` `, }) export class HeroComponent { imageUrl = input.required(); } ``` For detailed patterns, see [references/component-patterns.md](references/component-patterns.md).