angular-component
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.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install analogjs-angular-skills-angular-component
Repository
Skill path: skills/angular-component
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.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Frontend, Tech Writer.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: analogjs.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install angular-component into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/analogjs/angular-skills before adding angular-component to shared team environments
- Use angular-component for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
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 UserCard {
// 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 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
```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 Card {}
// Usage:
// <app-card>
// <h2 card-header>Title</h2>
// <p>Main content</p>
// <button card-footer>Action</button>
// </app-card>
```
## Lifecycle Hooks
```typescript
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
```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 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`.
```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 Hero {
imageUrl = input.required<string>();
}
```
For detailed patterns, see [references/component-patterns.md](references/component-patterns.md).
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/component-patterns.md
```markdown
# Angular Component Patterns
## Table of Contents
- [Model Inputs (Two-Way Binding)](#model-inputs-two-way-binding)
- [View Queries](#view-queries)
- [Content Queries](#content-queries)
- [Dependency Injection in Components](#dependency-injection-in-components)
- [Component Communication Patterns](#component-communication-patterns)
- [Dynamic Components](#dynamic-components)
## Model Inputs (Two-Way Binding)
For two-way binding with `[(value)]` syntax:
```typescript
import { Component, model } from '@angular/core';
@Component({
selector: 'app-slider',
host: {
'(input)': 'onInput($event)',
},
template: `
<input
type="range"
[value]="value()"
[min]="min()"
[max]="max()"
/>
<span>{{ value() }}</span>
`,
})
export class Slider {
// Model creates both input and output
value = model(0);
min = input(0);
max = input(100);
onInput(event: Event) {
const target = event.target as HTMLInputElement;
this.value.set(Number(target.value));
}
}
// Usage: <app-slider [(value)]="sliderValue" />
```
Required model:
```typescript
value = model.required<number>();
```
## View Queries
Query elements and components in the template:
```typescript
import { Component, viewChild, viewChildren, ElementRef } from '@angular/core';
@Component({
selector: 'app-gallery',
template: `
<div #container class="gallery">
@for (image of images(); track image.id) {
<app-image-card [image]="image" />
}
</div>
`,
})
export class Gallery {
images = input.required<Image[]>();
// Query single element
container = viewChild.required<ElementRef<HTMLDivElement>>('container');
// Query single component (optional)
firstCard = viewChild(ImageCard);
// Query all matching components
allCards = viewChildren(ImageCard);
}
```
## Content Queries
Query projected content:
```typescript
import { Component, contentChild, contentChildren, effect, signal } from '@angular/core';
@Component({
selector: 'app-tabs',
template: `
<div class="tab-headers">
@for (tab of tabs(); track tab.label()) {
<button
[class.active]="tab === activeTab()"
(click)="selectTab(tab)"
>
{{ tab.label() }}
</button>
}
</div>
<div class="tab-content">
<ng-content />
</div>
`,
})
export class Tabs {
// Query all projected Tab children
tabs = contentChildren(Tab);
// Query single projected element
header = contentChild('tabHeader');
activeTab = signal<Tab | undefined>(undefined);
constructor() {
// Set first tab as active when tabs are available
effect(() => {
const firstTab = this.tabs()[0];
if (firstTab && !this.activeTab()) {
this.activeTab.set(firstTab);
}
});
}
selectTab(tab: Tab) {
this.activeTab.set(tab);
}
}
@Component({
selector: 'app-tab',
template: `<ng-content />`,
host: {
'[class.active]': 'isActive()',
'[style.display]': 'isActive() ? "block" : "none"',
},
})
export class Tab {
label = input.required<string>();
isActive = input(false);
}
```
## Dependency Injection in Components
Use `inject()` function instead of constructor injection:
```typescript
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-dashboard',
template: `...`,
})
export class Dashboard {
private router = inject(Router);
private userService = inject(User);
private config = inject(APP_CONFIG);
// Optional injection
private analytics = inject(Analytics, { optional: true });
// Self-only injection
private localService = inject(Local, { self: true });
navigateToProfile() {
this.router.navigate(['/profile']);
}
}
```
## Component Communication Patterns
### Parent to Child (Inputs)
```typescript
// Parent
@Component({
template: `<app-child [data]="parentData()" [config]="config" />`,
})
export class Parent {
parentData = signal({ name: 'Test' });
config = { theme: 'dark' };
}
// Child
@Component({ selector: 'app-child' })
export class Child {
data = input.required<Data>();
config = input<Config>();
}
```
### Child to Parent (Outputs)
```typescript
// Child
@Component({
selector: 'app-child',
template: `<button (click)="save()">Save</button>`,
})
export class Child {
saved = output<Data>();
save() {
this.saved.emit({ id: 1, name: 'Item' });
}
}
// Parent
@Component({
template: `<app-child (saved)="onSaved($event)" />`,
})
export class Parent {
onSaved(data: Data) {
console.log('Saved:', data);
}
}
```
### Shared Service Pattern
```typescript
// Shared state service
@Injectable({ providedIn: 'root' })
export class Cart {
private items = signal<CartItem[]>([]);
readonly items$ = this.items.asReadonly();
readonly total = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0)
);
addItem(item: CartItem) {
this.items.update(items => [...items, item]);
}
removeItem(id: string) {
this.items.update(items => items.filter(i => i.id !== id));
}
}
// Component A
@Component({ template: `<button (click)="add()">Add</button>` })
export class Product {
private cart = inject(Cart);
product = input.required<Product>();
add() {
this.cart.addItem({ ...this.product(), quantity: 1 });
}
}
// Component B
@Component({ template: `<span>Total: {{ cart.total() }}</span>` })
export class CartSummary {
cart = inject(Cart);
}
```
## Dynamic Components
Using `@defer` for lazy loading:
```typescript
@Component({
template: `
@defer (on viewport) {
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<div class="chart-placeholder">Loading chart...</div>
} @loading (minimum 500ms) {
<app-spinner />
} @error {
<p>Failed to load chart</p>
}
`,
})
export class Dashboard {
chartData = input.required<ChartData>();
}
```
Defer triggers:
- `on viewport` - When element enters viewport
- `on idle` - When browser is idle
- `on interaction` - On user interaction (click, focus)
- `on hover` - On mouse hover
- `on immediate` - Immediately after non-deferred content
- `on timer(500ms)` - After specified delay
- `when condition` - When expression becomes true
```typescript
@Component({
template: `
@defer (on interaction; prefetch on idle) {
<app-comments [postId]="postId()" />
} @placeholder {
<button>Load Comments</button>
}
`,
})
export class Post {
postId = input.required<string>();
}
```
## Attribute Directives on Components
```typescript
@Directive({
selector: '[appHighlight]',
host: {
'[style.backgroundColor]': 'color()',
},
})
export class Highlight {
color = input('yellow', { alias: 'appHighlight' });
}
// Usage on component
@Component({
imports: [Highlight],
template: `<app-card appHighlight="lightblue" />`,
})
export class Page {}
```
## Error Boundaries
```typescript
@Component({
selector: 'app-error-boundary',
template: `
@if (hasError()) {
<div class="error">
<h3>Something went wrong</h3>
<button (click)="retry()">Retry</button>
</div>
} @else {
<ng-content />
}
`,
})
export class ErrorBoundary {
hasError = signal(false);
private errorHandler = inject(ErrorHandler);
retry() {
this.hasError.set(false);
}
}
```
```