Angular Components
What I Do
- Create Angular components following modern best practices
- Implement signal-based inputs, outputs, and model bindings
- Configure change detection strategies (Default vs OnPush)
- Set up content projection with
ng-content - Apply smart/presentational component architecture
- Use lifecycle hooks correctly (ngOnInit, ngOnDestroy, afterRender)
- Query child components with viewChild/contentChild signals
- Fix common errors (ExpressionChangedAfterItHasBeenChecked)
Standalone is the default. As of Angular v17+, all components, directives, and pipes generated by
ng generate componentare standalone by default. Standalone components declare their own dependencies via theimportsarray — noNgModulerequired. NgModule is legacy and only needed when maintaining pre-v17 code or sharing a single module across many bootstrapped entry points. The examples throughout this skill assume standalone.
When to Use Me
Use this skill when you:
- Create, generate, or scaffold new Angular components
- Refactor components to use signals and modern APIs
- Implement parent-child component communication
- Configure content projection with ng-content slots
- Optimize components with OnPush change detection
- Debug lifecycle hook or change detection issues
Component Architecture
Smart vs Presentational Pattern
Presentational - Pure UI, no services, OnPush, input/output only:
@Component({
selector: 'user-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div class="card"><h3>{{ name() }}</h3></div>`
})
export class UserCardComponent {
readonly name = input.required<string>();
readonly select = output<string>();
}
Smart - Inject services, manage state, coordinate children:
@Component({
selector: 'user-list-page',
template: `@for (user of users(); track user.id) { <user-card [name]="user.name" /> }`
})
export class UserListPageComponent {
private userService = inject(UserService);
users = signal<User[]>([]);
}
Inputs and Outputs
// Signal inputs (recommended)
value = input(0); // With default
userId = input.required<string>(); // Required
label = input('', { transform: trimString }); // With transform
disabled = input(false, { transform: booleanAttribute }); // Built-in
// Model input (two-way binding)
value = model(0); // Enables [(value)]="parentValue"
// Signal outputs
saved = output<void>();
valueChanged = output<number>();
this.valueChanged.emit(42);
Lifecycle Hooks
Order: constructor → ngOnChanges → ngOnInit → ngAfterContentInit → ngAfterViewInit
export class MyComponent implements OnInit {
private destroyRef = inject(DestroyRef);
ngOnInit() { this.loadData(); }
constructor() {
this.destroyRef.onDestroy(() => this.cleanup());
afterNextRender(() => this.measureElement()); // DOM operations
}
}
Change Detection
OnPush triggers only when: input reference changes, event occurs, signal changes, or markForCheck() called.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OptimizedComponent {
data = signal<Data | null>(null);
displayName = computed(() => this.data()?.name ?? 'Unknown');
}
Content Projection
@Component({
selector: 'card',
template: `
<ng-content select="[card-header]" />
<ng-content select="[card-body]" />
<ng-content>Fallback content</ng-content>
`
})
export class CardComponent {}
Component Queries
// View queries (template elements)
header = viewChild(HeaderComponent);
header = viewChild.required(HeaderComponent); // Guaranteed
items = viewChildren(ItemComponent);
// Content queries (projected content)
toggle = contentChild(ToggleComponent);
items = contentChildren(ItemComponent, { descendants: true });
Context7 Integration
For current Angular documentation, use Context7 MCP server:
context7_resolve-library-id: angular
context7_query-docs: libraryId: /angular/angular, query: "signal inputs"
Common Errors
ExpressionChangedAfterItHasBeenChecked - Schedule state changes:
// BAD: Mutating bound state during change detection
ngAfterViewInit() { this.title = 'Changed'; }
// GOOD: Schedule for next tick
constructor() {
afterNextRender(() => this.title.set('Changed'));
}
// Or refactor to compute value earlier (constructor/initializer)
OnPush not updating - Create new references:
this.items = [...this.items, newItem]; // Not this.items.push()
Memory leaks - Use takeUntilDestroyed or toSignal:
data = toSignal(this.data$);
// Or: this.data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
Best Practices
- Use signals for component state
- Use OnPush for presentational components
- Use computed instead of getters
- Use inject() over constructor injection
- Use readonly for inputs, outputs, queries
- Track items in @for with unique IDs
Reactive Forms Integration
@Component({
template: `<input [formControl]="nameControl">`
})
export class FormComponent {
nameControl = new FormControl('', Validators.required);
// Signal from form control value
name = toSignal(this.nameControl.valueChanges, { initialValue: '' });
}
Router Integration
@Component({...})
export class UserComponent {
private route = inject(ActivatedRoute);
// Signal input from route (Angular 18+)
userId = input.required<string>(); // With withComponentInputBinding()
// Or from ActivatedRoute
userId = toSignal(this.route.paramMap.pipe(
map(params => params.get('id'))
));
}
Related Skills
| Skill | Use When |
|---|---|
| angular-state | Application state, stores, RxJS |
| angular-testing | Testing components, mocking |
| typescript-advanced | Generic components |
| nx-workspace | Monorepo organization |
References
| Reference | Description |
|---|---|
| research.md | Detailed patterns and examples |
| Angular Docs | Official documentation |