Back to Blog
Mastering Angular Components: The Building Blocks
angularfrontendui-architecturemodular-designjavascript-frameworks

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.

Related Posts

Angular Dependency Injection: A Visual Deep Dive

Angular Dependency Injection: A Visual Deep Dive

A comprehensive slide-deck breakdown of Angular DI — from @Injectable to InjectionTokens, injector hierarchy, and advanced resolution strategies like skipSelf and host.

angulartypescriptfrontend+2 more
Read More
Angular DI Essentials: What Changed the Game for Me

Angular DI Essentials: What Changed the Game for Me

Most Angular devs use Dependency Injection every day without fully understanding it. Here are the key concepts that levelled up my Angular architecture.

angulartypescriptfrontend+2 more
Read More
Angular Signals: Reactive State Made Easy

Angular Signals: Reactive State Made Easy

Angular Signals make reactive state easier to reason about. A breakdown of writable, computed, linked, and effect-based primitives for precise dependency tracking.

angularsignalsfrontend+2 more
Read More

Design & Developed by Sachin Singh
© 2026. All rights reserved.