Docs
/
Angular
Chapter 2

02 — Components & Templates

What is a Component?

A component controls a piece of the UI. Every Angular app is a tree of components starting from the root AppComponent.

AppComponent
├── HeaderComponent
├── SidebarComponent
└── MainComponent
    ├── DashboardComponent
    └── UserListComponent
        └── UserCardComponent

Creating a Component

ng g c features/user-card --inline-style --skip-tests

Standalone Component (Angular 17+)

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
      <button (click)="onSelect()">Select</button>
    </div>
  `,
  styles: [`
    .card { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }
  `],
})
export class UserCardComponent {
  @Input({ required: true }) user!: User;
  @Output() selected = new EventEmitter<User>();

  onSelect() {
    this.selected.emit(this.user);
  }
}

Using the Component

<!-- parent.component.html -->
@for (user of users; track user.id) {
  <app-user-card
    [user]="user"
    (selected)="handleSelect($event)"
  />
}

Data Binding

TypeSyntaxDirection
Interpolation{{ expression }}Component → Template
Property binding[property]="expr"Component → Template
Event binding(event)="handler($event)"Template → Component
Two-way binding[(ngModel)]="prop"Both directions
Attribute binding[attr.aria-label]="label"Component → Attribute
Class binding[class.active]="isActive"Component → Class
Style binding[style.color]="textColor"Component → Style
<!-- Interpolation -->
<h1>{{ title }}</h1>
<p>{{ user.name | uppercase }}</p>
<p>{{ getFullName() }}</p>

<!-- Property binding -->
<img [src]="imageUrl" [alt]="imageAlt">
<button [disabled]="isLoading">Submit</button>
<app-child [data]="parentData"></app-child>

<!-- Event binding -->
<button (click)="save()">Save</button>
<input (input)="onInput($event)">
<input (keyup.enter)="search()">
<div (mouseover)="highlight()" (mouseleave)="unhighlight()">

<!-- Two-way binding (requires FormsModule) -->
<input [(ngModel)]="searchQuery">

<!-- Class binding -->
<div [class.active]="isActive" [class.error]="hasError">
<div [ngClass]="{ active: isActive, error: hasError, 'text-bold': isBold }">

<!-- Style binding -->
<div [style.background-color]="bgColor">
<div [ngStyle]="{ 'font-size': fontSize + 'px', color: textColor }">

New Control Flow (Angular 17+)

Angular 17 introduced a new block-based control flow that replaces *ngIf, *ngFor, *ngSwitch.

@if / @else

@if (user) {
  <h1>Welcome, {{ user.name }}</h1>
} @else if (isLoading) {
  <p>Loading...</p>
} @else {
  <p>Please log in</p>
}

@for with track

@for (item of items; track item.id) {
  <app-item-card [item]="item" />
} @empty {
  <p>No items found.</p>
}

<!-- Index and other context variables -->
@for (item of items; track item.id; let i = $index, first = $first, last = $last) {
  <div [class.first]="first" [class.last]="last">
    {{ i + 1 }}. {{ item.name }}
  </div>
}

@switch

@switch (user.role) {
  @case ('admin') {
    <app-admin-panel />
  }
  @case ('editor') {
    <app-editor-panel />
  }
  @default {
    <app-user-panel />
  }
}

@defer — Lazy Load Template Parts

@defer (on viewport) {
  <app-heavy-chart [data]="chartData" />
} @placeholder {
  <div class="skeleton">Loading chart...</div>
} @loading (minimum 500ms) {
  <app-spinner />
} @error {
  <p>Failed to load chart</p>
}

<!-- Triggers -->
@defer (on idle) { ... }         <!-- When browser is idle -->
@defer (on viewport) { ... }     <!-- When element enters viewport -->
@defer (on interaction) { ... }  <!-- On click/focus/input -->
@defer (on hover) { ... }        <!-- On mouse hover -->
@defer (on timer(3s)) { ... }    <!-- After 3 seconds -->
@defer (when condition) { ... }  <!-- When expression is true -->

Component Lifecycle Hooks

Hooks run in this order:

HookWhen It Runs
constructorInstantiation (DI only — no DOM, no inputs)
ngOnChangesBefore ngOnInit + every time an @Input changes
ngOnInitOnce, after first ngOnChanges — fetch data here
ngDoCheckEvery change detection cycle
ngAfterContentInitAfter projected content (<ng-content>) is initialized
ngAfterContentCheckedAfter projected content is checked
ngAfterViewInitAfter component's view + children are initialized
ngAfterViewCheckedAfter view + children are checked
ngOnDestroyBefore component is destroyed — unsubscribe here
import {
  Component, OnInit, OnDestroy, OnChanges,
  AfterViewInit, Input, SimpleChanges
} from '@angular/core';
import { Subject, takeUntil } from 'rxjs';

@Component({ ... })
export class UserProfileComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy {
  @Input() userId!: string;

  private destroy$ = new Subject<void>();

  // ❌ Don't fetch data here — inputs aren't set yet
  constructor(private userService: UserService) {}

  // Called when @Input changes
  ngOnChanges(changes: SimpleChanges) {
    if (changes['userId'] && !changes['userId'].firstChange) {
      this.loadUser();   // Reload when userId input changes
    }
  }

  // ✅ Fetch initial data here
  ngOnInit() {
    this.loadUser();

    this.userService.notifications$
      .pipe(takeUntil(this.destroy$))    // Auto-unsubscribe
      .subscribe(msg => console.log(msg));
  }

  ngAfterViewInit() {
    // DOM is ready — safe to access ViewChild elements
  }

  // ✅ Clean up subscriptions
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

  private loadUser() {
    this.userService.getById(this.userId)
      .pipe(takeUntil(this.destroy$))
      .subscribe(user => this.user = user);
  }
}

@Input() and @Output()

Input — Parent → Child

// child.component.ts
@Input() name: string = '';                   // With default
@Input({ required: true }) user!: User;       // Required (Angular 16+)
@Input({ alias: 'userData' }) user!: User;    // Use different attribute name
@Input({ transform: booleanAttribute }) disabled = false;  // Transform string → boolean

// Input with setter
private _count = 0;
@Input()
set count(value: number) {
  this._count = Math.max(0, value);  // Enforce non-negative
}
get count() { return this._count; }
<!-- parent.component.html -->
<app-child [name]="'Alice'" [user]="selectedUser" [disabled]="true" />

Output — Child → Parent

// child.component.ts
@Output() saved = new EventEmitter<User>();
@Output('userDeleted') deleted = new EventEmitter<string>();  // Alias

save() {
  this.saved.emit(this.user);
}
<!-- parent.component.html -->
<app-child (saved)="handleSave($event)" (userDeleted)="handleDelete($event)" />

@ViewChild / @ViewChildren

import { ViewChild, ViewChildren, QueryList, ElementRef, AfterViewInit } from '@angular/core';

@Component({ ... })
export class ParentComponent implements AfterViewInit {
  @ViewChild('myInput') inputRef!: ElementRef<HTMLInputElement>;
  @ViewChild(ChildComponent) child!: ChildComponent;
  @ViewChildren(ItemComponent) items!: QueryList<ItemComponent>;

  ngAfterViewInit() {
    this.inputRef.nativeElement.focus();        // Access DOM
    this.child.someMethod();                    // Call child method
    this.items.changes.subscribe(() => {});     // React to list changes
  }
}
<input #myInput type="text">
<app-child />
@for (item of list; track item.id) {
  <app-item [data]="item" />
}

Component Interaction Patterns

PatternUse Case
@Input / @OutputParent ↔ Child
Service with SubjectSibling / unrelated components
@ViewChildParent calls child methods
<ng-content>Content projection (slots)
Router paramsRoute-based data passing
SignalsReactive shared state

Key Takeaways

  • Use standalone components — import dependencies directly instead of NgModules
  • Use the new control flow (@if, @for, @switch, @defer) over structural directives
  • @defer is powerful for lazy-loading heavy UI sections based on viewport, interaction, or timer
  • track is required in @for — use a unique identifier like item.id
  • Fetch data in ngOnInit, not in the constructor
  • Always unsubscribe in ngOnDestroy — use takeUntil(destroy$) or DestroyRef
  • Use @Input({ required: true }) for mandatory inputs — catches missing bindings at compile time