
Mastering Angular Components: The Building Blocks
A deep dive into Angular components — from structure and composition to lifecycle hooks, change detection, and DOM interactions.
Mastering Angular Components: The Building Blocks
Understanding Angular starts with mastering components.
I went through Angular's core technical documentation and created a slide deck that breaks down how components form the fundamental building blocks of an Angular application. Each component combines TypeScript logic, HTML templates, and CSS styles to create modular, reusable UI units.
Component Structure and Composition
Every Angular component is a TypeScript class with a @Component decorator that defines its metadata:
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `
<div class="profile-card">
<h2>{{ name() }}</h2>
<p>{{ bio() }}</p>
</div>
`,
styles: `
.profile-card {
padding: 1rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserProfileComponent {
name = input.required<string>();
bio = input('No bio provided');
}How Decorators Configure Behavior and Metadata
The @Component decorator tells Angular everything it needs to know about your component — its selector, template, styles, and change detection strategy.
Inputs — Accepting Data
The modern way to accept data uses the input() function, which returns an InputSignal:
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-custom-slider',
template: `<label>{{ label() }}</label>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomSliderComponent {
// Declare an input with a default value
value = input(0);
// Required input — must be set in the template
label = input.required<string>();
// Computed signal derived from inputs
displayValue = computed(() => `Current value: ${this.value()}`);
}Outputs — Custom Events
Define custom events using the output() function:
import { Component, output } from '@angular/core';
@Component({
selector: 'app-expandable-panel',
template: `
<div class="panel">
<button (click)="close()">Close</button>
<ng-content />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExpandablePanelComponent {
panelClosed = output<void>();
close() {
this.panelClosed.emit();
}
}Model Inputs — Two-Way Binding
Model inputs enable components to propagate values back to parent components:
import { Component, model, signal } from '@angular/core';
@Component({
selector: 'app-custom-slider',
template: `
<input
type="range"
[value]="value()"
(input)="onInput($event)"
/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomSliderComponent {
value = model(0);
onInput(event: Event) {
const target = event.target as HTMLInputElement;
this.value.set(Number(target.value));
}
}
// Parent component using two-way binding
@Component({
template: `<app-custom-slider [(value)]="volume" />`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MediaControlsComponent {
volume = signal(0);
}Building Complex Interfaces Through Nested Components
Angular's component model excels at composition. Complex UIs are built by nesting focused, single-responsibility components:
@Component({
selector: 'app-dashboard',
template: `
<app-header [user]="currentUser()" />
<main>
<app-sidebar (navChange)="onNavigate($event)" />
<app-content [page]="activePage()" />
</main>
<app-footer />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DashboardComponent {
currentUser = input.required<User>();
activePage = signal('home');
onNavigate(page: string) {
this.activePage.set(page);
}
}The Component Lifecycle
Angular components go through a well-defined lifecycle. Understanding when each hook runs is key to building maintainable apps.
| Phase | Hook | When It Runs |
| -------------------- | ------------------ | --------------------------------------- |
| Creation | constructor | When Angular instantiates the component |
| Change Detection | ngOnInit | Once, after all inputs are initialized |
| | ngOnChanges | Every time inputs change |
| | ngDoCheck | Every change detection cycle |
| | ngAfterViewInit | Once, after the view is initialized |
| Rendering | afterNextRender | Once, after next DOM render |
| | afterEveryRender | After every DOM render |
| Destruction | ngOnDestroy | Before the component is destroyed |
ngOnInit and ngOnDestroy in Practice
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { DestroyRef } from '@angular/core';
@Component({
selector: 'app-user-profile',
template: `<div>{{ userData() }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserProfileComponent implements OnInit {
private userService = inject(UserService);
userData = signal<User | null>(null);
constructor() {
// Modern approach: use DestroyRef instead of ngOnDestroy
inject(DestroyRef).onDestroy(() => {
console.log('UserProfile destruction — cleanup here');
});
}
ngOnInit() {
// Runs once after inputs are initialized
this.loadUser();
}
private loadUser() {
this.userData.set(this.userService.getCurrentUser());
}
}afterNextRender for DOM Operations
import { Component, ElementRef, afterNextRender, inject } from '@angular/core';
@Component({
selector: 'app-chart',
template: `<canvas #chart></canvas>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChartComponent {
constructor() {
const elementRef = inject(ElementRef);
afterNextRender({
write: () => {
// Safely access the DOM after render
const canvas = elementRef.nativeElement.querySelector('canvas');
this.initChart(canvas);
},
});
}
private initChart(canvas: HTMLCanvasElement) {
// Initialize chart library here
}
}Managing Change Detection Efficiently
Always use OnPush change detection to avoid unnecessary re-renders. Combined with signals, this gives you fine-grained reactivity:
import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';
@Component({
selector: 'app-todo-list',
template: `
<h2>Todos ({{ remainingCount() }} remaining)</h2>
@for (todo of todos(); track todo.id) {
<div [class.completed]="todo.done">
{{ todo.text }}
</div>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodoListComponent {
todos = signal<Todo[]>([
{ id: 1, text: 'Learn Angular', done: false },
{ id: 2, text: 'Build an app', done: false },
]);
remainingCount = computed(
() => this.todos().filter((t) => !t.done).length
);
toggleTodo(id: number) {
this.todos.update((todos) =>
todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
}
}By understanding how and when Angular runs component logic, it becomes much easier to build applications that are both maintainable and dynamic.
Slides attached to the original LinkedIn post 👇 Happy to hear how others approach component lifecycle management in real-world Angular projects.


