From frontend-angular
Angular component patterns including standalone components, signals, directives, pipes, content projection, view queries, lifecycle hooks, and change detection strategies.
How this skill is triggered — by the user, by Claude, or both
Slash command
/frontend-angular:angular-componentsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
All new components use `standalone: true`. Do not create NgModules for new code. Standalone components declare their own imports directly.
All new components use standalone: true. Do not create NgModules for new code. Standalone components declare their own imports directly.
@Component({
selector: 'app-user-card',
standalone: true,
imports: [CommonModule, RouterLink, AsyncPipe],
templateUrl: './user-card.component.html',
styleUrl: './user-card.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
// ...
}
Bootstrap using bootstrapApplication:
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes), provideHttpClient()],
});
Use Angular Signals for reactive state within components.
import { signal, computed, effect } from '@angular/core';
@Component({ standalone: true, ... })
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
constructor() {
effect(() => {
console.log('count changed:', this.count());
});
}
increment() {
this.count.update(c => c + 1);
}
reset() {
this.count.set(0);
}
}
signal(initialValue) — writable signalcomputed(() => ...) — derived, read-only signal; memoizedeffect(() => ...) — side-effect that re-runs when tracked signals changecount() to read its value.set(value), .update(fn), or .mutate(fn) for objects/arraysUse the signal-based input() and output() APIs instead of decorators.
import { input, output, model } from '@angular/core';
@Component({ standalone: true, ... })
export class ToggleComponent {
// Required input
label = input.required<string>();
// Optional input with default
disabled = input<boolean>(false);
// Two-way binding via model()
checked = model<boolean>(false);
// Typed output event
toggled = output<boolean>();
toggle() {
const next = !this.checked();
this.checked.set(next);
this.toggled.emit(next);
}
}
Template usage:
<app-toggle
label="Enable notifications"
[(checked)]="notificationsEnabled"
(toggled)="onToggle($event)"
/>
Use model() when the parent needs two-way binding. Use output() for one-way events.
Modify the appearance or behavior of a host element.
@Directive({
selector: '[appHighlight]',
standalone: true,
host: {
'[class.highlighted]': 'isHighlighted()',
'(mouseenter)': 'onEnter()',
'(mouseleave)': 'onLeave()',
},
})
export class HighlightDirective {
color = input<string>('yellow');
isHighlighted = signal(false);
onEnter() { this.isHighlighted.set(true); }
onLeave() { this.isHighlighted.set(false); }
}
Control DOM structure. Prefer Angular's built-in control flow (@if, @for, @switch) over *ngIf / *ngFor in new code.
Custom structural directive when needed:
@Directive({
selector: '[appIfRole]',
standalone: true,
})
export class IfRoleDirective {
private vcr = inject(ViewContainerRef);
private tmpl = inject(TemplateRef);
@Input() set appIfRole(role: string) {
const hasRole = this.authService.hasRole(role);
if (hasRole) {
this.vcr.createEmbeddedView(this.tmpl);
} else {
this.vcr.clear();
}
}
}
Pure pipes are stateless and memoized. Prefer pure pipes.
@Pipe({
name: 'truncate',
standalone: true,
pure: true,
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 100, ellipsis = '...'): string {
if (value.length <= limit) return value;
return value.substring(0, limit) + ellipsis;
}
}
Use async pipe to subscribe to Observables in templates — automatically unsubscribes on component destroy.
<div *ngIf="users$ | async as users">
@for (user of users; track user.id) {
<app-user-card [user]="user" />
}
</div>
With the new control flow syntax and signals via toSignal(), prefer toSignal() over async pipe when possible.
Use <ng-content> to project external content into a component.
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<div class="card-header">
<ng-content select="[slot=header]" />
</div>
<div class="card-body">
<ng-content />
</div>
<div class="card-footer">
<ng-content select="[slot=footer]" />
</div>
</div>
`,
})
export class CardComponent {}
Usage:
<app-card>
<h2 slot="header">Title</h2>
<p>Main content here.</p>
<button slot="footer">Close</button>
</app-card>
Use signal-based query APIs: viewChild(), viewChildren(), contentChild(), contentChildren().
@Component({ standalone: true, ... })
export class FormComponent {
// Single element query — required
submitBtn = viewChild.required<ElementRef>('submitBtn');
// Optional query
modal = viewChild<ModalComponent>(ModalComponent);
// Query list
inputs = viewChildren<InputComponent>(InputComponent);
// Content child query
icon = contentChild<IconComponent>(IconComponent);
focusSubmit() {
this.submitBtn().nativeElement.focus();
}
}
Template:
<button #submitBtn type="submit">Submit</button>
Do not use @ViewChild / @ViewChildren decorators in new code — prefer the signal-based query functions.
Implement lifecycle interfaces explicitly. Common hooks:
| Hook | When |
|---|---|
ngOnInit | After inputs are set for the first time |
ngOnChanges | When input values change |
ngAfterViewInit | After view (and child views) are initialized |
ngAfterContentInit | After content children are projected |
ngOnDestroy | Before component is destroyed |
@Component({ standalone: true, ... })
export class DataComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.dataService.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => this.data.set(data));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Prefer takeUntilDestroyed(this.destroyRef) (injected DestroyRef) over manual destroy subjects.
Always use ChangeDetectionStrategy.OnPush for standalone components. It triggers change detection only when:
markForCheck() or detectChanges()@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
...
})
export class MyComponent { }
When using signals, Angular tracks reads automatically and schedules updates without manual markForCheck().
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>{{ count() }}</p>`,
})
export class SignalComponent {
count = signal(0); // template re-renders automatically on change
}
Use the @if, @for, @switch control flow blocks (Angular 17+) instead of *ngIf, *ngFor, *ngSwitch.
@if (isLoggedIn()) {
<app-dashboard />
} @else if (isPending()) {
<app-loading />
} @else {
<app-login />
}
Always provide a track expression for efficient DOM reconciliation.
@for (item of items(); track item.id) {
<app-item [item]="item" />
} @empty {
<p>No items found.</p>
}
@switch (status()) {
@case ('active') { <span class="badge-green">Active</span> }
@case ('inactive') { <span class="badge-gray">Inactive</span> }
@default { <span class="badge-red">Unknown</span> }
}
input() and emit events via output() — no service injection.*.component.ts, *.component.html, *.component.scss, *.component.spec.ts.inject() function instead of constructor injection for cleaner code.@Component({ standalone: true, ... })
export class ProductListComponent {
private productService = inject(ProductService);
products = toSignal(this.productService.getProducts(), { initialValue: [] });
}
npx claudepluginhub gagandeepp/software-agent-teams --plugin frontend-angularGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.