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
| Type | Syntax | Direction |
|---|---|---|
| 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:
| Hook | When It Runs |
|---|---|
constructor | Instantiation (DI only — no DOM, no inputs) |
ngOnChanges | Before ngOnInit + every time an @Input changes |
ngOnInit | Once, after first ngOnChanges — fetch data here |
ngDoCheck | Every change detection cycle |
ngAfterContentInit | After projected content (<ng-content>) is initialized |
ngAfterContentChecked | After projected content is checked |
ngAfterViewInit | After component's view + children are initialized |
ngAfterViewChecked | After view + children are checked |
ngOnDestroy | Before 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
| Pattern | Use Case |
|---|---|
@Input / @Output | Parent ↔ Child |
| Service with Subject | Sibling / unrelated components |
@ViewChild | Parent calls child methods |
<ng-content> | Content projection (slots) |
| Router params | Route-based data passing |
| Signals | Reactive 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 @deferis powerful for lazy-loading heavy UI sections based on viewport, interaction, or timertrackis required in@for— use a unique identifier likeitem.id- Fetch data in
ngOnInit, not in the constructor - Always unsubscribe in
ngOnDestroy— usetakeUntil(destroy$)orDestroyRef - Use
@Input({ required: true })for mandatory inputs — catches missing bindings at compile time