🆕 Angular 18 / 19 / 20 — The Modern Era
Learn Angular Visually
The Modern Way
Step-by-step, animated, zero-jargon explanations of Signals, Standalone Components, New Control Flow, and more. Everything you need to think like an Angular developer.
⚡ Signals
🧩 Standalone
🔀 @if / @for
⏳ @defer
💉 inject()
🚫 No NgModule
What Changed in Angular 18+
Side-by-side old vs new comparisons for every major concept
Start Here
Standalone Components
Build components without NgModule — the new default
Updated
Signals
Angular's reactive primitive — signal(), computed(), effect()
New
New Control Flow
@if, @for, @switch, @defer — no more *ngIf or *ngFor
New
Component Communication
input(), output(), model() signals replace @Input/@Output
Upgraded
Lifecycle Hooks
All hooks + new afterRender() and afterNextRender()
Updated
Services & inject()
inject() function replaces constructor injection everywhere
New
Cheat Sheet
All syntax, all patterns, one page quick reference
Complete
No Node.js needed • Open in any browser • Angular 18, 19, 20 compatible
Overview
What Changed in Angular 18+
Angular went through a massive modernization. Here's every major change, shown old vs new, so you always know which way to write things.
Think of Angular 18+ like a phone OS update: the old stuff still works (for now), but the new way is faster, cleaner, and has superpowers. This module shows every old→new pair side by side.
Summary Table
App Bootstrap
NgModule Gone
Inputs/Outputs
Reactivity
Dependency Injection
| Concept | Old Way (Angular <17) | New Way (Angular 18+) | Status |
|---|---|---|---|
| App startup | platformBrowserDynamic().bootstrapModule(AppModule) | bootstrapApplication(AppComponent) | Changed |
| Module system | Every component needed @NgModule | standalone: true — no NgModule needed | Changed |
| Conditional rendering | *ngIf="condition" | @if (condition) { } | New |
| Loops | *ngFor="let x of list" | @for (x of list; track x.id) { } | New |
| Switch statements | [ngSwitch] + *ngSwitchCase | @switch (val) { @case ('x') { } } | New |
| Lazy content | No built-in template defer | @defer (on viewport) { } @placeholder { } | New |
| Component inputs | @Input() name: string = '' | name = input<string>('') (Signal Input) | New |
| Component outputs | @Output() clicked = new EventEmitter() | clicked = output<void>() | New |
| Two-way binding | @Input() value + @Output() valueChange | value = model<string>('') | New |
| Reactivity | Zone.js detects all changes automatically | Signals: only update what changed | New |
| Dependency Injection | constructor(private http: HttpClient) | http = inject(HttpClient) | New |
| DOM access timing | No guaranteed post-render hook | afterRender() / afterNextRender() | New |
| OnChanges with signals | ngOnChanges() fires on @Input change | Signals: use effect() — ngOnChanges NOT triggered | Watch out |
Don't panic! Old code still works.
*ngIf, @NgModule, and @Input() decorators are still valid. Angular just gives you better options now. Migrate at your own pace.How Angular starts your app — Every Angular app needs an entry point. Angular 18 changed this from "bootstrap a module" to "bootstrap a single component". Way simpler.
❌ Old Way (needed NgModule)
// main.ts — old
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
// app.module.ts — required a whole file!
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
bootstrap: [AppComponent]
})
export class AppModule {}
✅ New Way (no NgModule!)
// main.ts — Angular 18+
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes), // add router
provideHttpClient() // add HTTP
]
});
No separate module file. Providers are passed directly. Much less boilerplate!
Memory trick: Old = "bootstrap the MODULE, module loads component". New = "bootstrap THE COMPONENT directly". Think of cutting out the middle man.
NgModule was the old "container" that grouped components. In Angular 18, each component is self-contained — it imports only what it needs. This is called a Standalone Component.
❌ Old: 3 files needed for 1 component
// user.module.ts
@NgModule({
declarations: [UserComponent],
imports: [CommonModule, FormsModule],
exports: [UserComponent]
})
export class UserModule {}
// user.component.ts
@Component({ selector: 'app-user', templateUrl: '...' })
export class UserComponent {}
// app.module.ts — must declare UserModule!
@NgModule({ imports: [UserModule, ...] })
export class AppModule {}
✅ New: 1 file, self-contained
// user.component.ts
@Component({
selector: 'app-user',
standalone: true, // <-- magic line
imports: [ // import directly here!
CommonModule,
FormsModule
],
template: `<div>...</div>`
})
export class UserComponent {}
// That's it! No module file needed.
Angular 18 introduced Signal Inputs and Outputs. Instead of decorator-based
@Input(), you use a function input() that returns a reactive Signal. The template auto-updates when the parent changes the value.❌ Old: Decorator-based
import { Input, Output, EventEmitter }
from '@angular/core';
export class ChildComponent {
// INPUT: parent passes value in
@Input() title: string = '';
// OUTPUT: child sends event to parent
@Output()
clicked = new EventEmitter<string>();
sendClick() {
this.clicked.emit('hello');
}
}
✅ New: Signal-based functions
import { input, output }
from '@angular/core';
export class ChildComponent {
// INPUT: now returns a Signal!
title = input<string>('');
// Use as: this.title() — note the ()
// OUTPUT: cleaner, no EventEmitter import
clicked = output<string>();
sendClick() {
this.clicked.emit('hello');
}
}
Key difference: With the old
@Input(), you read the value as this.title. With the new signal input, you read it as this.title() — always call it like a function! The signal stores the current value inside it.🧠 Analogy: Zone.js vs Signals
Old (Zone.js): Imagine a security guard who checks EVERY room in a building after ANY noise. Even if only room 3 changed, he checks all 100 rooms. Very thorough, but slow.New (Signals): Each room now has its own alarm. When room 3 changes, ONLY room 3's alarm goes off and only that part of the page updates. Surgical and fast.
❌ Old: Zone.js (automatic but dumb)
export class CounterComponent {
count = 0;
increment() {
this.count++;
// Zone.js detects this change and
// re-checks the ENTIRE component tree!
// Works, but wastes CPU on unchanged parts.
}
}
// Template:
<p>Count: {{ count }}</p>
// Works, but triggers full change detection
✅ New: Signals (surgical updates)
import { signal } from '@angular/core';
export class CounterComponent {
// signal() creates a reactive value
count = signal(0);
increment() {
this.count.update(v => v + 1);
// Only the <p> that shows count() updates!
// Nothing else re-renders.
}
}
// Template:
<p>Count: {{ count() }}</p>
// Note the () — reads the signal's value
Dependency Injection (DI) is how Angular gives your component the services it needs. Old way: ask in constructor. New way: call
inject() anywhere in your class (field initializer, constructor, or factory function).❌ Old: Constructor injection
export class UserComponent {
// Must list ALL services in constructor
constructor(
private userService: UserService,
private router: Router,
private http: HttpClient,
private dialog: MatDialog
) {
// Hard to test, boilerplate-heavy
}
}
✅ New: inject() function
import { inject } from '@angular/core';
import { UserService } from './user.service';
import { Router } from '@angular/router';
export class UserComponent {
// inject() each service as a field!
private userService = inject(UserService);
private router = inject(Router);
private http = inject(HttpClient);
// No constructor needed! Cleaner, testable
}
Module 2
🧩 Standalone Components
No more NgModule! Every component is now self-contained and declares its own dependencies. This is the default in Angular 17+.
Concept
Anatomy
What to Import
Routing
Live Demo
🧠 Analogy: Component as a Self-Contained App
Think of each component like a smartphone app. Old Angular: every app had to be registered on an "App Store" (NgModule) before it could work. New Angular: each app runs independently and only downloads what it needs on its own. No registry needed.
What standalone gives you:
• No NgModule file to maintain
• Component imports only what it uses
• Easier tree-shaking (smaller bundle)
• Simpler testing
• Clear dependency graph
• No NgModule file to maintain
• Component imports only what it uses
• Easier tree-shaking (smaller bundle)
• Simpler testing
• Clear dependency graph
Remember:
• Add
• Move imports (like
• You can still use NgModule-based libraries — just import their modules directly in
• Add
standalone: true to every component• Move imports (like
CommonModule) into the component itself• You can still use NgModule-based libraries — just import their modules directly in
imports: []1
Add
standalone: true to @ComponentThis single flag tells Angular this component manages its own dependencies
2
Add an
imports: [] array directly in @ComponentList every Angular feature you use in the template: NgIf, RouterLink, FormsModule, other components, etc.
3
Delete NgModule files
You no longer need app.module.ts or any feature.module.ts files
📄 Full Standalone Component Anatomy
Every line explained with a comment
// ✅ Angular 18+ Standalone Component — annotated
import { Component, signal, computed, input, output } from '@angular/core';
import { CommonModule } from '@angular/common'; // for @if/@for or NgClass
import { RouterLink } from '@angular/router'; // for [routerLink] in template
import { FormsModule } from '@angular/forms'; // for [(ngModel)] two-way binding
@Component({
selector: 'app-profile', // how you use it in HTML: <app-profile />
standalone: true, // ← THIS IS THE KEY: no NgModule needed
imports: [ // ← everything the TEMPLATE uses goes here
CommonModule, // brings NgClass, NgStyle, etc.
RouterLink, // brings [routerLink] directive
FormsModule // brings [(ngModel)]
],
changeDetection: ChangeDetectionStrategy.OnPush, // optional: faster with signals
template: `
<h1>Hello, {{ name() }}!</h1> <!-- reads signal input -->
<button (click)="greet.emit(name())">Say Hi</button>
`
})
export class ProfileComponent {
name = input<string>('World'); // Signal Input from parent
greet = output<string>(); // Signal Output to parent
}
What goes in
imports: []? Anything your template uses that Angular needs to know about. Here's a practical guide:| If template uses... | Import this | From |
|---|---|---|
@if / @for / @switch | No import needed — built-in! | Angular 18+ template syntax |
*ngIf / *ngFor (old way) | CommonModule or NgIf, NgFor | @angular/common |
[routerLink] | RouterLink | @angular/router |
<router-outlet> | RouterOutlet | @angular/router |
[(ngModel)] | FormsModule | @angular/forms |
formGroup reactive forms | ReactiveFormsModule | @angular/forms |
Another component <app-child> | ChildComponent (the class itself) | Your own file |
| async pipe | AsyncPipe | @angular/common |
| date pipe | DatePipe | @angular/common |
| Angular Material button | MatButtonModule | @angular/material/button |
Pro tip: In Angular 17+, you can import individual directives instead of entire modules.
imports: [NgIf, NgFor] instead of CommonModule. Smaller bundles!🗺️ Routing with Standalone Components
Routes now reference standalone components directly — no loadChildren modules needed
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () => // lazy load a standalone component
import('./home/home.component')
.then(m => m.HomeComponent) // no .module.ts needed!
},
{
path: 'users',
loadChildren: () => // lazy load child routes
import('./users/users.routes')
.then(m => m.userRoutes) // an array of routes, not a module!
}
];
// main.ts
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)] // wire up routes here
});
See a visual diagram of how a standalone component bootstraps and declares its own dependencies without needing NgModule.
📄 main.ts
bootstrapApplication(AppComponent)
bootstrapApplication(AppComponent)
↓
🧩 AppComponent
standalone: true
imports: [RouterOutlet]
standalone: true
imports: [RouterOutlet]
↙
↘
🧩 HomeComponent
standalone: true
standalone: true
🧩 UsersComponent
standalone: true
standalone: true
No NgModule anywhere in this tree! ✅
Module 3 — Most Important
⚡ Signals
Signals are Angular's new reactivity system. Learn signal(), computed(), effect(), input(), output(), and model() with interactive demos and real analogies.
What is a Signal?
signal() basics
computed()
effect()
input() / output()
model()
🎮 Live Demo
🧠 Analogy: Signal = A Smart Variable
Think of a regular variable like a whiteboard. You write a number on it, but nobody knows it changed. You have to tell everyone manually.A Signal is like a digital display board. When you update it, everyone watching it automatically sees the new value. Angular's template "watches" the signal and re-renders only the part that uses that signal.
📌 Regular variable (dumb)
count = 0; // just a number
increment() {
this.count++;
// Template may or may not update
// Zone.js has to check everything
}
⚡ Signal (smart reactive)
count = signal(0); // a reactive container
increment() {
this.count.update(v => v + 1);
// ONLY the part of template
// reading count() will re-render!
}
🔑 The 3 Operations of a Signal
| Operation | Syntax | What it does |
|---|---|---|
| Read | this.count() | Get the current value. Always call as a function () |
| Write (set) | this.count.set(5) | Replace the value entirely |
| Write (update) | this.count.update(v => v + 1) | Compute new value based on old one |
Most common mistake: Forgetting the
() when reading a signal. count gives you the signal OBJECT. count() gives you the VALUE. Always use () to read!Creating and Using Signals
import { Component, signal } from '@angular/core';
@Component({
standalone: true,
template: `
<h2>Count: {{ count() }}</h2> <!-- read signal with () -->
<h3>Name: {{ user().name }}</h3> <!-- signals can hold objects! -->
<button (click)="increment()">+1</button>
<button (click)="reset()">Reset</button>
`
})
export class CounterComponent {
// ── Creating signals ──────────────────────
count = signal(0); // number signal, starts at 0
name = signal('Alice'); // string signal
isActive = signal(true); // boolean signal
items = signal<string[]>([]); // array signal (typed)
user = signal({ name: 'Bob', age: 25 }); // object signal
// ── Reading signal value ──────────────────
logCount() {
const value = this.count(); // call like a function!
console.log('Count is', value);
}
// ── Writing signal value ──────────────────
increment() {
this.count.update(current => current + 1); // based on current
}
reset() {
this.count.set(0); // replace with new value
}
addItem(item: string) {
this.items.update(list => [...list, item]); // spread to create new array
}
updateUserName(newName: string) {
this.user.update(u => ({ ...u, name: newName })); // spread to preserve other fields
}
}
Array/Object trap: Never mutate a signal's array directly:
this.items().push(x) — this changes the array but Angular won't detect it. Always use .update(arr => [...arr, x]) to create a new array reference so Angular knows something changed.🧠 Analogy: computed() = A Formula Cell in Excel
In Excel, if you have A1=5 and B1=A1*2, when you change A1, B1 automatically recalculates. That's exactly what computed() does — it's a signal whose value is derived from other signals, and it auto-updates whenever those signals change.
import { signal, computed } from '@angular/core';
export class CartComponent {
// Base signals (the "raw data")
price = signal(10);
quantity = signal(3);
taxRate = signal(0.08);
firstName = signal('John');
lastName = signal('Doe');
// Derived signals (auto-update when dependencies change)
subtotal = computed(() =>
this.price() * this.quantity() // depends on price AND quantity
);
tax = computed(() =>
this.subtotal() * this.taxRate() // depends on subtotal (which depends on price/qty)
);
total = computed(() =>
this.subtotal() + this.tax() // chain of computed signals!
);
fullName = computed(() =>
`\${this.firstName()} \${this.lastName()}` // string concatenation
);
// When price.set(20):
// ↳ subtotal auto-recalculates → 20*3 = 60
// ↳ tax auto-recalculates → 60*0.08 = 4.80
// ↳ total auto-recalculates → 64.80
// All template bindings using these signals update instantly!
}
computed() is READ-ONLY! You cannot call
.set() or .update() on a computed signal. It always derives its value from other signals. To change it, change the underlying source signals.🧠 Analogy: effect() = A Surveillance Camera
A surveillance camera watches a room. Whenever something moves (signal changes), it triggers a recording (side effect) — like logging, saving to localStorage, or calling an API. The camera runs automatically, you just set it up once.
import { signal, effect } from '@angular/core';
export class ThemeComponent {
theme = signal<'light' | 'dark'>('dark');
userId = signal(1);
username = signal('Alice');
constructor() {
// effect() runs immediately AND whenever tracked signals change
effect(() => {
// Reads theme() — so Angular tracks this dependency
document.body.classList.toggle('dark-mode', this.theme() === 'dark');
console.log('Theme changed to:', this.theme());
});
// This effect saves to localStorage whenever userId changes
effect(() => {
localStorage.setItem('userId', this.userId().toString());
});
// Use multiple signals in one effect
effect(() => {
console.log(`User \${this.username()} logged in as \${this.userId()}`);
// Runs when either username OR userId changes
});
}
toggleTheme() {
this.theme.update(t => t === 'dark' ? 'light' : 'dark');
// → effect runs automatically, updates document.body.classList
}
}
Where to put effect(): Always inside a constructor (or injection context). Never call effect() inside a method or setTimeout — Angular needs to know which component "owns" the effect for cleanup.
Don't update signals inside effect(): This creates infinite loops! effect() reads a signal → signal changes → effect runs again → ... Instead, use computed() for derived state.
Signal Inputs and Outputs replace the old
@Input() and @Output() decorators. The key difference: signal inputs return a Signal, so you can use them in computed() and effect()!📥 input() — Receiving from Parent
import { input, computed } from '@angular/core';
export class UserCardComponent {
// Required input (no default, parent MUST provide)
userId = input.required<number>();
// Optional input (with default value)
role = input<string>('viewer');
// Renamed input (property vs attribute name)
fullName = input<string>('', { alias: 'name' });
// Parent writes: [name]="..." but class has this.fullName()
// Use in computed! (impossible with @Input)
isAdmin = computed(() =>
this.role() === 'admin'
);
showInfo() {
console.log(this.userId()); // always call with ()
}
}
// In parent template:
// <app-user-card [userId]="5" [role]="'admin'" />
📤 output() — Sending to Parent
import { output } from '@angular/core';
export class FormComponent {
// output() creates an event emitter
submitted = output<string>(); // emits a string
cancelled = output<void>(); // emits nothing
onSubmit(value: string) {
this.submitted.emit(value); // fires event to parent
}
onCancel() {
this.cancelled.emit(); // void: emit with no value
}
}
// In parent template:
// <app-form
// (submitted)="handleSubmit($event)"
// (cancelled)="handleCancel()" />
// Parent method:
handleSubmit(value: string) {
console.log('Got:', value);
}
input() vs @Input() and ngOnChanges: With the old
@Input(), when the parent changes the value, ngOnChanges() fires. With signal input(), ngOnChanges does NOT fire. Instead, use effect(() => { this.myInput(); }) to react to input changes.🧠 Analogy: model() = A Shared Whiteboard
Old two-way binding was like passing a note back and forth between parent and child. model() is like putting a shared whiteboard between them — both can read AND write to the same value, and both see changes instantly.
🔄 model() — Two-Way Signal Binding
Replaces the old
@Input() value + @Output() valueChange pattern// child: toggle.component.ts
import { model } from '@angular/core';
export class ToggleComponent {
// model() = input + output in one!
// Parent can read it AND child can change it
checked = model(false); // default: false
toggle() {
// Child updates the shared value
this.checked.update(v => !v);
// Parent automatically sees the new value!
}
}
// parent template:
// <app-toggle [(checked)]="isEnabled" />
// ↑
// The [()] syntax is "banana in a box" = two-way binding
// Parent's isEnabled signal stays in sync with child's checked signal
// ─────────────────────────────────────────────
// OLD WAY (required 2 decorators!):
// @Input() checked = false;
// @Output() checkedChange = new EventEmitter<boolean>();
// toggle() { this.checkedChange.emit(!this.checked); }
// ─────────────────────────────────────────────
This demo visualizes how signals propagate changes through a dependency graph. Watch computed() signals auto-update when source signals change.
⚡ Signal Playground
price signal
price = 10
quantity signal
qty = 2
Auto-computed values ↓
subtotal = 20 price × qty
tax = 1.60 subtotal × 8%
total = 21.60 subtotal + tax
📄 Code
price = signal(10);
qty = signal(2);
subtotal = computed(() =>
this.price() * this.qty()
);
tax = computed(() =>
this.subtotal() * 0.08
);
total = computed(() =>
this.subtotal() + this.tax()
);
// effect() logs changes
effect(() => {
console.log(
'total:',
this.total()
);
});
Change price or quantity → subtotal, tax, and total all auto-update. This is reactive computation with Signals!
Module 4
🔀 New Control Flow
Angular 17 introduced a new template syntax: @if, @for, @switch, and @defer. These replace *ngIf, *ngFor, and [ngSwitch]. They're built into Angular — no imports needed!
@if / @else
@for
@switch
@defer
🎮 Live Demo
🧠 Analogy: @if is an If-Else in Your Template
Just like JavaScript's if/else if/else, but written directly in your HTML template. No need to import anything!❌ Old: *ngIf + ng-template
<!-- requires CommonModule or NgIf import -->
<div *ngIf="isLoggedIn">
Welcome back!
</div>
<<!-- else requires a template ref -->
<div *ngIf="isLoggedIn; else loggedOut">
Welcome!
</div>
<ng-template #loggedOut>
Please log in.
</ng-template>
✅ New: @if / @else if / @else
<!-- No imports! Reads like JS code -->
@if (isLoggedIn) {
<div>Welcome back!</div>
} @else if (isGuest) {
<div>Welcome, guest!</div>
} @else {
<div>Please log in.</div>
}
<!-- Works with any expression -->
@if (user()?.role === 'admin') {
<admin-panel />
}
Big win: No more
ng-template needed for else branches! The @else block is written right after the @if block, making it readable.@for replaces *ngFor. The big change: track is now required. Track tells Angular how to identify each item so it can do minimal DOM updates when the list changes.❌ Old: *ngFor
<ul>
<li *ngFor="let user of users; let i = index">
{{ i + 1 }}. {{ user.name }}
</li>
</ul>
<!-- No built-in @empty fallback -->
<div *ngIf="!users.length">No users</div>
✅ New: @for (track is required!)
<ul>
@for (user of users; track user.id) {
<!-- track by unique id = fast DOM updates -->
<li>{{ user.name }}</li>
} @empty {
<!-- built-in empty state! -->
<li>No users found.</li>
}
</ul>
📋 @for Variables
@for (item of items; track item.id; let i = $index, even = $even, odd = $odd, first = $first, last = $last, count = $count) {
<!-- $index = current position (0-based) -->
<!-- $even = true if $index is even -->
<!-- $odd = true if $index is odd -->
<!-- $first = true if first item -->
<!-- $last = true if last item -->
<!-- $count = total number of items -->
<div [class.alternate]="even">
{{ i + 1 }} / {{ count }}: {{ item.name }}
@if (first) { <span>⭐ First</span> }
@if (last) { <span>🏁 Last</span> }
</div>
}
Why is
track required? Without it, Angular can't tell which item is which when the list changes. It would destroy and recreate ALL DOM elements every time (slow!). With track item.id, Angular knows which specific items changed and only updates those.❌ Old: [ngSwitch]
<div [ngSwitch]="status">
<span *ngSwitchCase="'loading'">⏳</span>
<span *ngSwitchCase="'success'">✅</span>
<span *ngSwitchCase="'error'">❌</span>
<span *ngSwitchDefault>❓</span>
</div>
✅ New: @switch
@switch (status) {
@case ('loading') { <span>⏳ Loading...</span> }
@case ('success') { <span>✅ Done!</span> }
@case ('error') { <span>❌ Failed</span> }
@default { <span>❓ Unknown</span> }
}
🧠 Analogy: @defer = Lazy Loading Images
Imagine loading a webpage where images below the fold don't load until you scroll to them. @defer does this for Angular components — heavy components (charts, rich editors) only load when they're needed. Your page loads fast!⏳ @defer with all its blocks
<!-- Full @defer with all blocks -->
@defer (on viewport) {
<!-- Main content: only loaded when <div> enters the viewport -->
<app-heavy-chart /> <!-- downloaded only when needed! -->
} @placeholder {
<!-- Shown before trigger fires -->
<div>📊 Chart will load when you scroll here...</div>
} @loading (minimum 500ms) {
<!-- Shown while the chunk is downloading -->
<div>⏳ Loading chart...</div>
} @error {
<!-- Shown if download fails -->
<div>❌ Failed to load chart.</div>
}
🎛️ @defer Triggers
| Trigger | When it fires |
|---|---|
on viewport | Placeholder enters visible area |
on idle | Browser is idle (after main work done) |
on interaction | User clicks or touches the placeholder |
on hover | User hovers over placeholder |
on timer(2s) | After 2 seconds |
when condition | When a boolean expression becomes true |
Interactive demo of all control flow blocks. Use the controls to see how @if, @for, @switch, and @defer behave visually.
🔀 @if / @else Demo
❓ Click a button to see @if in action
🔁 @for Demo
@empty block: No items yet
⏳ @defer Demo
📦 @placeholder — Component not yet loaded
📄 Template Code
<!-- @if example -->
@if (status === 'loggedIn') {
<div>👋 Welcome back!</div>
} @else if (status === 'guest') {
<div>👤 Welcome, Guest!</div>
} @else {
<div>🔑 Please log in</div>
}
<!-- @for example -->
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
} @empty {
<div>No items yet</div>
}
<!-- @defer example -->
@defer (on interaction) {
<app-chart />
} @placeholder {
<div>📦 Not loaded</div>
} @loading {
<div>⏳ Loading...</div>
} @error {
<div>❌ Failed</div>
}
Module 5
🔗 Component Communication
Components need to talk to each other. Angular 18 has 4 patterns. All are shown here with animations so you can literally see the data moving.
input() Parent→Child
output() Child→Parent
Service/Signal
viewChild()
🧠 Analogy: input() = Sending a Package
The parent puts data in a box and sends it down to the child. The child receives it as a Signal and can only read it — it can't change the original! One-way data flow: parent → child.🎮 Live Demo — Click to Send Data
ParentComponent
Owns the data, passes it down
↓
ChildComponent
Receives via
message = input<string>()Waiting for data...
📄 Code
// parent.component.ts
@Component({
template: `
<app-child
[message]="greeting"
/>
`
})
export class ParentComponent {
greeting = 'Hello from Parent!';
}
// child.component.ts
@Component({ ... })
export class ChildComponent {
// Receives from parent as a Signal
message = input<string>('');
showMessage() {
// Always read with () !
console.log(this.message());
}
}
// template: {{ message() }}
🧠 Analogy: output() = Pressing a Button to Ring a Bell
The child has a button (output). When pressed, a bell rings in the parent. The parent hears the bell and reacts. The child doesn't know what the parent does — it just "emits" the event upward.🎮 Live Demo
ParentComponent
Listens to (buttonClicked) event
Waiting for event from child...
↑
ChildComponent
Emits via
buttonClicked = output<string>()📄 Code
// child.component.ts
export class ChildComponent {
buttonClicked = output<string>();
onClick() {
this.buttonClicked
.emit('Child was clicked!');
}
}
// parent.component.ts
@Component({
template: `
<app-child
(buttonClicked)="onChildClick($event)"
/>
`
})
export class ParentComponent {
onChildClick(msg: string) {
console.log('Received:', msg);
}
}
🧠 Analogy: Service = A Shared Radio Station
Imagine 3 people in different rooms with radios tuned to the same station. When the DJ (service) plays a song (updates a signal), all 3 radios hear it simultaneously — without the rooms knowing about each other.📡 Signal-Based Service Demo
ComponentA — can read & write
↓ Service (shared signal) ↓
🗄️ SharedService.count() = 0
↓ All subscribers update ↓
ComponentB — reads service signal
Count = 0
ComponentC — also reads same signal
Count = 0
📄 Code
// shared.service.ts
@Injectable({ providedIn: 'root' })
export class SharedService {
// Signal as shared state
count = signal(0);
increment() {
this.count.update(n => n + 1);
}
}
// component-a.ts (writes)
export class ComponentA {
private shared = inject(SharedService);
onClick() { this.shared.increment(); }
}
// component-b.ts (reads)
export class ComponentB {
private shared = inject(SharedService);
// In template: {{ shared.count() }}
// Auto-updates when service changes!
}
viewChild() gives you a reference to a child component or DOM element. Angular 18+ provides a signal-based
viewChild() function — you call it with () to get the current value.import { viewChild, afterViewInit, ElementRef } from '@angular/core';
export class ParentComponent implements AfterViewInit {
// New signal-based viewChild
childComp = viewChild(ChildComponent);
myDiv = viewChild<ElementRef>('myRef'); // by template ref
ngAfterViewInit() {
// Access via () — might be undefined before view loads
const child = this.childComp();
if (child) {
child.reset(); // call child method directly!
}
}
}
// template:
// <app-child #myRef />
// <div #myRef>...</div>
viewChild() returns undefined before the view is initialized. Always use it inside
ngAfterViewInit or inside an effect(), and always check if the value is defined before calling methods on it.Module 6
🔄 Lifecycle Hooks
Angular calls special methods on your component as it goes through stages: creation, changes, rendering, and destruction. Angular 18 adds two new hooks. Click the buttons to see each hook fire in real time.
Overview
Creation Hooks
Change Hooks
Render Hooks (New!)
Destroy Hook
🎮 Live Demo
🧠 Analogy: Lifecycle = Stages of a Plant
Seed planted = constructor() → component object created in memorySeedling sprouts = ngOnInit() → component fully initialized with data
Leaves grow = ngOnChanges() → parent changes the plant's environment
Flowers bloom = ngAfterViewInit() → the visible part is ready to see
Plant dies = ngOnDestroy() → cleanup, stop timers, unsubscribe
| Hook | When it runs | Use it for | |
|---|---|---|---|
constructor() | When the class is instantiated | inject() services only | |
ngOnChanges() | When @Input values change (not signal inputs!) | React to parent data changes | ⚠️ |
ngOnInit() | Once, after first ngOnChanges | Fetch data, setup subscriptions | Common |
ngDoCheck() | Every change detection cycle | Custom change detection (rarely needed) | |
ngAfterContentInit() | After ng-content projected | Access projected content | |
ngAfterContentChecked() | After content checked | Rarely used | |
ngAfterViewInit() | After component's view is rendered | Access DOM, @ViewChild | Common |
ngAfterViewChecked() | After view re-checked | Rarely used, be careful | |
afterNextRender() | Once, after next render cycle | Third-party DOM libs (new!) | New |
afterRender() | After every render cycle | Continuous DOM access (new!) | New |
ngOnDestroy() | Just before component is removed | Cleanup: unsubscribe, clear timers | Important |
🌱 constructor() + ngOnInit() — The Most Important Pair
import { Component, OnInit, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({ standalone: true, ... })
export class UserComponent implements OnInit {
private userService = inject(UserService);
users = signal<User[]>([]);
constructor() {
// ✅ ONLY for inject() / signal creation
// ❌ DON'T call APIs here — inputs aren't ready yet
// ❌ DON'T access DOM here — not rendered yet
console.log('1. constructor() — class created');
}
ngOnInit(): void {
// ✅ Inputs are ready here
// ✅ Safe to call APIs here
// ✅ Setup subscriptions here
// ❌ Still can't access DOM (view not rendered)
console.log('2. ngOnInit() — inputs ready, fetch data');
this.userService.getUsers().subscribe(data => {
this.users.set(data);
});
}
}
The Golden Rule: Use
constructor() only for injection. Put everything else in ngOnInit(). This is because in the constructor, @Input values haven't been set yet by the parent.🔄 ngOnChanges() — Reacting to Input Changes
Signal inputs do NOT trigger ngOnChanges! Only old-style
@Input() decorators trigger it. For signal inputs, use effect() instead.import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
export class DisplayComponent implements OnChanges {
// Only @Input() triggers ngOnChanges (not signal input())
@Input() userId!: number;
ngOnChanges(changes: SimpleChanges): void {
// changes is an object: { propertyName: { currentValue, previousValue, firstChange } }
if (changes['userId']) {
const { currentValue, previousValue, firstChange } = changes['userId'];
if (firstChange) {
console.log('First time userId was set:', currentValue);
} else {
console.log(`userId changed: \${previousValue} → \${currentValue}`);
this.loadUserData(currentValue);
}
}
}
loadUserData(id: number) { /* fetch user */ }
}
// ─── Signal version (use effect() instead): ───
export class DisplayComponentNew {
userId = input<number>(0);
constructor() {
effect(() => {
const id = this.userId(); // tracked!
console.log('userId changed to:', id);
this.loadUserData(id);
});
}
}
New in Angular 18:
afterNextRender() and afterRender(). These replace the need to hack with ngAfterViewInit for third-party DOM libraries like Chart.js, D3, or any library that needs to touch the DOM after Angular renders.❌ Old approach: ngAfterViewInit
export class ChartComponent
implements AfterViewInit {
@ViewChild('canvas')
canvas!: ElementRef;
ngAfterViewInit() {
// Works but fires even on server,
// breaks SSR (Angular Universal)
new Chart(this.canvas.nativeElement, ...);
}
}
✅ New: afterNextRender()
import { afterNextRender, viewChild }
from '@angular/core';
export class ChartComponent {
canvas = viewChild<ElementRef>('canvas');
constructor() {
// Runs ONCE after first render
// Only runs in browser (SSR safe!)
afterNextRender(() => {
const el = this.canvas()?.nativeElement;
new Chart(el, ...);
});
}
}
afterNextRender() vs afterRender()
| Hook | Runs | Use for |
|---|---|---|
afterNextRender() | ONCE after next render | Initialize third-party libraries (Chart.js, D3, etc.) |
afterRender() | EVERY render cycle | Sync DOM measurements with signals (like a scroll position) |
🧠 Analogy: ngOnDestroy = Checkout from a Hotel
When you check out, you return the key card, cancel room service, and empty the mini fridge. ngOnDestroy is your "checkout" — return/cancel everything you started (subscriptions, intervals, event listeners).import { Component, OnDestroy, inject } from '@angular/core';
import { Subscription } from 'rxjs';
export class TimerComponent implements OnDestroy {
private intervalId!: ReturnType<typeof setInterval>;
private sub!: Subscription;
private destroy$ = new Subject<void>(); // common pattern
ngOnInit() {
// Start a timer
this.intervalId = setInterval(() => { ... }, 1000);
// Subscribe to an Observable
this.sub = this.dataService.getData()
.pipe(takeUntil(this.destroy$)) // auto-unsubscribe!
.subscribe(data => { ... });
}
ngOnDestroy(): void {
// ✅ ALWAYS clean up to prevent memory leaks!
clearInterval(this.intervalId); // stop the timer
this.sub.unsubscribe(); // stop the subscription
this.destroy$.next(); // signal all takeUntil to stop
this.destroy$.complete(); // close the subject
}
}
// Angular 18 alternative: use DestroyRef
export class ModernComponent {
private destroyRef = inject(DestroyRef);
ngOnInit() {
const sub = this.dataService.getData()
.pipe(takeUntilDestroyed(this.destroyRef)) // auto-cleanup!
.subscribe(...);
}
// No ngOnDestroy needed! DestroyRef handles it.
}
Click the lifecycle stage buttons to see hooks animate in real time and watch the execution log fill up.
⚡ Lifecycle Simulator
constructor()
Class created, inject() called
ngOnChanges()
@Input property changed
ngOnInit()
First render, inputs ready
ngDoCheck()
Custom change detection
ngAfterContentInit()
ng-content projected
ngAfterContentChecked()
After content check
ngAfterViewInit()
View rendered, DOM ready
ngAfterViewChecked()
After view re-check
ngOnDestroy()
Cleanup before removal
📋 Execution Log
🟢 Green = creation hooks
🔵 Blue = change/check hooks
🟣 Purple = view/child hooks
🔴 Red = destruction
🔵 Blue = change/check hooks
🟣 Purple = view/child hooks
🔴 Red = destruction
Module 7
💉 Services & Dependency Injection
Services hold shared logic and data. inject() is the new way to use them — no constructor needed. Learn @Injectable, inject(), and how to provide services.
What is a Service?
inject() Function
Creating Services
Scopes & Providers
HTTP + inject()
🧠 Analogy: Service = A Shared Toolbox
Imagine 5 workers (components) all need a hammer. Instead of each buying their own, they share one toolbox (service). The toolbox is created once and injected into any worker who needs it.Use a Service when:
• Multiple components share the same data
• You want to call an API
• You need to share state between unrelated components
• You want to separate business logic from UI
• Multiple components share the same data
• You want to call an API
• You need to share state between unrelated components
• You want to separate business logic from UI
Services vs Components:
• Component = UI logic + template
• Service = Business logic + data + API calls
• Rule of thumb: If it touches the DOM, it's in a component. Everything else belongs in a service.
• Component = UI logic + template
• Service = Business logic + data + API calls
• Rule of thumb: If it touches the DOM, it's in a component. Everything else belongs in a service.
💉 inject() — The Modern Way
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { UserService } from './user.service';
@Component({ standalone: true })
export class DashboardComponent {
// inject() called as field initializers — modern, clean!
private http = inject(HttpClient);
private router = inject(Router);
private userService = inject(UserService);
users = signal<User[]>([]);
ngOnInit() {
// Use the injected service
this.userService.getAll().subscribe(data => {
this.users.set(data);
});
}
goToProfile(id: number) {
this.router.navigate(['/profile', id]);
}
}
// inject() also works in factory functions and guards:
export const authGuard = () => {
const authService = inject(AuthService); // works here too!
return authService.isLoggedIn();
};
🏗️ Creating a Service — Full Example
import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root' // ← makes this service available app-wide, singleton
})
export class CartService {
private http = inject(HttpClient); // inject other services here too!
// State as signals — reactive!
private _items = signal<CartItem[]>([]);
// Public readable signal (read-only outside service)
items = this._items.asReadonly();
// Computed state derived from items
totalCount = computed(() => this._items().length);
totalPrice = computed(() =>
this._items().reduce((sum, i) => sum + i.price, 0)
);
addItem(item: CartItem) {
this._items.update(list => [...list, item]);
}
removeItem(id: number) {
this._items.update(list => list.filter(i => i.id !== id));
}
clearCart() {
this._items.set([]);
}
// API call returns Observable
checkout() {
return this.http.post('/api/checkout', { items: this._items() });
}
}
Where you provide a service determines how many instances exist and who can use it.
| Scope | How | Result |
|---|---|---|
| App-wide (singleton) | @Injectable({ providedIn: 'root' }) | One instance for entire app. Most common. ✅ |
| Module-scoped | @Injectable({ providedIn: UserModule }) | Only available in that module |
| Component-scoped | @Component({ providers: [MyService] }) | New instance per component. Destroyed with component. |
| Lazy-loaded route | { providers: [MyService] } in route config | New instance for that lazy chunk |
🌐 HTTP Calls with inject() and Signals
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUsers() {
// Returns an Observable<User[]>
return this.http.get<User[]>('https://api.example.com/users');
}
}
// In a component using signals pattern:
export class UsersComponent {
private userService = inject(UserService);
users = signal<User[]>([]);
loading = signal(true);
error = signal<string | null>(null);
ngOnInit() {
this.userService.getUsers().subscribe({
next: (data) => {
this.users.set(data);
this.loading.set(false);
},
error: (err) => {
this.error.set(err.message);
this.loading.set(false);
}
});
}
}
// Angular 18+ also has resource() API for HTTP with signals:
import { resource } from '@angular/core';
usersResource = resource({
loader: () => fetch('https://api.example.com/users').then(r => r.json())
});
// usersResource.value() = the data (signal!)
// usersResource.isLoading() = boolean signal
// usersResource.error() = error signal
Reference
📋 Angular 18+ Cheat Sheet
Everything in one place. Quick reference for all Angular 18+ syntax, patterns, and best practices.
Signals
Template Syntax
Component
Lifecycle
DI & inject()
Routing
// ───── SIGNALS QUICK REFERENCE ─────────────────
// Creating
count = signal(0); // number
name = signal('Alice'); // string
items = signal<string[]>([]); // typed array
user = signal<User | null>(null); // nullable
// Reading (always call with ()!)
const v = this.count(); // get value
const n = this.user()?.name; // optional chaining
// Writing
this.count.set(5); // replace value
this.count.update(v => v + 1); // update based on old
this.items.update(arr => [...arr, 'new']); // add to array
// Computed (read-only, auto-derived)
doubled = computed(() => this.count() * 2); // lazy, cached
total = computed(() => this.subtotal() + this.tax()); // chain signals
// Effect (side effects when signals change)
effect(() => { console.log(this.count()); }); // in constructor!
// Signal Inputs/Outputs
title = input<string>(''); // optional input
id = input.required<number>(); // required input
name = input<string>('', { alias: 'fullName' }); // aliased
clicked = output<string>(); // output event
value = model<string>(''); // two-way binding
// ViewChild
childComp = viewChild(ChildComponent); // signal ViewChild
el = viewChild<ElementRef>('refName'); // by template ref
// ───── NEW CONTROL FLOW ────────────────────────
// @if / @else if / @else
@if (cond) { ... } @else if (cond2) { ... } @else { ... }
// @for (track required!)
@for (item of list; track item.id) {
<div>{{ item.name }}</div>
} @empty { <div>Empty!</div> }
// @for variables
@for (x of list; track x.id; let i=$index, ev=$even, od=$odd, f=$first, l=$last, c=$count) {}
// @switch
@switch (val) {
@case ('a') { ... } @case ('b') { ... } @default { ... }
}
// @defer
@defer (on viewport) { <heavy-cmp /> }
@placeholder { <span>Waiting...</span> }
@loading (minimum 200ms) { <span>Loading...</span> }
@error { <span>Failed!</span> }
// ───── BINDINGS ───────────────────────────────
[property]="value" // Property binding
(event)="handler($event)" // Event binding
[(ngModel)]="field" // Two-way (banana in a box)
{{ expression }} // Interpolation
[class.active]="bool" // Conditional class
[style.color]="val" // Inline style binding
[attr.aria-label]="val" // Attribute binding
// ───── TEMPLATE REFS ─────────────────────────
<input #myInput> // Template ref variable
{{ myInput.value }} // Use in template
// ───── PIPES ─────────────────────────────────
{{ date | date:'yyyy-MM-dd' }} // DatePipe
{{ price | currency:'USD' }} // CurrencyPipe
{{ obs$ | async }} // AsyncPipe (auto-subscribes)
{{ text | uppercase }} // UpperCasePipe
{{ arr | slice:0:5 }} // SlicePipe
// ───── COMPONENT TEMPLATE ────────────────────
@Component({
selector: 'app-my', // HTML tag to use this component
standalone: true, // No NgModule (Angular 17+ default)
imports: [...], // Dependencies: directives, components, pipes
template: `<div>...</div>`, // Inline template OR...
templateUrl: './my.html', // ...external template file
styles: [`:host { ... }`], // Inline styles OR...
styleUrl: './my.css', // ...external CSS file
changeDetection: ChangeDetectionStrategy.OnPush, // recommended with signals
host: { 'class': 'my-host' } // add class to host element
})
export class MyComponent {
// Inputs
label = input(''); // optional with default
id = input.required<number>(); // required
// Outputs
save = output<string>(); // type of emitted value
// Two-way
value = model(''); // [(value)]="x" in parent
// State
count = signal(0);
double = computed(() => this.count() * 2);
// Services
private myService = inject(MyService);
}
// ───── LIFECYCLE HOOKS ORDER ─────────────────
export class MyComponent
implements OnInit, OnDestroy, OnChanges, AfterViewInit {
constructor() {
// 1st — inject() only!
afterNextRender(() => { ... }); // ← New! Once after first render
afterRender(() => { ... }); // ← New! After every render
effect(() => { ... }); // Always in constructor
}
ngOnChanges(c: SimpleChanges) {
// 2nd — only for @Input() (NOT signal input()!)
}
ngOnInit() {
// 3rd — inputs ready, fetch data here
}
ngDoCheck() {
// 4th — every change detection cycle (avoid if possible)
}
ngAfterContentInit() {
// 5th — after ng-content projected
}
ngAfterContentChecked() {
// 6th — after content checked (avoid if possible)
}
ngAfterViewInit() {
// 7th — DOM rendered, safe to use ViewChild
}
ngAfterViewChecked() {
// 8th — after every view re-check (be careful!)
}
ngOnDestroy() {
// Last — cleanup: unsubscribe, clearInterval
}
}
// ───── SERVICES & INJECT() ───────────────────
// Create a service
@Injectable({ providedIn: 'root' }) // app-wide singleton
export class MyService {
private http = inject(HttpClient); // inject in service too
data = signal<any[]>([]);
load() { return this.http.get('...'); }
}
// Use in component
export class MyComponent {
private myService = inject(MyService);
private router = inject(Router);
private route = inject(ActivatedRoute);
private destroyRef = inject(DestroyRef);
}
// inject() works in function guards too
export const authGuard = () => {
return inject(AuthService).isLoggedIn();
};
// ───── ROUTING (Angular 18+) ─────────────────
// app.routes.ts
export const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{
path: 'home',
loadComponent: () => import('./home.component')
.then(m => m.HomeComponent)
},
{
path: 'users/:id', // route with param
loadComponent: () => import('./user.component')
.then(m => m.UserComponent),
canActivate: [authGuard] // guard
}
];
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes,
withPreloading(PreloadAllModules) // optional
),
provideHttpClient() // for HTTP calls
]
});
// Read route params in component:
export class UserComponent {
private route = inject(ActivatedRoute);
ngOnInit() {
const id = this.route.snapshot.params['id'];
// or with Observable:
this.route.params.subscribe(p => { ... });
}
}
// Navigate programmatically:
private router = inject(Router);
this.router.navigate(['/users', id]);
this.router.navigate(['/users'], { queryParams: { page: 1 } });