Skill v1.0.1
currentLLM-judged scan90/1003 files
version: "1.0.1" 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. metadata: category: development suggest_for: filename:
- '*.component.ts'
- '*.component.html'
source: repository: 'https://github.com/analogjs/angular-skills' path: skills/angular-component license_path: LICENSE commit: 610c90eb9490194bcff703f343f97fa0e00bdb2f
Angular Component
Create standalone components for Angular v20+. Components are standalone by default—do NOT set standalone: true.
Component Structure
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 UserCard {// Required inputname = input.required<string>();// Optional input with defaultemail = input<string>('');showEmail = input(false);// Input with transformisActive = input(false, { transform: booleanAttribute });// Computed from inputsavatarUrl = computed(() => `https://api.example.com/avatar/${this.name()}`);// Outputselected = output<string>();handleClick() {this.selected.emit(this.name());}}
Signal Inputs
// Required - must be provided by parentname = input.required<string>();// Optional with default valuecount = input(0);// Optional without default (undefined allowed)label = input<string>();// With alias for template bindingsize = input('medium', { alias: 'buttonSize' });// With transform functiondisabled = input(false, { transform: booleanAttribute });value = input(0, { transform: numberAttribute });
Signal Outputs
import { output, outputFromObservable } from '@angular/core';// Basic outputclicked = output<void>();selected = output<Item>();// With aliasvalueChange = output<number>({ alias: 'change' });// From Observable (for RxJS interop)scroll$ = new Subject<number>();scrolled = outputFromObservable(this.scroll$);// Emit valuesthis.clicked.emit();this.selected.emit(item);
Host Bindings
Use the host object in @Component—do NOT use @HostBinding or @HostListener decorators.
@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 Button {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
@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 Card {}// Usage:// <app-card>// <h2 card-header>Title</h2>// <p>Main content</p>// <button card-footer>Action</button>// </app-card>
Lifecycle Hooks
import { OnDestroy, OnInit, afterNextRender, afterRender } from '@angular/core';export class My implements OnInit, OnDestroy {constructor() {// For DOM manipulation after render (SSR-safe)afterNextRender(() => {// Runs once after first render});afterRender(() => {// Runs after every render});}ngOnInit() { /* Component initialized */ }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
@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 Toggle {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.
<!-- 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:
<!-- 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:
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 Hero {imageUrl = input.required<string>();}
For detailed patterns, see references/component-patterns.md.