angular-component
Installation
SKILL.md
Angular Component
Modern Angular component patterns with standalone components, signals, and OnPush change detection.
Basic Component Structure
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-example',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [],
template: `
<div>Component content</div>
`
})
export class ExampleComponent {}
Signal Inputs (Angular 17.1+)
Required Inputs
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<h2>{{ name() }}</h2>
<p>{{ email() }}</p>
`
})
export class UserCardComponent {
// Required input
name = input.required<string>();
email = input.required<string>();
}
// Usage
<app-user-card [name]="userName" [email]="userEmail" />
Optional Inputs with Defaults
import { Component, input } from '@angular/core';
@Component({
selector: 'app-badge',
template: `
<span [class]="'badge badge-' + variant()">
{{ text() }}
</span>
`
})
export class BadgeComponent {
text = input<string>('Badge');
variant = input<'primary' | 'secondary' | 'success'>('primary');
}
Transformed Inputs
import { Component, input } from '@angular/core';
@Component({
selector: 'app-price',
template: `<span>{{ formattedPrice() }}</span>`
})
export class PriceComponent {
// Transform string to number
price = input(0, {
transform: (value: string | number) =>
typeof value === 'string' ? parseFloat(value) : value
});
formattedPrice = computed(() =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(this.price())
);
}
// Usage: <app-price price="99.99" />
Signal Outputs (Angular 17.3+)
import { Component, output } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button (click)="handleClick()">
{{ label() }}
</button>
`
})
export class ButtonComponent {
label = input<string>('Click me');
clicked = output<MouseEvent>();
handleClick() {
this.clicked.emit(new MouseEvent('click'));
}
}
// Usage
<app-button
label="Submit"
(clicked)="onSubmit($event)"
/>
Output with Custom Event Type
import { Component, output } from '@angular/core';
interface FormSubmitEvent {
data: Record<string, any>;
timestamp: number;
}
@Component({
selector: 'app-form',
template: `...`
})
export class FormComponent {
submitted = output<FormSubmitEvent>();
onSubmit(formData: Record<string, any>) {
this.submitted.emit({
data: formData,
timestamp: Date.now()
});
}
}
Host Bindings
Class Bindings
import { Component, input } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
host: {
'[class.card]': 'true',
'[class.card-elevated]': 'elevated()',
'[class.card-bordered]': 'bordered()'
},
template: `<ng-content />`
})
export class CardComponent {
elevated = input(false);
bordered = input(true);
}
Attribute Bindings
import { Component, input } from '@angular/core';
@Component({
selector: 'app-clickable',
host: {
'[attr.role]': '"button"',
'[attr.tabindex]': 'disabled() ? -1 : 0',
'[attr.aria-disabled]': 'disabled()'
},
template: `<ng-content />`
})
export class ClickableComponent {
disabled = input(false);
}
Event Bindings
import { Component, output } from '@angular/core';
@Component({
selector: 'app-interactive',
host: {
'(click)': 'handleClick($event)',
'(keydown.enter)': 'handleEnter($event)',
'(focus)': 'handleFocus()',
'(blur)': 'handleBlur()'
},
template: `<ng-content />`
})
export class InteractiveComponent {
clicked = output<MouseEvent>();
focused = output<void>();
handleClick(event: MouseEvent) {
this.clicked.emit(event);
}
handleEnter(event: KeyboardEvent) {
this.clicked.emit(event as any);
}
handleFocus() {
this.focused.emit();
}
handleBlur() {}
}
Content Projection
Single Slot Projection
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content />
</div>
`
})
export class CardComponent {}
// Usage
<app-card>
<h2>Title</h2>
<p>Content</p>
</app-card>
Multiple Slot Projection
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card-header">
<ng-content select="[header]" />
</div>
<div class="card-body">
<ng-content />
</div>
<div class="card-footer">
<ng-content select="[footer]" />
</div>
</div>
`
})
export class CardComponent {}
// Usage
<app-card>
<div header>Header Content</div>
<p>Body Content</p>
<div footer>Footer Content</div>
</app-card>
Conditional Content Projection
import { Component, input, contentChild } from '@angular/core';
@Component({
selector: 'app-expandable',
template: `
<div class="header" (click)="toggle()">
<ng-content select="[header]" />
</div>
@if (expanded()) {
<div class="content">
<ng-content />
</div>
}
`
})
export class ExpandableComponent {
expanded = input(false);
toggle() {
// Implementation
}
}
ViewChild and ContentChild with Signals
viewChild (Angular 17.3+)
import { Component, viewChild, ElementRef, afterNextRender } from '@angular/core';
@Component({
selector: 'app-autofocus',
template: `
<input #inputElement type="text" />
`
})
export class AutofocusComponent {
inputElement = viewChild<ElementRef<HTMLInputElement>>('inputElement');
constructor() {
afterNextRender(() => {
this.inputElement()?.nativeElement.focus();
});
}
}
contentChild
import { Component, contentChild, Directive } from '@angular/core';
@Directive({
selector: '[appFormField]',
standalone: true
})
export class FormFieldDirective {}
@Component({
selector: 'app-form-wrapper',
template: `
<div class="form-wrapper">
<ng-content />
</div>
`
})
export class FormWrapperComponent {
formField = contentChild(FormFieldDirective);
ngAfterContentInit() {
if (this.formField()) {
console.log('Form field found');
}
}
}
Lifecycle Hooks
Modern Lifecycle with Signals
import {
Component,
input,
effect,
afterNextRender,
afterRender
} from '@angular/core';
@Component({
selector: 'app-lifecycle',
template: `<p>{{ data() }}</p>`
})
export class LifecycleComponent {
data = input.required<string>();
constructor() {
// Runs when input signals change
effect(() => {
console.log('Data changed:', this.data());
});
// Runs once after first render
afterNextRender(() => {
console.log('First render complete');
});
// Runs after every render
afterRender(() => {
console.log('Render complete');
});
}
}
Traditional Lifecycle Hooks
import {
Component,
OnInit,
OnDestroy,
AfterViewInit
} from '@angular/core';
@Component({
selector: 'app-traditional',
template: `...`
})
export class TraditionalComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnInit() {
console.log('Component initialized');
}
ngAfterViewInit() {
console.log('View initialized');
}
ngOnDestroy() {
console.log('Component destroyed');
}
}
Component Composition
Container/Presentation Pattern
// Presentation Component (Dumb)
@Component({
selector: 'app-user-list',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (user of users(); track user.id) {
<app-user-card
[user]="user"
(selected)="userSelected.emit($event)"
/>
}
`
})
export class UserListComponent {
users = input.required<User[]>();
userSelected = output<User>();
}
// Container Component (Smart)
@Component({
selector: 'app-users-container',
standalone: true,
imports: [UserListComponent],
template: `
<app-user-list
[users]="users()"
(userSelected)="onUserSelected($event)"
/>
`
})
export class UsersContainerComponent {
private userService = inject(UserService);
users = toSignal(this.userService.getUsers(), { initialValue: [] });
onUserSelected(user: User) {
console.log('Selected:', user);
}
}
Best Practices
- Always use OnPush change detection for better performance
- Prefer signal inputs over @Input() decorator (Angular 17.1+)
- Use signal outputs over @Output() decorator (Angular 17.3+)
- Keep components small and focused - single responsibility
- Use host bindings instead of :host CSS when possible
- Leverage content projection for flexible component APIs
- Avoid direct DOM manipulation - use Angular APIs
- Use viewChild/contentChild instead of @ViewChild/@ContentChild
Resources
- Angular Components Guide: https://angular.dev/guide/components
- Signal Inputs: https://angular.dev/guide/signals/inputs
- Signal Outputs: https://angular.dev/guide/signals/outputs