Getting Started
CLI Setup & Project Creation
The Angular CLI is the primary tool for development. ng new scaffolds a complete project with routing and styling preconfigured. Use ng generate (ng g) for consistent file creation. The CLI handles build, test, and deployment.
# install Angular CLI globally
npm install -g @angular/cli
# create a new project
ng new my-app --routing --style=scss
cd my-app
# serve the app (default: localhost:4200)
ng serve --open
# generate components, services, etc.
ng generate component components/user-profile
ng g service services/auth
ng g module modules/admin --routingProject Structure Overview
Angular enforces a clear folder structure. src/app holds all feature code. main.ts bootstraps the root component. angular.json configures build, test, and serve options. Environments folder lets you swap API URLs per environment.
my-app/
├── src/
│ ├── app/ # application code lives here
│ │ ├── app.component.ts
│ │ ├── app.component.html
│ │ └── app.config.ts # standalone bootstrap config
│ ├── assets/ # static images, fonts
│ ├── environments/ # env-specific values
│ ├── index.html # main HTML entry
│ ├── main.ts # bootstrap entry point
│ └── styles.scss # global styles
├── angular.json # CLI workspace config
├── package.json
└── tsconfig.jsonServing & Building
ng serve starts a dev server with HMR-style reload on port 4200 by default. Production builds tree-shake, minify, and hash filenames for caching. Use --stats-json with webpack-bundle-analyzer to find bloated dependencies. ng test runs Karma by default.
# dev server with live reload
ng serve --port 4300 --open
# production build (outputs to dist/)
ng build --configuration production
# analyze bundle size
ng build --stats-json
npx webpack-bundle-analyzer dist/my-app/stats.json
# run unit tests (Karma + Jasmine)
ng test
# run end-to-end tests
ng e2eGenerating Code (Schematics)
Schematics ensure consistent file naming and boilerplate. The --standalone flag generates standalone components (recommended since v17). --route + --module wires up a lazy-loaded route automatically. Guards created with --functional return a function instead of a class.
# generate a standalone component
ng g component user-profile --standalone
# generate a service with root provider
ng g service services/auth
# generate a guard (functional by default)
ng g guard guards/auth --functional
# generate a pipe, directive, or module
ng g pipe pipes/shorten
ng g directive directives/highlight
ng g module admin --routing --route admin --module app
# generate an interface or enum
ng g interface models/user
ng g enum models/roleConfiguration (angular.json)
angular.json is the workspace config. Each project has build, serve, test, and lint targets. budgets warn or error when bundles exceed thresholds. polyfills array includes zone.js required by Angular. Multiple projects (app + library) can coexist in one workspace.
{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"outputPath": "dist/my-app",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [{ "type": "initial", "maximumWarning": "500kb" }]
}
}
}
}
}
}
}Environments
Angular swaps environment files during build based on the configuration (dev vs prod). File replacements are configured in angular.json under fileReplacements. Inject the environment object anywhere to access env-specific values without hardcoded URLs. Never commit real secrets to source control.
// src/environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
apiKey: 'dev-key'
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.example.com',
apiKey: 'prod-key'
};
// usage in a service
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class ApiService {
private baseUrl = environment.apiUrl;
}Components Basics
Basic Component
A component is a TypeScript class decorated with @Component. selector is the custom HTML tag used in templates. templateUrl/styleUrls point to external files; use template/styles for inline. @Input marks properties that parents can bind to. Keep components focused on one responsibility.
// user-profile.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss'],
})
export class UserProfileComponent {
@Input() name = 'Guest';
@Input() age = 0;
greet() {
return `Hello, ${this.name}!`;
}
}Inline Template Component
Inline templates are convenient for small components and avoid extra files. Use backticks for multi-line strings — escape backticks inside as \`. styles accepts an array of CSS strings. :host selector targets the component's own element. Prefer external files once a template grows beyond ~10 lines.
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{ count }}</span>
<button (click)="increment()">+</button>
`,
styles: [`
:host { display: flex; gap: 8px; align-items: center; }
button { padding: 4px 12px; }
`],
})
export class CounterComponent {
count = 0;
increment() { this.count++; }
decrement() { this.count--; }
}Standalone Component (v14+)
Standalone components don't need an NgModule — they declare their own imports array listing dependencies (CommonModule, FormsModule, other components). This is the modern recommended approach. Import only what you use for better tree-shaking. Bootstrap standalone components directly in main.ts.
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-search',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<input [(ngModel)]="query" placeholder="Search..." />
<p>Searching for: {{ query }}</p>
`,
})
export class SearchComponent {
query = '';
}Inputs & Outputs
@Input lets parents pass data in via property bindings. @Output exposes an EventEmitter that parents subscribe to via event bindings. Always initialize inputs with defaults to avoid undefined errors. Use the new input()/output() signals API for cleaner code in v17+.
@Component({
selector: 'app-toggle',
template: `
<button (click)="toggle()">{{ label }}: {{ on ? 'On' : 'Off' }}</button>
`,
})
export class ToggleComponent {
@Input() label = 'Toggle';
@Input() on = false;
@Output() changed = new EventEmitter<boolean>();
toggle() {
this.on = !this.on;
this.changed.emit(this.on);
}
}
<!-- parent template -->
<app-toggle [label]="'Power'" [on]="isOn" (changed)="onToggle($event)" />Signal-Based Inputs (v17.1+)
The input() function creates a signal-based, readonly input that you call like a function. It supports required inputs via input.required<T>(), transforms, and works seamlessly with computed() and effect(). output() is the modern replacement for @Output EventEmitter. This is the future of Angular component APIs.
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div>{{ title() }} - {{ count() }}</div>
<button (click)="clicked.emit()">Click</button>
`,
})
export class CardComponent {
// readonly signal inputs with defaults
title = input<string>('Default');
count = input<number>(0);
// output() function replaces @Output
clicked = output<void>();
// computed derived from inputs
// requires import { computed }
// label = computed(() => this.title().toUpperCase());
}View Encapsulation
Emulated (default) adds unique attributes to scope styles to the component without real Shadow DOM. None makes styles global (use sparingly). ShadowDom uses native Shadow DOM for true isolation but affects styling penetration and some third-party libraries. Stick with Emulated unless you have specific needs.
@Component({
selector: 'app-styles',
template: `<p class="local">Emulated (default)</p>`,
styles: [`p.local { color: red; }`],
encapsulation: ViewEncapsulation.Emulated, // default
})
export class StylesComponent {}
// ViewEncapsulation.None -> styles leak globally
// ViewEncapsulation.ShadowDom -> real Shadow DOM (strong isolation)
// ViewEncapsulation.Emulated -> scoped attributes (default, recommended)Template Syntax
Text Interpolation
Double curly braces {{ }} render expressions as text, HTML-escaped. You can use most JS expressions: arithmetic, ternary, method calls, property access. No new, typeof, or assignment operators. Pipes (|) transform output. Interpolation re-evaluates on every change detection cycle.
<p>{{ title }}</p> <!-- renders the value -->
<p>{{ 1 + 2 }}</p> <!-- expressions allowed -->
<p>{{ user.name }}</p> <!-- property access -->
<p>{{ items.length }}</p> <!-- method/property -->
<p>{{ price | currency:'USD' }}</p> <!-- pipe in interpolation -->
<p>{{ isReady ? 'Yes' : 'No' }}</p> <!-- ternary -->
<!-- one-time interpolation updates when the source changes -->
<!-- new control flow @if, @for work without *ngIf/*ngFor directives -->Property & Attribute Binding
[prop] binds to a DOM property (preferred). [attr.name] binds to HTML attributes for cases without a DOM property (colspan, SVG attrs). [class.x] toggles a single class by truthiness; [style.prop] sets a style, with optional unit suffix like .px or %. Avoid binding to innerHTML with untrusted content.
<!-- property binding: [prop]="expression" -->
<img [src]="user.avatar" [alt]="user.name" />
<button [disabled]="isSaving">Save</button>
<my-comp [data]="items"></my-comp>
<!-- attribute binding (for attrs without DOM props) -->
<td [attr.colspan]="2">Merged cell</td>
<svg [attr.viewBox]="box"></svg>
<!-- class & style binding -->
<div [class.active]="isActive">Active?</div>
<div [class]="dynamicClassString">
<div [style.color]="colorVar">
<div [style.width.px]="size">Width</div>Event Binding
(event) binds to DOM events and custom EventEmitter outputs. $event is the event payload (DOM Event for native events, emitted value for custom). Event modifiers like .enter, .esc, .prevent, .stop filter or transform events. The target in parentheses determines what triggers the handler.
<button (click)="save()">Save</button>
<input (input)="onInput($event)" (keyup)="onKey($event)" />
<div (mouseenter)="hover=true" (mouseleave)="hover=false">Hover</div>
<!-- $event is the DOM event object -->
<input (keyup.enter)="submit()" (keyup.esc)="cancel()" />
<!-- passive events (scroll, touchmove) -->
<div (scroll.passive)="onScroll($event)">...</div>
<!-- binding to custom component outputs -->
<app-child (changed)="onChanged($event)"></app-child>Template Reference Variables
A #var (or ref-var) references the DOM element or component instance it's declared on. Accessible anywhere in the template. For components, the value is the component instance; for elements, it's the HTMLElement. Inside @for, $index, $first, $last, $even, $odd are implicit context variables.
<input #nameInput type="text" />
<button (click)="nameInput.focus()">Focus</button>
<p>You typed: {{ nameInput.value }}</p>
<!-- ref to a component instance -->
<app-form #f="appForm"></app-form>
<button [disabled]="!f.valid" (click)="f.submit()">Submit</button>
<!-- ref inside @for -->
<ul>
@for (item of items; track item.id) {
<li>{{ item.name }} (index: {{ $index }})</li>
}
</ul>Two-Way Binding (Banana in a Box)
[(ngModel)] requires FormsModule. The [(x)] syntax desugars to [x] + (xChange), so custom two-way bindings need an input named x and an output named xChange. Remember the mnemonic 'banana in a box' — the parentheses go inside the brackets. The model() signal function simplifies this in v17.2+.
<!-- [()] = "banana in a box" - property + event combined -->
<input [(ngModel)]="username" />
<!-- equivalent to -->
<input [ngModel]="username" (ngModelChange)="username = $event" />
<!-- two-way binding on custom components -->
<app-counter [(value)]="count"></app-counter>
<!-- requires: @Input() value; @Output() valueChange = new EventEmitter() -->
<!-- signals: model() for two-way (v17.2+) -->
// value = model<number>(0);Safe Navigation & Non-Null Assertion
The ?. operator safely navigates nullable chains, returning undefined instead of throwing. Use it for async data not yet loaded. The ! operator is a compile-time assertion only — it doesn't add runtime checks, so use only when you're certain. For arrays that may be null, use ?? [] as a fallback.
<!-- safe navigation: short-circuits to undefined if null -->
<p>{{ user?.profile?.bio }}</p>
<p>{{ user?.getName?.() }}</p>
<!-- non-null assertion: tells compiler it's not null -->
<p>{{ user!.name }}</p>
<!-- in property bindings -->
<img [src]="user?.avatar" />
<app-detail [data]="user!"></app-detail>
<!-- safe navigation in @for doesn't replace track -->
@for (item of items ?? []; track item.id) { ... }Directives
@if / @else (New Control Flow)
The new @if/@else if/@else block syntax replaces *ngIf. No import of CommonModule or NgIf needed, no asterisk, and @else works natively (unlike ngIf's ng-template hack). This is the recommended syntax for new code. It's part of the built-in control flow introduced in v17 and stable in v18.
@if (user) {
<p>Hello, {{ user.name }}</p>
} @else if (loading) {
<p>Loading...</p>
} @else {
<p>No user found.</p>
}
<!-- no structural directive import needed, no * prefix -->
<!-- works in standalone templates automatically (v17+) -->@for / @empty (New Control Flow)
@for replaces *ngFor with mandatory track expression for efficient DOM diffing. @empty renders fallback content when the collection is empty. Implicit context variables ($index, $first, $last, etc.) are always available. Tracking by a stable unique id is critical for performance with large lists.
<ul>
@for (item of items; track item.id) {
<li>{{ item.name }} ({{ $index }})</li>
} @empty {
<li>No items yet.</li>
}
</ul>
<!-- 'track' is REQUIRED for performance -->
<!-- implicit vars: $index, $first, $last, $even, $odd, $count -->
<!-- @for over a range -->
@for (i of [1,2,3,4,5]; track i) {
<span>{{ i }}</span>
}ngClass & ngStyle
ngClass accepts an object (truthy keys applied), array, or string. ngStyle accepts an object with camelCase or kebab-case keys, with optional unit suffixes. Requires importing CommonModule or the specific directives. For single class/style, [class.x] and [style.x] are lighter and don't need imports.
<div [ngClass]="{ active: isActive, disabled: !enabled }">Box</div>
<div [ngClass]="['card', 'shadow', themeClass]">Card</div>
<div [ngClass]="currentClass">String</div>
<div [ngStyle]="{ color: textColor, 'font-size.px': size }">Text</div>
<div [ngStyle]="styleObject">Styled</div>
<!-- import CommonModule (or NgClass/NgStyle) for these -->
<!-- prefer [class.x] and [style.x] for single value bindings -->@switch (New Control Flow)
@switch replaces *ngSwitch (and the ngSwitchCase/ngSwitchDefault directives). It's cleaner — no wrapping container needed, no imports. Each @case renders only when its value matches the @switch expression. @default is the fallback. This is the built-in control flow available in v17+.
@switch (status) {
@case ('loading') {
<spinner>Loading...</spinner>
}
@case ('error') {
<p class="error">Something went wrong</p>
}
@case ('success') {
<p>Done!</p>
}
@default {
<p>Idle</p>
}
}Custom Attribute Directive
Attribute directives change the appearance or behavior of DOM elements. The [brackets] in selector match attribute usage. ElementRef gives direct DOM access; HostListener binds to DOM events. Inject ElementRef<HTMLElement> for type safety. Prefer Renderer2 for DOM manipulation in server-side rendering scenarios.
import { Directive, ElementRef, Input, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]',
})
export class HighlightDirective {
@Input('appHighlight') highlightColor = 'yellow';
constructor(private el: ElementRef<HTMLElement>) {}
@HostListener('mouseenter') onMouseEnter() {
this.el.nativeElement.style.backgroundColor = this.highlightColor;
}
@HostListener('mouseleave') onMouseLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}
<!-- usage: <p appHighlight="lightblue">Hover me</p> -->Structural Directive Internals
Structural directives add/remove DOM nodes via TemplateRef (the wrapped template) and ViewContainerRef (where to insert). The asterisk (*) is sugar that wraps the element in an <ng-template>. The new @if/@for/@switch blocks replace most custom structural directives, but this pattern is still useful for advanced cases.
import { Directive, TemplateRef, ViewContainerRef, Input } from '@angular/core';
@Directive({ selector: '[appUnless]' })
export class UnlessDirective {
private hasView = false;
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.vc.createEmbeddedView(this.tpl);
this.hasView = true;
} else if (condition && this.hasView) {
this.vc.clear();
this.hasView = false;
}
}
constructor(private tpl: TemplateRef<unknown>, private vc: ViewContainerRef) {}
}
<!-- usage: <div *appUnless="hide">Shown when hide is false</div> -->Pipes
Built-in Pipes
Pipes transform template display values. Common built-ins: date, number, currency, percent, uppercase, lowercase, titlecase, slice, json, async. Pipes are pure by default — they recompute only when the input reference changes. Format strings follow Angular's locale-aware rules.
<!-- date formatting -->
<p>{{ today | date }}</p> <!-- Sep 10, 2024 -->
<p>{{ today | date:'short' }}</p>
<p>{{ today | date:'yyyy-MM-dd HH:mm' }}</p>
<!-- number & currency -->
<p>{{ 3.14159 | number:'1.2-2' }}</p> <!-- 3.14 -->
<p>{{ price | currency:'USD':'symbol':'1.2-2' }}</p>
<p>{{ ratio | percent }}</p>
<!-- text transforms -->
<p>{{ 'hello world' | uppercase }}</p> <!-- HELLO WORLD -->
<p>{{ 'HELLO' | lowercase }}</p>
<p>{{ title | titlecase }}</p>
<p>{{ 'a/b/c' | slice:0:2 }}</p>
<!-- json (for debugging) -->
<pre>{{ obj | json }}</pre>Chaining Pipes & Parameters
Pipes chain left-to-right, each receiving the previous output. Parameters are colon-separated and positional. Use them for pure transformations (formatting, filtering) to keep templates declarative. Avoid impure pipes for performance-sensitive data; compute in the component instead.
<!-- chain: left to right, output of one feeds the next -->
<p>{{ ' hello ' | trim | uppercase }}</p> <!-- HELLO -->
<p>{{ birthday | date:'fullDate' | uppercase }}</p>
<p>{{ items | slice:0:3 | json }}</p>
<!-- parameters separated by colons -->
<p>{{ amount | number:'1.2-2':'en-US' }}</p>
<p>{{ meeting | date:'shortTime' }}</p>
<p>{{ list | slice:1:5 }}</p>
<!-- multiple parameters -->
<p>{{ value | myPipe:arg1:arg2:arg3 }}</p>Custom Pipe
Custom pipes implement PipeTransform.transform(). The @Pipe name is used in templates. Register standalone pipes (v15+) in a component's imports array, or declare in an NgModule. Pure pipes (default) only re-run when the input reference changes — pass new references to trigger updates.
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 20, ellipsis = '...'): string {
if (!value) return '';
return value.length > limit
? value.slice(0, limit) + ellipsis
: value;
}
}
// register in a standalone component imports:
// imports: [TruncatePipe]
<!-- usage -->
<p>{{ longText | truncate:50 }}</p>
<p>{{ longText | truncate:10:'>>' }}</p>Async Pipe
The async pipe subscribes to an Observable or Promise, renders the value, and unsubscribes automatically on destroy — no manual subscribe/unsubscribe needed. Combined with *ngIf as or @for, it cleanly handles null while loading. It marks the component for check when a new value arrives, integrating with OnPush.
@Component({
selector: 'app-user',
template: `
<div *ngIf="user$ | async as user">
{{ user.name }}
</div>
<ul>
@for (item of items$ | async; track item.id) {
<li>{{ item.name }}</li>
}
</ul>
`,
imports: [AsyncPipe, NgIf],
})
export class UserComponent {
user$ = this.http.get<User>('/api/user');
items$ = this.http.get<Item[]>('/api/items');
constructor(private http: HttpClient) {}
}Pure vs Impure Pipes
Pure pipes cache results until the input reference changes; mutating arrays or objects won't trigger them. Impure pipes (pure: false) re-run every change detection — convenient for filtering mutated arrays but costly. Prefer keeping data immutable (replace arrays instead of mutating) and use pure pipes for performance.
// PURE (default): re-runs only when input reference changes
@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {
transform(items: Item[], term: string): Item[] {
return items.filter(i => i.name.includes(term));
}
}
// Issue: mutating items.push() won't trigger re-evaluation.
// IMPURE: re-runs on every change detection cycle
@Pipe({ name: 'filter', pure: false })
export class ImpureFilterPipe implements PipeTransform { ... }
// ⚠️ Expensive! Use sparingly — affects performance.i18n & Locale Pipes
Pipes like date, number, currency respect LOCALE_ID. Register locale data via registerLocaleData before bootstrap. The currency/decimal formats follow the active locale. For multi-language apps, provide LOCALE_ID per user or use the i18n tooling for template translations. Missing locale data falls back to en-US.
// register locale data in app.config.ts
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
registerLocaleData(localeFr);
// provide a default locale
providers: [{ provide: LOCALE_ID, useValue: 'fr-FR' }]
// usage - now locale-aware
<p>{{ price | currency:'EUR' }}</p> <!-- 1 234,56 € -->
<p>{{ today | date:'fullDate' }}</p> <!-- lundi 10 septembre 2024 -->
<p>{{ number | number:'1.2-2' }}</p> <!-- 1 234,56 -->
<p>{{ amount | currency:'JPY' }}</p> <!-- ¥1,235 -->Services & Dependency Injection
Basic Service
providedIn: 'root' creates a singleton available app-wide without module registration. The service is tree-shakeable — removed if unused. Use the inject() function (v14+) instead of constructor injection for cleaner code and easier testing. Services hold shared state and business logic; components stay presentational.
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class UserService {
private users = ['Alice', 'Bob'];
getUsers() { return this.users; }
addUser(name: string) { this.users.push(name); }
}
// usage in a component
export class UserListComponent {
// Angular injects the singleton instance
constructor(private users: UserService) {}
get users() { return this.usersService.getUsers(); }
private usersService = inject(UserService);
}inject() Function
inject() is the modern alternative to constructor injection. It works in field initializers, making the code more readable and enabling better TypeScript inference. It must run during construction (not in async callbacks later). Use it consistently — mixing constructor params and inject() is fine but inject() is cleaner.
import { Injectable, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
private router = inject(Router);
login(creds: Credentials) {
return this.http.post('/api/login', creds);
}
}
// in a component
export class DashboardComponent {
private auth = inject(AuthService);
user = this.auth.currentUser; // works in field initializers
}Provider Configuration
Providers can be configured at any level (root, module, component). useClass swaps implementations (great for mocking). useValue provides constants. useExisting aliases one service to another. useFactory creates instances dynamically. InjectionToken is for non-class values like strings or config objects.
@Component({
// ... provide at component level for instance-per-component
providers: [
UserService, // shorthand
{ provide: Logger, useClass: ConsoleLogger },
{ provide: API_URL, useValue: 'https://api.example.com' },
{ provide: Storage, useExisting: LocalStorageService },
{
provide: Config,
useFactory: () => new Config(env.production)
},
],
})
export class AdminComponent {}
// InjectionToken for non-class values
export const API_URL = new InjectionToken<string>('API_URL');Hierarchical Injectors
Angular's DI tree mirrors the component tree. providedIn: 'root' = one instance app-wide. Component-level providers create child injectors — each instance gets its own. This is useful for stateful components like editors. 'any' scopes to lazy modules. 'platform' is rare, used for multi-app setups.
// root injector (singleton app-wide)
@Injectable({ providedIn: 'root' })
export class AuthService {}
// component-level provider: new instance for this component and children
@Component({
providers: [UserService], // each AdminComponent gets its own UserService
})
export class AdminComponent {}
// 'any' creates a lazy singleton per lazy module
@Injectable({ providedIn: 'any' })
export class SharedService {}
// 'platform' shared across all Angular apps on the page
@Injectable({ providedIn: 'platform' })
export class GlobalConfig {}Multi Providers & Tokens
multi: true registers multiple providers under one token, injected as an array. Useful for plugin systems, interceptors, validators, or any extensible collection. The order of injection follows registration order. Combine with InjectionToken to type the array properly.
// define a token for a list of plugins
export const PLUGIN = new InjectionToken<Plugin>('PLUGIN');
// provide multiple plugins
@Component({
providers: [
{ provide: PLUGIN, useClass: AuthPlugin, multi: true },
{ provide: PLUGIN, useClass: AnalyticsPlugin, multi: true },
],
})
export class AppComponent {}
// inject all of them as an array
export class PluginManager {
plugins = inject(PLUGIN); // Plugin[]
}Optional & Default Providers
@Optional() or inject(token, { optional: true }) returns null if no provider is found instead of throwing. Use it for non-critical dependencies. Other modifiers: @SkipSelf() (don't check current injector), @Self() (only current), @Host() (up to host component). inject() equivalents accept flags in the options object.
import { Injectable, inject, Optional, Inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class Logger {
constructor(@Optional() @Inject(CONFIG) private cfg?: Config) {
// cfg is null if no provider exists — handle gracefully
}
}
// or with inject()
export class Service {
cfg = inject(CONFIG, { optional: true }); // Config | null
baseUrl = inject(API_URL, { optional: true }) ?? '/api';
}
// skipSelf: look in parent injectors only
// self: only this component's injector
// host: limit to host componentRouting
Route Configuration
Routes map URL paths to components. loadComponent/loadChildren enable lazy loading (smaller initial bundle). :id is a route parameter. The wildcard '**' catches unmatched URLs — place it last. provideRouter sets up the router in standalone apps. Set title for browser tab names; it can be a string or a ResolveFn.
// app.routes.ts (standalone)
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', component: HomeComponent, title: 'Home' },
{ path: 'about', loadComponent: () => import('./about/about.component')
.then(m => m.AboutComponent) },
{ path: 'users/:id', component: UserDetailComponent },
{ path: 'admin', loadChildren: () => import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES) },
{ path: '**', component: NotFoundComponent }, // wildcard
];
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)]
};Router Outlet & Links
router-outlet marks where routed components render. routerLink creates navigation links (use array form for params). routerLinkActive applies a class when the route matches. Programmatic navigation uses router.navigate() or routerLink. Named outlets enable secondary routes (e.g., a modal alongside the main view).
<!-- app.component.html -->
<nav>
<a routerLink="/" routerLinkActive="active">Home</a>
<a routerLink="/about" routerLinkActive="active">About</a>
<a [routerLink]="['/users', userId]">My Profile</a>
</nav>
<router-outlet></router-outlet>
<!-- named outlet (secondary route) -->
<router-outlet name="popup"></router-outlet>
<!-- navigate programmatically -->
<button (click)="go()">Go</button>
// constructor(private router: Router) {}
// go() { this.router.navigate(['/users', 42]); }Route Parameters & Query Params
Use snapshot for one-time reads when the component is destroyed and recreated per navigation. Use the observable param/queryParamMap when the same component instance handles different params (e.g., navigating from /users/1 to /users/2). Always unsubscribe or use async pipe to avoid leaks.
// reading route params
export class UserDetailComponent {
constructor(private route: ActivatedRoute) {
// snapshot: one-time read (good for static links)
const id = this.route.snapshot.paramMap.get('id');
// observable: reacts to param changes (same component reused)
this.route.paramMap.subscribe(params => {
console.log(params.get('id'));
});
}
}
// query params: /search?q=angular&page=2
this.route.queryParamMap.subscribe(qp => {
this.q = qp.get('q');
});
<!-- link with query params -->
<a [routerLink]="['/search']" [queryParams]="{ q: 'angular', page: 2 }">Child & Nested Routes
Child routes render in a nested router-outlet inside the parent component. redirectTo with pathMatch: 'full' redirects empty child paths. This pattern builds master-detail layouts (parent chrome, child content). Each level of nesting needs its own router-outlet in the parent's template.
export const routes: Routes = [
{
path: 'admin',
component: AdminLayoutComponent,
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'users', component: AdminUsersComponent },
{ path: 'settings', component: SettingsComponent },
],
},
];
<!-- AdminLayoutComponent template -->
<h2>Admin Panel</h2>
<nav>...</nav>
<router-outlet></router-outlet> <!-- child routes render here -->Lazy Loading
Lazy loading splits the app into chunks loaded on demand, shrinking the initial bundle. loadChildren loads a route file (default export of Routes); loadComponent loads a single standalone component. The dynamic import() string must be a static path for the bundler to split correctly. Use this for feature areas behind navigation.
// main routes - load feature modules on demand
export const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
},
{
path: 'reports',
loadComponent: () => import('./reports/reports.component')
.then(m => m.ReportsComponent),
},
];
// admin.routes.ts
export const ADMIN_ROUTES: Routes = [
{ path: '', component: AdminComponent },
{ path: 'users', component: AdminUsersComponent },
];Router Events & Navigation
Router events (NavigationStart, NavigationEnd, GuardsCheckStart, etc.) let you track navigation lifecycle. Always filter to specific events and unsubscribe. router.navigate accepts extras: queryParams, fragment, state (transient data not in URL), and replaceUrl. state is readable via router.getCurrentNavigation().extras.state.
// subscribe to router events
export class AppComponent implements OnDestroy {
private sub = this.router.events.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd)
).subscribe(e => {
console.log('Navigated to:', e.url);
this.analytics.track(e.url);
});
constructor(private router: Router) {}
ngOnDestroy() { this.sub.unsubscribe(); }
}
// navigation with extras
this.router.navigate(['/users'], {
queryParams: { sort: 'name' },
fragment: 'top',
state: { from: 'login' }, // passed to next route, not in URL
});Forms
Template-driven Form
Template-driven forms use ngModel and template references. FormsModule is required. The form's state (validity, touched, dirty) is tracked via ngForm and ngModel directives. Best for simple forms. Validation is declared in the template (required, minlength, pattern). Access control state via template refs like #e='ngModel'.
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-login',
standalone: true,
imports: [FormsModule],
template: `
<form #f="ngForm" (ngSubmit)="save(f.value)">
<input name="email" [(ngModel)]="email" required email #e="ngModel" />
<span *ngIf="e.invalid && e.touched">Email required</span>
<input name="pwd" type="password" [(ngModel)]="pwd" required minlength="6" />
<button [disabled]="f.invalid">Submit</button>
</form>
`,
})
export class LoginComponent {
email = '';
pwd = '';
save(v: any) { console.log(v); }
}Reactive Form Setup
Reactive forms define the model in the component class as FormControl/FormGroup instances. ReactiveFormsModule is required. formControlName binds inputs to controls. The form model is observable and testable without the DOM. nonNullable: true prevents null values when the input is cleared. Preferred for complex forms.
import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms';
@Component({
selector: 'app-profile',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="save()">
<input formControlName="name" />
<input formControlName="email" />
<button [disabled]="form.invalid">Save</button>
</form>
<p>Valid: {{ form.valid }}</p>
`,
})
export class ProfileComponent {
form = new FormGroup({
name: new FormControl('', { nonNullable: true }),
email: new FormControl('', { nonNullable: true }),
});
save() { console.log(this.form.value); }
}FormBuilder & Typed Forms
FormBuilder reduces boilerplate. fb.group() infers types from defaults. Nested groups model nested data. FormArray manages dynamic lists of controls. With nonNullable, controls stay as their declared type instead of null. Use get() or controls for type-safe access. Typed forms (v14+) give you autocomplete and type checking on value.
import { FormBuilder, FormGroup } from '@angular/forms';
export class CheckoutComponent {
private fb = inject(FormBuilder);
// typed form (v14+ nonNullable)
form = this.fb.group({
name: ['', { nonNullable: true }],
address: this.fb.group({
street: [''],
city: [''],
zip: [''],
}),
items: this.fb.array<CartItem>([]),
});
// type-safe access
get items() { return this.form.controls.items; }
addItem(item: CartItem) { this.items.push(this.fb.control(item)); }
}Form Validation
Validators.required, .minLength, .maxLength, .pattern, .email, .min, .max are built-in. Apply multiple via an array. Cross-field validation lives on the FormGroup with a validator function receiving the whole group. Check control.touched/dirty before showing errors to avoid scolding users on first render. Use .hasError('key') to test specific errors.
import { Validators } from '@angular/forms';
// built-in validators
const email = new FormControl('', [
Validators.required,
Validators.email,
Validators.maxLength(100),
]);
const password = new FormControl('', [
Validators.required,
Validators.minLength(8),
Validators.pattern(/[A-Z]/),
]);
// cross-field validation at group level
const form = this.fb.group({
pwd: ['', Validators.required],
confirm: ['', Validators.required],
}, { validators: matchPassword }); // custom group validator
// reading state in template
<!-- <span *ngIf="email.touched && email.hasError('required')">Required</span> -->Custom Validator
Custom validators are functions returning ValidationErrors (a map) or null if valid. Async validators return an Observable or Promise — use them for server-side checks like username availability. Always debounce async validators (via timer/switchMap) to avoid hammering the server on every keystroke.
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
// function validator
export const matchPassword: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
const pwd = group.get('pwd')?.value;
const confirm = group.get('confirm')?.value;
return pwd === confirm ? null : { mismatch: true };
};
// async validator (returns Observable/Promise)
export function uniqueUsername(http: HttpClient): AsyncValidatorFn {
return (control: AbstractControl) => {
return timer(500).pipe(
switchMap(() => http.get(`/api/check/${control.value}`)),
map(exists => exists ? { taken: true } : null),
);
};
}Form Value Changes & State
valueChanges and statusChanges are Observables that emit on every input. debounceTime prevents excessive work on rapid typing. takeUntil with a Subject is the idiomatic way to unsubscribe on destroy. patchValue updates partial form values; setValue requires all fields. Use { emitEvent: false } to update silently without triggering valueChanges.
export class FilterComponent implements OnDestroy {
private destroy = new Subject<void>();
form = this.fb.group({ q: [''], category: ['all'] });
ngOnInit() {
// react to entire form changes (debounced)
this.form.valueChanges.pipe(
debounceTime(300),
takeUntil(this.destroy),
).subscribe(v => this.search(v));
// react to a single control
this.form.controls.q.valueChanges.subscribe(q => {
console.log('q changed:', q);
});
}
ngOnDestroy() { this.destroy.next(); }
}HttpClient
Setup & Basic GET
provideHttpClient() registers the client app-wide. HttpClient methods return cold Observables — they only execute when subscribed (use async pipe in templates or subscribe in services). Responses are typed via generics. Always handle errors centrally or per-call. withFetch() (v18) opts into the modern fetch-based backend.
// app.config.ts
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient()],
};
// usage in a service
@Injectable({ providedIn: 'root' })
export class PostService {
private http = inject(HttpClient);
private api = 'https://jsonplaceholder.typicode.com/posts';
getPosts() {
return this.http.get<Post[]>(this.api);
}
}
// in a component (use async pipe to auto-unsubscribe)
export class PostListComponent {
posts$ = this.posts.getPosts();
constructor(private posts: PostService) {}
}POST / PUT / DELETE
post/put/patch return Observables emitting the response once, then completing. Pass the body as the second argument. Generic type narrows the response. The URL can be a template literal with params. Always subscribe — Observables are cold and won't fire otherwise. Consider toPromise() (deprecated) or firstValueFrom() for one-shot reads.
export class PostService {
private http = inject(HttpClient);
create(post: Omit<Post, 'id'>) {
return this.http.post<Post>('/api/posts', post);
}
update(id: number, patch: Partial<Post>) {
return this.http.put<Post>(`/api/posts/${id}`, patch);
}
remove(id: number) {
return this.http.delete<void>(`/api/posts/${id}`);
}
}
// all return Observables — subscribe or use async pipe
// HTTP methods return a single value then completeQuery Parameters & Headers
params accepts an object, HttpParams, or array — Angular encodes them into the query string. headers sets request headers. observe: 'response' returns the full HttpResponse (status, headers, body) instead of just the body. observe: 'events' with reportProgress streams progress events for file uploads/downloads.
export class SearchService {
private http = inject(HttpClient);
search(term: string, page = 1) {
return this.http.get<Result[]>('/api/search', {
params: { q: term, page, sort: 'desc' },
headers: { 'X-Api-Key': 'secret' },
observe: 'response', // get full HttpResponse
responseType: 'json',
});
}
// observe: 'events' streams upload/download progress
upload(file: File) {
return this.http.post('/api/upload', file, {
reportProgress: true,
observe: 'events',
});
}
}Interceptors
Functional interceptors (v15+) are plain functions chained via withInterceptors(). They run in order for every request, perfect for auth tokens, logging, retry logic, or caching. clone() creates a modified request (requests are immutable). The next(req) function returns the response stream — pipe operators like retry, catchError apply here.
// functional interceptor (v15+)
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('token');
const authReq = token
? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
: req;
return next(authReq);
};
// register in app.config.ts
providers: [provideHttpClient(withInterceptors([authInterceptor]))]
// error/logging interceptor
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
console.log('Request:', req.url);
return next(req).pipe(tap({
error: e => console.error('Failed:', req.url, e),
}));
};Error Handling
Use RxJS operators for HTTP resilience: retry re-subscribes on error, timeout aborts slow requests, catchError maps errors to fallback values or re-throws. Always handle errors at the consumer side too — uncaught Observable errors stop the stream. Subscribe with next/error/complete callbacks for full lifecycle handling.
export class PostService {
getPost(id: number) {
return this.http.get<Post>(`/api/posts/${id}`).pipe(
retry(2), // retry on failure
timeout(5000), // abort after 5s
catchError(err => {
if (err.status === 404) return of(null); // graceful fallback
return throwError(() => new Error('Network error'));
}),
);
}
}
// component-side
this.posts.getPost(1).subscribe({
next: p => this.post = p,
error: e => this.error = e.message,
complete: () => this.loading = false,
});Typed Responses & withFetch
Generics type the response body but don't validate at runtime — for safety, use a runtime validator like zod. observe: 'response' exposes status, headers, and body. withFetch() (v18) uses the Fetch API instead of XHR, enabling streaming and better server-side rendering. withXsrfConfiguration adds automatic CSRF token handling.
// typed generic — response is typed, runtime NOT validated
this.http.get<User[]>('/api/users');
// full HttpResponse observation
this.http.get<User>('/api/users/1', { observe: 'response' })
.subscribe(res => {
console.log(res.status); // 200
console.log(res.headers.get('X-Total-Count'));
console.log(res.body); // User
});
// opt into fetch() backend (v18+, recommended)
// provideHttpClient(withFetch())
// with XSRF protection enabled
// provideHttpClient(withXsrfConfiguration({ cookieName: 'XSRF-TOKEN' }))Observables & RxJS
Basic Observable
Observables are lazy — the producer only runs when subscribed. They may emit zero or more values, then complete or error. The subscribe callback receives next, error, complete. Always unsubscribe from long-lived subscriptions (or use async pipe / takeUntilDestroyed). of() emits values synchronously; from() flattens arrays or promises.
import { Observable } from 'rxjs';
const obs = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
setTimeout(() => {
subscriber.next(3);
subscriber.complete();
}, 1000);
});
const sub = obs.subscribe({
next: v => console.log(v),
error: e => console.error(e),
complete: () => console.log('done'),
});
// cleanup
sub.unsubscribe();
// use of(...) for synchronous values
of(1, 2, 3).subscribe(v => console.log(v));
// from(iterable) for arrays/async iterables
from([1, 2, 3]).subscribe(console.log);Common Operators
Operators transform streams. map/filter work like array methods. debounceTime waits for a quiet period (great for search inputs). distinctUntilChanged skips duplicate consecutive values. switchMap cancels the previous inner Observable when a new value arrives — perfect for search-as-you-type. Pipe operators left-to-right.
import { of, from, interval } from 'rxjs';
import { map, filter, switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
of(1, 2, 3, 4).pipe(
map(x => x * 10), // 10, 20, 30, 40
filter(x => x > 15), // 20, 30, 40
).subscribe(console.log);
// search input with debounce
input$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => http.get(`/api/search?q=${term}`)),
).subscribe(results => this.results = results);Combining Streams
combineLatest emits whenever any source emits (after all have emitted at least once) — great for derived view state. forkJoin waits for all sources to complete, returning the last value of each — use for parallel HTTP requests. merge interleaves emissions. zip pairs by index. race picks the first Observable to emit and ignores the rest.
import { combineLatest, forkJoin, merge, race, zip } from 'rxjs';
// combineLatest: emits when ANY source emits, after all have emitted
combineLatest([user$, settings$]).subscribe(([u, s]) => ...);
// forkJoin: waits for all to complete, emits last values (like Promise.all)
forkJoin({ user: user$, posts: posts$ }).subscribe(r => ...);
// merge: emits values from all sources as they come
merge(clicks$, keys$).subscribe(e => ...);
// zip: pairs emissions by index
zip(range(1, 3), ['a', 'b', 'c']).subscribe(([n, letter]) => ...);
// race: takes the first to emit, ignores the rest
race(primary$, fallback$).subscribe(v => ...);Subjects (Multicasting)
Subjects are both Observables and Observers — call .next() to push values. Subject: no initial value, late subscribers miss earlier emissions. BehaviorSubject: requires an initial value, new subscribers immediately get the current value (use for state). ReplaySubject: buffers last N values for new subscribers (use for replaying events).
import { Subject, BehaviorSubject, ReplaySubject } from 'rxjs';
// Subject: emits to current subscribers only
const click$ = new Subject<MouseEvent>();
click$.subscribe(x => console.log('A:', x));
click$.next({} as MouseEvent); // A: logs it
// BehaviorSubject: keeps last value, new subs get it immediately
const count$ = new BehaviorSubject(0);
count$.subscribe(v => console.log(v)); // logs 0 immediately
count$.next(1); // logs 1 to subscribers
// ReplaySubject: replays last N values to new subscribers
const recent$ = new ReplaySubject<string>(2);
recent$.next('a'); recent$.next('b'); recent$.next('c');
recent$.subscribe(v => console.log(v)); // logs 'b', then 'c'takeUntil & Cleanup
takeUntil(unsubscribe$) auto-completes a subscription when the notifier emits — the standard pattern for avoiding leaks in components. takeUntilDestroyed() (v16+) uses Angular's DestroyRef to auto-unsubscribe without OnDestroy boilerplate. It must be called in an injection context (constructor or field initializer) or accept a DestroyRef explicitly.
import { Subject, takeUntil, takeUntilDestroyed } from 'rxjs';
export class DemoComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.svc.stream$.pipe(
takeUntil(this.destroy$),
).subscribe(data => this.data = data);
}
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
}
// v16+: takeUntilDestroyed (uses DestroyRef, no boilerplate)
export class DemoComponent {
data: any;
constructor() {
inject(SomeService).stream$.pipe(
takeUntilDestroyed(),
).subscribe(d => this.data = d);
}
}switchMap vs mergeMap vs concatMap
These flattening operators differ in how they handle overlapping inner Observables. switchMap cancels (search-as-you-type). mergeMap parallels (load many). concatMap queues (preserve order). exhaustMap drops (prevent double-submits). Choosing the wrong one causes race conditions — match the operator to the use case's cancellation semantics.
import { from, interval } from 'rxjs';
import { switchMap, mergeMap, concatMap, exhaustMap } from 'rxjs/operators';
// switchMap: cancels the previous inner stream on new value
input$.pipe(switchMap(term => search(term)));
// Best for: typeahead search (discard stale results)
// mergeMap: runs all inner streams in parallel
ids$.pipe(mergeMap(id => fetchDetail(id)));
// Best for: parallel requests, order doesn't matter
// concatMap: queues inner streams, runs one at a time
queue$.pipe(concatMap(task => runTask(task)));
// Best for: ordered sequential operations
// exhaustMap: ignores new values while inner stream is active
save$.pipe(exhaustMap(() => saveForm()));
// Best for: prevent double-submit (ignore clicks while saving)Lifecycle Hooks
Hook Order Overview
Hooks fire in a strict order. ngOnChanges fires on @Input changes (and initially). ngOnInit fires once after the first ngOnChanges. ngDoCheck runs every change detection cycle. The After* hooks signal that content/view are ready for DOM queries. ngOnDestroy is the cleanup spot — unsubscribe, release resources, cancel timers.
export class LifecycleComponent implements
OnChanges, OnInit, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
ngOnChanges() { console.log('1. input changes'); }
ngOnInit() { console.log('2. component init (once)'); }
ngDoCheck() { console.log('3. custom change detection'); }
ngAfterContentInit() { console.log('4. projected content ready'); }
ngAfterContentChecked() { console.log('5. projected content checked'); }
ngAfterViewInit() { console.log('6. view (template) ready'); }
ngAfterViewChecked() { console.log('7. view checked'); }
ngOnDestroy() { console.log('8. cleanup before destroy'); }
}ngOnInit & ngOnDestroy
ngOnInit runs once after inputs are set — the right place for expensive init (fetching, subscriptions) since constructor should stay cheap. ngOnDestroy is the only hook called on component destruction — never skip cleanup here or you'll leak subscriptions, timers, and event listeners. Constructor is for DI only; use ngOnInit for logic.
export class ChartComponent implements OnInit, OnDestroy {
private timer?: ReturnType<typeof setInterval>;
ngOnInit() {
// good for: data fetching, initialization, subscriptions
this.timer = setInterval(() => this.refresh(), 5000);
this.loadInitialData();
}
ngOnDestroy() {
// cleanup: clear timers, unsubscribe, release resources
if (this.timer) clearInterval(this.timer);
// also: this.sub.unsubscribe();
}
private refresh() { /* ... */ }
private loadInitialData() { /* ... */ }
}ngOnChanges (SimpleChange)
ngOnChanges fires whenever an @Input reference changes, with a SimpleChanges map: each entry has previousValue, currentValue, firstChange, and isFirstChange(). It does NOT fire when you mutate an object's properties — only when the reference changes. For deep changes, use ngOnChanges with a setter or ngDoCheck with IterableDiffers.
export class PaginationComponent implements OnChanges {
@Input() page = 1;
@Input() total = 0;
ngOnChanges(changes: SimpleChanges) {
if (changes['page'] && !changes['page'].firstChange) {
// react only when page actually changed (not on init)
console.log('page:', changes['page'].previousValue, '->', changes['page'].currentValue);
this.fetchPage(this.page);
}
}
private fetchPage(p: number) { /* ... */ }
}ngAfterViewInit & ViewChild
ViewChild/ContentChild queries resolve AFTER their respective init hooks. ngAfterViewInit for elements in the component's own template; ngAfterContentInit for projected content via <ng-content>. Accessing these refs in ngOnInit returns undefined. The ! non-null assertion tells the compiler it's set after init — but only safely usable in After* hooks.
export class CanvasComponent implements AfterViewInit {
@ViewChild('canvas') canvasRef!: ElementRef<HTMLCanvasElement>;
// ❌ canvasRef is undefined in ngOnInit
ngOnInit() { /* this.canvasRef is undefined here */ }
// ✅ available after view init
ngAfterViewInit() {
const canvas = this.canvasRef.nativeElement;
const ctx = canvas.getContext('2d');
ctx?.fillText('Hello', 10, 50);
}
}
// template: <canvas #canvas></canvas>ngDoCheck (Custom Detection)
ngDoCheck runs every change detection cycle — keep it light. Use IterableDiffers/KeyValueDiffers to detect mutations to arrays/objects that ngOnChanges misses (since references don't change). This is the escape hatch for tracking internal mutations. Avoid expensive logic here; it runs frequently and can cause performance issues.
export class ListComponent implements DoCheck {
@Input() items: Item[] = [];
private differ: IterableDiffer<Item>;
constructor(differs: IterableDiffers) {
this.differ = differs.find(items).create<Item>((i) => i.id);
}
ngDoCheck() {
const changes = this.differ.diff(this.items);
if (changes) {
changes.forEachAddedItem(r => console.log('Added:', r.item));
changes.forEachRemovedItem(r => console.log('Removed:', r.item));
}
}
}Content vs View Hooks
Content hooks (AfterContentInit/Checked) fire for projected content via <ng-content>. View hooks (AfterViewInit/Checked) fire for the component's own template. Content hooks fire BEFORE view hooks because content is initialized before the view completes. @ContentChild queries projected elements; @ViewChild queries own template elements.
export class CardComponent implements
AfterContentInit, AfterViewInit, AfterContentChecked, AfterViewChecked {
@ContentChild('header') headerTpl?: TemplateRef<unknown>;
@ViewChild('body') bodyEl?: ElementRef<HTMLElement>;
// projected content (<ng-content>) is ready
ngAfterContentInit() {
console.log('content ready:', this.headerTpl);
}
// own template view is ready
ngAfterViewInit() {
console.log('view ready:', this.bodyEl?.nativeElement);
}
// ContentChecked/ViewChecked run every CD cycle after init — beware perf
}Content Projection
Basic ng-content
<ng-content> is a placeholder where parent-provided content is projected. Think of it as Angular's <slot>. Without select, all projected content goes here. Projected content is compiled in the parent's context — bindings like {{ }} resolve against the parent component, not CardComponent.
// card.component.ts
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content></ng-content>
</div>
`,
styles: ['.card { border: 1px solid #ccc; padding: 16px; }'],
})
export class CardComponent {}
<!-- usage: anything inside projects into <ng-content> -->
<app-card>
<h3>Title</h3>
<p>Body content here.</p>
</app-card>Multi-slot Projection
select attribute on ng-content targets specific projected content: by element ('header'), attribute ('[header]'), class ('.active'), or combinations ('div.foo[bar]'). Unmatched content falls into the unselect'd ng-content. Projected content keeps the parent's styles and context — only its location in the DOM changes.
@Component({
selector: 'app-layout',
template: `
<header><ng-content select="[header]"></ng-content></header>
<main><ng-content></ng-content></main>
<footer><ng-content select="footer"></ng-content></footer>
`,
})
export class LayoutComponent {}
<!-- usage: select matches attributes, elements, classes -->
<app-layout>
<nav header>Top nav</nav>
<p>Main body</p>
<footer>Bottom</footer>
</app-layout>Conditional Projection (ng-template)
For conditional or repeated projection, ng-template with ngTemplateOutlet gives full control. ContentChildren queries projected directives. ngTemplateOutlet renders a TemplateRef at a chosen location — useful for tabs, accordions, or conditional slots. This pattern decouples content declaration from where/when it renders.
@Component({
selector: 'app-tabs',
template: `
<div class="tabs">
@for (t of tabs; track t) {
<button (click)="active = t">{{ t.title }}</button>
}
</div>
@for (t of tabs; track t) {
@if (t === active) {
<ng-container [ngTemplateOutlet]="t.template" />
}
}
`,
})
export class TabsComponent {
@ContentChildren(TabDirective) tabs!: QueryList<TabDirective>;
active: any;
}ContentChild & ContentChildren
@ContentChild / @ContentChildren query projected content (vs ViewChild for own template). ContentChildren returns a QueryList that updates when content changes. Resolve in ngAfterContentInit. The { descendants: true } option (default) walks all projected content. Use { read: ... } to query for a specific type (ElementRef, TemplateRef, etc.).
@Directive({ selector: 'tab' })
export class TabDirective {
@Input() title = '';
@Input() active = false;
@ContentChild(TemplateRef) template!: TemplateRef<unknown>;
}
@Component({
selector: 'app-tabset',
template: `
@for (tab of tabs; track tab) {
<button (click)="select(tab)">{{ tab.title }}</button>
}
<ng-container *ngTemplateOutlet="activeTab?.template" />
`,
})
export class TabsetComponent implements AfterContentInit {
@ContentChildren(TabDirective) tabs!: QueryList<TabDirective>;
activeTab?: TabDirective;
ngAfterContentInit() {
this.activeTab = this.tabs.find(t => t.active) ?? this.tabs.first;
}
}ngTemplateOutlet with Context
ngTemplateOutlet renders a TemplateRef with an optional context. let-x binds to context.$implicit (the default value); let-y='key' binds to context.key. This is how *ngFor and ngTemplateOutlet pass loop variables. Use this pattern for reusable list/grid/item templates that parents can customize while the component controls the iteration logic.
@Component({
selector: 'app-list',
template: `
@for (item of items; track item.id) {
<ng-container *ngTemplateOutlet="itemTpl; context: { $implicit: item, i: $index }" />
}
<ng-template #itemTpl let-item let-i="i">
<li>{{ i }}: {{ item.name }}</li>
</ng-template>
`,
})
export class ListComponent {
items = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
}ng-content select Patterns
select accepts CSS-like selectors: classes (.x), attributes ([x]), elements (header), or comma-separated lists ('a, b'). Content matching no selector falls into the unselect'd ng-content. If no fallback exists, unmatched content is discarded. Selectors match against the projected elements' own attributes, not their internal structure.
@Component({
selector: 'app-modal',
template: `
<div class="modal">
<ng-content select=".modal-header"></ng-content>
<ng-content select="[body]"></ng-content>
<ng-content select="app-modal-footer, footer"></ng-content>
<!-- fallback for unmatched content -->
<ng-content></ng-content>
</div>
`,
})
<!-- matches by class, attribute, element, or selector list -->
<app-modal>
<div class="modal-header">Title</div>
<section body>Body text</section>
<app-modal-footer>OK</app-modal-footer>
<p>This falls into the unselect'd slot.</p>
</app-modal>Route Guards
CanActivate (Functional)
Functional guards (v14.2+) return true/false/UrlTree or an Observable/Promise of them. Returning a UrlTree redirects to that route. inject() works because guards run in an injection context. CanActivate prevents navigation to a route. Register guards per-route in the canActivate array. They run on each navigation to that route.
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn) return true;
// redirect to login, preserving the attempted URL
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};
// register in routes
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }CanActivateChild
CanActivateChild runs before each child route under the parent — cleaner than repeating canActivate on every child. Use it for shared protection of a feature area. Returns the same value types as CanActivate. For protecting the parent itself too, also add canActivate on the parent route.
export const adminGuard: CanActivateChildFn = (route, state) => {
const auth = inject(AuthService);
if (!auth.isAdmin) {
return inject(Router).parseUrl('/forbidden');
}
return true;
};
// applies to all children of this route
{
path: 'admin',
canActivateChild: [adminGuard],
children: [
{ path: 'users', component: AdminUsersComponent },
{ path: 'settings', component: AdminSettingsComponent },
],
}CanDeactivate (Guard Unsaved Changes)
CanDeactivate runs before leaving a route — perfect for warning about unsaved form changes. The guard receives the component instance, so it calls a method on it. Return false to cancel navigation, true to proceed. For async (modal confirmation), return an Observable or Promise. The component must implement a contract the guard expects.
import { CanDeactivateFn } from '@angular/router';
import { inject } from '@angular/core';
export interface CanComponentDeactivate {
canDeactivate: () => boolean | Observable<boolean>;
}
export const unsavedGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
return component?.canDeactivate() ?? true;
};
// component
export class EditComponent implements CanComponentDeactivate {
canDeactivate() {
if (this.form.dirty) return confirm('Discard changes?');
return true;
}
}
// route: { path: 'edit/:id', canDeactivate: [unsavedGuard] }CanMatch (Conditional Loading)
CanMatch (v14.1+) runs before a lazy route's chunk is even downloaded — more efficient than CanActivate for paid-feature gating. If it returns false, the route is skipped entirely (other matching routes get a chance). CanActivate would load the chunk first, then deny. Use CanMatch to prevent downloading code the user can't access.