Angular Cheatsheet v20+
This cheatsheet covers Angular v20+ features including standalone components, signals, new control flow, and modern patterns.
Table of Contents
- #Components & Standalone Architecture
- #Signals & Reactivity
- #Component Communication
- #Template Syntax & Control Flow
- #Directives
- #Pipes
- #Forms
- #Routing & Navigation
- #HTTP & Data
- #Dependency Injection
- #Lifecycle Hooks
- #Change Detection
- #Content Projection
- #Styling & ViewEncapsulation
- #Testing
- #Animations
- #Advanced Features
Components & Standalone Architecture
Creating a Standalone Component
import { Component } from '@angular/core';
@Component({
selector: 'app-user-profile',
standalone: true, // v19+ defaults to true
imports: [CommonModule, OtherComponents], // Import dependencies
template: `
<h1>{{ title }}</h1>
<p>{{ description }}</p>
`,
styles: [`
h1 { color: blue; }
`],
// or templateUrl: './user-profile.component.html',
// or styleUrl: './user-profile.component.css',
})
export class UserProfile {
title = 'User Profile';
description = 'Manage your profile';
}
Component Metadata Options
@Component({
selector: 'app-card', // CSS selector: element, [attribute], .class
standalone: true, // v19+ default, true = no NgModule needed
imports: [CommonModule], // Dependencies for standalone components
exports: [SubComponent], // Export to parent components
providers: [MyService], // Component-level providers
viewProviders: [MyService], // View-level providers (not available to content children)
template: '...', // Inline template
templateUrl: '...', // External template
styles: ['...'], // Inline styles
styleUrl: '...', // External styles
styleUrls: ['...'], // Multiple external styles
encapsulation: ViewEncapsulation.Emulated, // Style scoping
animations: [fadeIn], // Animations
host: { // Host bindings/listeners
'[class.active]': 'isActive',
'(click)': 'onClick()',
'role': ' 'button'
},
changeDetection: ChangeDetectionStrategy.OnPush, // Performance optimization
schemas: [NO_ERRORS_SCHEMA], // Allow unknown elements/properties
})
export class MyComponent {}
- Use element selectors for components:
app-user-card - Use attribute selectors for directives:
[appHighlight] - Use prefix to avoid conflicts (e.g., your app name)
Using a Component
// Import and use in parent component
import { UserProfile } from './user-profile.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [UserProfile], // Import to use in template
template: `<app-user-profile />`,
})
export class AppComponent {}
Signals & Reactivity
Signals are the recommended way to manage reactive state in Angular. They provide fine-grained reactivity and better performance than traditional change detection.
Writable Signals
import { signal } from '@angular/core';
@Component({ ... })
export class Counter {
// Create a writable signal with initial value
count = signal(0);
name = signal('Angular');
increment() {
// Update with a new value
this.count.set(1);
// Update based on previous value
this.count.update(value => value + 1);
// Mutate for objects/arrays
this.user.update(user => ({ ...user, name: 'Updated' }));
}
}
Computed Signals
import { signal, computed } from '@angular/core';
@Component({ ... })
export class Calculator {
count = signal(0);
// Computed signals derive from other signals
doubleCount = computed(() => this.count() * 2);
// Computed signals are read-only and memoized
isEven = computed(() => this.count() % 2 === 0);
// Dynamic dependencies
showCount = signal(true);
displayValue = computed(() => {
if (this.showCount()) {
return `Count: ${this.count()}`;
}
return 'Hidden';
});
}
Effects
import { signal, effect } from '@angular/core';
@Component({ ... })
export class DataLogger {
count = signal(0);
constructor() {
// Effects run when signals change
effect(() => {
console.log('Count changed:', this.count());
// Perform side effects: logging, persistence, etc.
});
}
}
Effects should be used sparingly. Prefer computed signals for derived state. Use effects only for:
- Logging/debugging
- Synchronizing state outside Angular (localStorage, etc.)
- Custom DOM manipulation
Avoid effects for data flow or state transformations.
Reading Signals
// In component class
const currentValue = count(); // Call signal to read value
// In template (automatic tracking)
@Component({
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
`
})
Signal Utilities
import { signal, computed, effect, untracked, isSignal } from '@angular/core';
// Check if something is a signal
if (isSignal(myValue)) {
console.log('This is a signal');
}
// Read signal without tracking dependency
effect(() => {
const value = untracked(someSignal); // Won't trigger effect when changed
console.log('One-time read:', value);
});
// Writable to readonly conversion
class CounterService {
private _count = signal(0);
readonly count = this._count.asReadonly();
increment() {
this._count.update(n => n + 1);
}
}
Signal Equality Functions
import { signal } from '@angular/core';
// Custom equality check (default is Object.is)
const data = signal(['test'], {
equal: (a, b) => JSON.stringify(a) === JSON.stringify(b)
});
// Useful for arrays/objects where you want deep comparison
Component Communication
Signal Inputs (Recommended v20+)
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<h2>{{ userName() }}</h2>
<p>Age: {{ age() }}</p>
`,
})
export class UserCard {
// Required input
userName = input.required<string>();
// Optional input with default
age = input<number>(0);
// With alias
userId = input<string>('', { alias: 'id' });
// Transform function
displayName = input('', {
transform: (value: string) => value.toUpperCase()
});
}
Usage:
<app-user-card userName="John" age="25" [id]="'123'" />
Signal Outputs
import { Component, output } from '@angular/core';
@Component({
selector: 'app-button',
standalone: true,
template: `<button (click)="onClick()">Click Me</button>`,
})
export class Button {
// Create an output
clicked = output<void>(); // EventEmitter<void> equivalent
// Typed output
valueChanged = output<number>();
onClick() {
this.clicked.emit();
this.valueChanged.emit(42);
}
}
Usage:
<app-button (clicked)="handleClick()" (valueChanged)="onValueChange($event)" />
Model (Two-Way Binding with Signals)
import { Component, model } from '@angular/core';
@Component({
selector: 'app-input',
standalone: true,
template: `<input [(ngModel)]="value" />`,
imports: [FormsModule],
})
export class CustomInput {
// Combines input + output for two-way binding
value = model<string>('');
// With alias: [(value)] becomes [(myValue)]
myValue = model<string>('', { alias: 'value' });
}
Usage:
<app-input [(value)]="parentValue" />
Use model() when you need two-way binding. It's simpler than creating separate input and output.
Decorator-Based Inputs/Outputs (Legacy)
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({ ... })
export class UserCard {
// Input
@Input() userName!: string;
@Input({ required: true }) userId!: string;
@Input() age: number = 0;
// Output
@Output() readonly userClick = new EventEmitter<string>();
onClick() {
this.userClick.emit(this.userName);
}
}
Service-Based Communication
// Shared service with signals
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class SharedState {
count = signal(0);
user = signal<User | null>(null);
increment() {
this.count.update(n => n + 1);
}
}
// Component A (producer)
@Component({ ... })
export class ProducerComponent {
private state = inject(SharedState);
add() {
this.state.increment();
}
}
// Component B (consumer)
@Component({
template: `Count: {{ state.count() }}`
})
export class ConsumerComponent {
state = inject(SharedState);
}
Template Syntax & Control Flow
New Control Flow (v17+)
// Must import CommonModule or import directly
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `
<!-- @if (replaces *ngIf) -->
@if (isVisible) {
<div>Visible content</div>
} @else if (isAlternative) {
<div>Alternative content</div>
} @else {
<div>Fallback content</div>
}
<!-- @for (replaces *ngFor) -->
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
} @empty {
<div>No items found</div>
}
<!-- @switch (replaces ngSwitch) -->
@switch (status) {
@case ('active') {
<span>Active</span>
}
@case ('inactive') {
<span>Inactive</span>
}
@default {
<span>Unknown</span>
}
}
`
})
track with @for for performance. It tells Angular how to identify items, reducing DOM manipulation.@for (item of items; track item.id) { ... }
@for (item of items; track $index) { ... } // Fallback
Property Binding
<!-- Property binding -->
<img [alt]="title" />
<button [disabled]="isDisabled">Click</button>
<div [class.active]="isActive">Content</div>
<!-- Style binding -->
<div [style.color]="textColor">Text</div>
<div [style.font-size.px]="fontSize">Text</div>
<!-- Class binding (multiple) -->
<div [class]="{ active: isActive, disabled: isDisabled }">Content</div>
<!-- Style binding (multiple) -->
<div [style]="{ color: 'red', 'font-size': '14px' }">Content</div>
Event Binding
<!-- Event binding -->
<button (click)="onClick()">Click</button>
<input (input)="onInput($event)" />
<div (mouseenter)="onHover()">Hover me</div>
<!-- Key events -->
<textarea (keydown.control.enter)="submit()"></textarea>
<input (keyup.escape)="clear()" />
<!-- Event modifiers -->
<button (click.stop)="onClick()">Stop propagation</button>
<form (submit.prevent)="onSubmit()">Prevent default</button>
Two-Way Binding
<!-- With NgModel (FormsModule) -->
<input [(ngModel)]="username" />
<!-- Custom two-way binding -->
<app-input [(value)]="parentValue" />
<!-- Expanded syntax -->
<app-input [value]="parentValue" (valueChange)="parentValue = $event" />
Template Reference Variables
<!-- Reference to DOM element -->
<input #emailInput (keyup.enter)="onSubmit(emailInput.value)" />
<!-- Reference to directive -->
<input #ngModel="ngModel" />
<p>Valid: {{ ngModel.valid }}</p>
<!-- Reference to component -->
<app-user #userCard />
<button (click)="userCard.refresh()">Refresh</button>
Safe Navigation Operator
<!-- Prevents null reference errors -->
<p>User: {{ user?.name }}</p>
<p>Nested: {{ user?.address?.city }}</p>
Non-Null Assertion Operator
<!-- Tells TypeScript the value is not null -->
<p>Value: {{ value! }}</p>
Directives
Built-in Structural Directives
<!-- New control flow (recommended) -->
@if (condition) { ... }
@for (item of items; track item.id) { ... }
<!-- Legacy (still supported) -->
<div *ngIf="condition">Content</div>
<div *ngFor="let item of items; let i = index">{{ i }}: {{ item }}</div>
<div *ngIf="condition; else elseBlock">Content</div>
<ng-template #elseBlock>Alternative</ng-template>
<div *ngIf="condition; then thenBlock; else elseBlock"></div>
<ng-template #thenBlock>Then</ng-template>
<ng-template #elseBlock>Else</ng-template>
<!-- *ngSwitch -->
<div [ngSwitch]="status">
<div *ngSwitchCase="'active'">Active</div>
<div *ngSwitchCase="'inactive'">Inactive</div>
<div *ngSwitchDefault>Unknown</div>
</div>
Built-in Attribute Directives
<!-- NgClass -->
<div [ngClass]="{ active: isActive, disabled: isDisabled }">Content</div>
<div [ngClass]="['class1', 'class2']">Content</div>
<!-- NgStyle -->
<div [ngStyle]="{ color: 'red', 'font-size': fontSize + 'px' }">Content</div>
<!-- NgModel (FormsModule) -->
<input [(ngModel)]="username" />
[class]instead of[ngClass][style]instead of[ngStyle]
Custom Attribute Directive
import { Directive, ElementRef, HostListener, input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
// Directive input
appHighlight = input('yellow');
constructor(private el: ElementRef) {}
@HostListener('mouseenter')
onMouseEnter() {
this.highlight(this.appHighlight());
}
@HostListener('mouseleave')
onMouseLeave() {
this.highlight('');
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
Usage:
<p appHighlight>Default highlight</p>
<p appHighlight="red">Red highlight</p>
<p [appHighlight]="userColor">Dynamic highlight</p>
Custom Structural Directive
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appUnless]',
standalone: true
})
export class UnlessDirective {
private hasView = false;
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
}
Usage:
<div *appUnless="condition">Show unless condition is true</div>
Pipes
Built-in Pipes
<!-- Common pipes (from @angular/common) -->
{{ value | currency }}
{{ value | date }}
{{ value | date:'short':'UTC' }}
{{ value | number }}
{{ value | percent }}
{{ value | json }}
{{ value | uppercase }}
{{ value | lowercase }}
{{ value | titlecase }}
{{ value | slice:0:10 }}
<!-- Async pipe (auto-subscribes to Observable/Promise) -->
<div *ngIf="user$ | async as user">{{ user.name }}</div>
{{ user$ | async | json }}
<!-- KeyValuePipe -->
<div *ngFor="let item of object | keyvalue">
Key: {{ item.key }}, Value: {{ item.value }}
</div>
Chaining Pipes
<!-- Pipes execute left to right -->
{{ birthday | date:'fullDate' | uppercase }}
{{ value | number:'1.2-2' | currency:'USD' }}
Custom Pipe
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'kebabCase',
standalone: true
})
export class KebabCasePipe implements PipeTransform {
transform(value: string): string {
return value.toLowerCase().replace(/ /g, '-');
}
}
// Pipe with parameters
@Pipe({
name: 'truncate',
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 20): string {
return value.length > limit
? value.substring(0, limit) + '...'
: value;
}
}
Usage:
<p>{{ 'Hello World' | kebabCase }}</p> <!-- hello-world -->
<p>{{ longText | truncate:50 }}</p>
Pure vs Impure Pipes
// Pure pipe (default) - only re-evaluates when input reference changes
@Pipe({
name: 'purePipe',
pure: true // default
})
export class PurePipe implements PipeTransform {
transform(value: any): any {
// Won't detect changes inside arrays/objects
return value;
}
}
// Impure pipe - re-evaluates on every change detection cycle
@Pipe({
name: 'impurePipe',
pure: false // Use with caution - performance impact!
})
export class ImpurePipe implements PipeTransform {
transform(value: any): any {
// Detects changes inside arrays/objects
return value;
}
}
Avoid impure pipes unless absolutely necessary. They run on every change detection cycle and can significantly impact performance.
Forms
Reactive Forms
import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="username" />
<p *ngIf="form.get('username')?.invalid">Required</p>
<div formGroupName="address">
<input formControlName="city" />
</div>
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
`
})
export class MyForm {
// Using FormBuilder
private fb = inject(FormBuilder);
form = this.fb.group({
username: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
address: this.fb.group({
city: [''],
zip: ['']
})
});
onSubmit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}
Signal-Based Forms (New v17+)
import {
FormControl,
Validators,
inject,
provideForms,
} from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
standalone: true,
imports: [CommonModule],
template: `
<form>
<label>Username</label>
<input [formControl]="usernameControl" />
@if (usernameControl.invalid && usernameControl.touched) {
<small>Username is required</small>
}
<label>Email</label>
<input [formControl]="emailControl" />
<button (click)="reset()">Reset</button>
</form>
`
})
export class SignalFormComponent {
// Create form controls
usernameControl = new FormControl('', {
validators: [Validators.required, Validators.minLength(3)],
nonNullable: true
});
emailControl = new FormControl('', {
validators: [Validators.required, Validators.email],
nonNullable: true
});
// Access signal-based value
username = computed(() => this.usernameControl.value);
reset() {
this.usernameControl.reset('');
}
}
// Configure in app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideForms()] // Enable signal forms
};
Template-Driven Forms
import { FormsModule } from '@angular/forms';
@Component({
standalone: true,
imports: [FormsModule],
template: `
<form #form="ngForm" (ngSubmit)="onSubmit(form)">
<input
name="username"
ngModel
required
minlength="3"
#username="ngModel"
/>
@if (username.invalid && username.touched) {
<small>Invalid</small>
}
<input name="email" ngModel email required />
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
`
})
export class TemplateForm {
onSubmit(form: NgForm) {
console.log(form.value);
}
}
Common Validators
import { Validators, AbstractControl, ValidationErrors } from '@angular/forms';
// Built-in validators
FormControl('', Validators.required);
FormControl('', Validators.email);
FormControl('', Validators.minLength(3));
FormControl('', Validators.maxLength(20));
FormControl('', Validators.pattern(/^[a-zA-Z]+$/));
FormControl('', Validators.min(0));
FormControl('', Validators.max(100));
// Multiple validators
FormControl('', [
Validators.required,
Validators.email,
Validators.minLength(5)
]);
// Custom validator
function ageValidator(control: AbstractControl): ValidationErrors | null {
const age = control.value;
return age < 18 ? { underage: true } : null;
}
// Async validator
function uniqueEmailValidator(
service: UserService
): AsyncValidatorFn {
return (control: AbstractControl) => {
return service.checkEmail(control.value).pipe(
map(isTaken => (isTaken ? { uniqueEmail: true } : null))
);
};
}
Dynamic Forms
@Component({ ... })
export class DynamicForm {
form = new FormGroup({});
addField(name: string) {
this.form.addControl(name, new FormControl(''));
}
removeField(name: string) {
this.form.removeControl(name);
}
setField(name: string, value: any) {
this.form.get(name)?.setValue(value);
}
}
Routing & Navigation
Route Configuration
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{
path: 'home',
loadComponent: () =>
import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'users',
loadComponent: () =>
import('./users/users.component').then(m => m.UsersComponent),
children: [
{
path: ':id',
loadComponent: () =>
import('./user-detail/user-detail.component')
.then(m => m.UserDetailComponent)
}
]
},
{
path: '**',
loadComponent: () =>
import('./not-found/not-found.component')
.then(m => m.NotFoundComponent)
}
];
Router Configuration
// app.config.ts
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)]
};
Router Navigation
import { Router, RouterLink, RouterOutlet } from '@angular/router';
@Component({
standalone: true,
imports: [RouterLink, RouterOutlet],
template: `
<nav>
<a routerLink="/home" routerLinkActive="active">Home</a>
<a [routerLink]="['/users', userId]">User</a>
<a [routerLink]="['/products']"
[queryParams]="{ page: 1 }"
fragment="section">Products</a>
</nav>
<router-outlet></router-outlet>
`
})
export class AppComponent {
private router = inject(Router);
navigateToUser(id: string) {
// Navigate with required parameters
this.router.navigate(['/users', id]);
// Navigate with query parameters
this.router.navigate(['/search'], {
queryParams: { q: 'angular' },
fragment: 'results'
});
}
// Router events
constructor() {
inject(Router).events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
console.log('Navigated to:', event.url);
});
}
}
Route Parameters
@Component({
template: `
<p>User ID: {{ userId }}</p>
<p>Query: {{ searchQuery }}</p>
`
})
export class UserDetailComponent {
// Required route parameter
userId = route.paramMap.pipe(
map(params => params.get('id')!)
);
// Optional query parameter
searchQuery = route.queryParamMap.pipe(
map(params => params.get('q') || '')
);
constructor(private route: ActivatedRoute) {}
}
Route Guards
// canActivate.guard.ts
import { inject } from '@angular/core';
import {
CanActivateFn,
Router,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '@angular/router';
export const authGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
// Redirect to login
return router.parseUrl('/login');
};
// canActivateChild
export const adminGuard: CanActivateChildFn = (
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) => {
const authService = inject(AuthService);
return authService.hasRole('admin');
};
// canDeactivate
export const canDeactivateGuard: CanDeactivateFn<Component> = (
component: Component
) => {
return component.hasUnsavedChanges()
? confirm('Discard changes?')
: true;
};
Usage in routes:
{
path: 'admin',
canActivate: [authGuard, adminGuard],
canDeactivate: [canDeactivateGuard],
loadComponent: () => import('./admin.component')
}
Resolvers
// user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
export const userResolver: ResolveFn<User> = (route, state) => {
const userService = inject(UserService);
const userId = route.paramMap.get('id')!;
return userService.getUser(userId);
};
Usage in routes:
{
path: 'users/:id',
resolve: { user: userResolver },
loadComponent: () => import('./user-detail.component')
}
Access resolved data:
@Component({ ... })
export class UserDetailComponent {
user = inject(ActivatedRoute).snapshot.data['user'] as User;
}
HTTP & Data
HttpClient Setup
// app.config.ts
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([loggingInterceptor]),
withFetch() // Use fetch API instead of XMLHttpRequest
)
]
};
Basic HTTP Operations
import { HttpClient, HttpParams } from '@angular/common/http';
import { inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
private apiUrl = '/api';
// GET request
getUsers() {
return this.http.get<User[]>(`${this.apiUrl}/users`);
}
// GET with params
searchUsers(query: string) {
const params = new HttpParams().set('q', query);
return this.http.get<User[]>(`${this.apiUrl}/search`, { params });
}
// POST request
createUser(user: User) {
return this.http.post<User>(`${this.apiUrl}/users`, user);
}
// PUT request
updateUser(id: string, user: User) {
return this.http.put<User>(`${this.apiUrl}/users/${id}`, user);
}
// PATCH request
partialUpdate(id: string, changes: Partial<User>) {
return this.http.patch<User>(`${this.apiUrl}/users/${id}`, changes);
}
// DELETE request
deleteUser(id: string) {
return this.http.delete(`${this.apiUrl}/users/${id}`);
}
}
HTTP Interceptors
// logging.interceptor.ts
import {
HttpRequest,
HttpHandlerFn,
HttpEvent,
HttpEventType
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
export function loggingInterceptor(
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> {
console.log('Request:', req.url);
return next(req).pipe(
tap(event => {
if (event.type === HttpEventType.Response) {
console.log('Response status:', event.status);
}
})
);
}
// auth.interceptor.ts
export function authInterceptor(
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> {
const authService = inject(AuthService);
const token = authService.getToken();
if (token) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return next(req);
}
Error Handling
import { catchError, throwError } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
getUsers() {
return this.http.get<User[]>('/api/users').pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 404) {
return throwError(() => new Error('Not found'));
}
if (error.status === 500) {
return throwError(() => new Error('Server error'));
}
return throwError(() => new Error('Something went wrong'));
})
);
}
}
Request Options
// With custom headers
this.http.get('/api/data', {
headers: { 'X-Custom-Header': 'value' }
});
// With response type (json, text, blob, arraybuffer)
this.http.get('/api/image', {
responseType: 'blob'
});
// With full response observation
this.http.get<User>('/api/user', {
observe: 'response'
}).subscribe(response => {
console.log('Status:', response.status);
console.log('Headers:', response.headers);
console.log('Body:', response.body);
});
// With timeout
this.http.get('/api/data', {
context: new HttpContext().set(TIMEOUT_TOKEN, 5000)
});
// With progress reporting
this.http.post('/api/upload', data, {
reportProgress: true,
observe: 'events'
}).subscribe(event => {
if (event.type === HttpEventType.UploadProgress) {
console.log('Progress:', event.loaded, '/', event.total);
}
});
Dependency Injection
The inject() Function (Recommended)
import { inject } from '@angular/core';
@Component({ ... })
export class MyComponent {
// Inject in field initializer (recommended)
private http = inject(HttpClient);
private router = inject(Router);
private userService = inject(UserService);
// Inject with options
private config = inject(MyConfig, { optional: true });
// Inject with host/skipSelf
private parentService = inject(ParentService, { skipSelf: true });
}
Constructor Injection
@Component({ ... })
export class MyComponent {
constructor(
private http: HttpClient,
private router: Router,
@Optional() private config?: MyConfig
) {}
}
Providing Services
@Injectable({ providedIn: 'root' })
export class UserService {
// Singleton service
}
Component-Level Providers
@Component({
providers: [
UserService, // New instance for this component tree
{ provide: MyService, useClass: MyServiceImpl },
{ provide: API_URL, useValue: 'https://api.example.com' },
{ provide: Logger, useExisting: ConsoleLogger }
]
})
export class MyComponent {}
Injection Tokens
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('api-url');
// Provide
bootstrapApplication(App, {
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
});
// Inject
const apiUrl = inject(API_URL);
Lightweight Injection Tokens (v17+)
// Create token without class
export const API_URL = new InjectionToken<string>('api-url', {
providedIn: 'root',
factory: () => 'https://default-api.com'
});
// Inject
const apiUrl = inject(API_URL);
Lifecycle Hooks
Component Lifecycle
import {
OnInit,
OnDestroy,
OnChanges,
AfterContentInit,
AfterContentChecked,
AfterViewInit,
AfterViewChecked,
SimpleChanges
} from '@angular/core';
@Component({ ... })
export class MyComponent implements
OnInit,
OnDestroy,
OnChanges,
AfterContentInit,
AfterContentChecked,
AfterViewInit,
AfterViewChecked {
// Input changes
ngOnChanges(changes: SimpleChanges) {
console.log('Inputs changed:', changes);
}
// Initialization
ngOnInit() {
console.log('Component initialized');
}
// Content projection
ngAfterContentInit() {
console.log('Content projected');
}
ngAfterContentChecked() {
console.log('Content checked');
}
// View initialization
ngAfterViewInit() {
console.log('View initialized');
}
ngAfterViewChecked() {
console.log('View checked');
}
// Cleanup
ngOnDestroy() {
console.log('Component destroyed');
// Clean up subscriptions, timers, etc.
}
}
- ngOnChanges (if inputs change)
- ngOnInit
- ngDoCheck (custom change detection)
- ngAfterContentInit
- ngAfterContentChecked
- ngAfterViewInit
- ngAfterViewChecked
- ngOnDestroy (when destroyed)
Using Signals Instead of OnChanges
// Old way
export class MyComponent implements OnChanges {
@Input() userId!: string;
ngOnChanges(changes: SimpleChanges) {
if (changes['userId']) {
this.loadUser();
}
}
}
// New way with signals
export class MyComponent {
userId = input.required<string>();
userService = inject(UserService);
user = toSignal(
this.userId.pipe(switchMap(id => this.userService.getUser(id)))
);
}
Change Detection
OnPush Strategy
import { ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
constructor(private cdr: ChangeDetectorRef) {}
// Manually trigger change detection
refresh() {
this.cdr.markForCheck();
}
// Detach from change detection tree
detach() {
this.cdr.detach();
}
// Re-attach
reattach() {
this.cdr.reattach();
}
}
Components using signals automatically work efficiently with OnPush. No manual change detection needed when using signals!
Zoneless (Experimental)
// app.config.ts
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection()
]
};
Async Pipe and Change Detection
@Component({
template: `
@if (user$ | async; as user) {
{{ user.name }}
}
`
})
export class MyComponent {
user$ = inject(UserService).getUser();
}
Content Projection
Single Slot Projection
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content></ng-content>
</div>
`
})
export class Card {}
Usage:
<app-card>
<p>Projected content</p>
</app-card>
Multi-Slot Projection
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content select="app-card-header"></ng-content>
<div class="body">
<ng-content select="app-card-body"></ng-content>
</div>
<ng-content select="app-card-footer"></ng-content>
</div>
`
})
export class Card {}
Usage:
<app-card>
<app-card-header>Title</app-card-header>
<app-card-body>Content</app-card-body>
<app-card-footer>Footer</app-card-footer>
</app-card>
Conditional Projection with ngProjectAs
<!-- Project h3 as card-header -->
<app-card>
<h3 ngProjectAs="app-card-header">Title</h3>
<p>Body content</p>
</app-card>
Styling & ViewEncapsulation
View Encapsulation Modes
import { ViewEncapsulation } from '@angular/core';
@Component({
encapsulation: ViewEncapsulation.Emulated // Default (scoped styles)
// encapsulation: ViewEncapsulation.None // Global styles
// encapsulation: ViewEncapsulation.ShadowDom // Shadow DOM
})
export class MyComponent {}
Dynamic Style Binding
<!-- Class binding -->
<div [class.active]="isActive">Content</div>
<div [class]="{ active: isActive, disabled: isDisabled }">Content</div>
<!-- Style binding -->
<div [style.color]="textColor">Text</div>
<div [style.font-size.px]="fontSize">Text</div>
<div [style]="{ color: 'red', fontSize: '14px' }">Text</div>
<!-- NgClass and NgStyle (legacy) -->
<div [ngClass]="['class1', 'class2']">Content</div>
<div [ngStyle]="{ color: 'red' }">Content</div>
Host Binding
@Component({
selector: 'app-card',
template: `<div>Content</div>`,
host: {
'[class.active]': 'isActive',
'[style.background]': 'backgroundColor',
'(mouseenter)': 'onMouseEnter()',
'role': 'article'
}
})
export class Card {
isActive = false;
backgroundColor = 'white';
onMouseEnter() {
this.isActive = true;
}
}
Or with @HostBinding:
@Component({ ... })
export class Card {
@HostBinding('class.active') isActive = false;
@HostBinding('style.background') backgroundColor = 'white';
@HostListener('mouseenter')
onMouseEnter() {
this.isActive = true;
}
}
Testing
Component Testing
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges(); // Trigger change detection
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display title', () => {
const titleElement: HTMLElement =
fixture.nativeElement.querySelector('h1');
expect(titleElement.textContent).toContain('My Component');
});
it('should increment count on button click', () => {
const button = fixture.debugElement.query(By.css('button'));
button.triggerEventHandler('click');
fixture.detectChanges();
expect(component.count()).toBe(1);
});
});
Testing with Inputs
it('should display user name', () => {
component.userName.set('John');
fixture.detectChanges();
const nameElement: HTMLElement =
fixture.nativeElement.querySelector('[data-test="name"]');
expect(nameElement.textContent).toBe('John');
});
Testing with Services
it('should load users', () => {
const userService = TestBed.inject(UserService);
spyOn(userService, 'getUsers').and.returnValue(of([
{ id: 1, name: 'John' }
]));
component.ngOnInit();
fixture.detectChanges();
expect(userService.getUsers).toHaveBeenCalled();
expect(component.users().length).toBe(1);
});
Animations
The trigger/state/transition animation system is deprecated in v20.2. Use CSS animations or the new animate.enter/leave API instead.
CSS-Based Animations (Recommended)
/* styles.css */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@Component({
styles: [`
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`]
})
export class MyComponent {}
New Animation API (v20+)
import { animate } from '@angular/animations';
@Component({
template: `
<div @animate.enter="'fade-in'" @animate.leave="'fade-out'">
Content
</div>
`,
animations: [
animate(`
:enter {
animation: fadeIn 0.3s ease-in;
}
:leave {
animation: fadeOut 0.3s ease-out;
}
`)
]
})
export class MyComponent {}
Advanced Features
Server-Side Rendering (SSR)
ng add @angular/ssr
// app.config.server.ts
import { provideServerRendering } from '@angular/ssr';
export const appConfig: ApplicationConfig = {
providers: [
provideServerRendering()
]
};
Custom Elements (Web Components)
import { createCustomElement } from '@angular/elements';
@Component({ ... })
export class MyComponent {}
// Convert to custom element
const customElement = createCustomElement(MyComponent, {
injector: app.injector
});
customElements.define('my-component', customElement);
Lazy Loading
// Routes
{
path: 'feature',
loadComponent: () =>
import('./feature/feature.component')
.then(m => m.FeatureComponent)
}
// Standalone component lazy loading
@Component({
imports: [
import('./heavy/HeavyComponent').then(m => m.HeavyComponent)
]
})
Quick Reference Tables
Binding Syntax
| Type | Syntax | Example |
|---|---|---|
| Property | [property] | [src]="imageUrl" |
| Event | (event) | (click)="onClick()" |
| Two-way | [(property)] | [(ngModel)]="value" |
| Attribute | [attr.attribute] | [attr.aria-label]="label" |
| Class | [class.name] | [class.active]="isActive" |
| Style | [style.property] |
Control Flow Comparison
| Feature | New Syntax | Old Syntax |
|---|---|---|
| If | @if (condition) {} | *ngIf="condition" |
| For | @for (item of items; track id) {} | *ngFor="let item of items" |
| Switch | @switch (val) { @case 'a' {} } | [ngSwitch] |
Form Approaches
| Approach | Use Case | Complexity |
|---|---|---|
| Template-Driven | Simple forms | Low |
| Reactive | Complex forms | Medium |
| Signal Forms | Modern, reactive | Medium |
Best Practices
- Use standalone components
- Use signals for state management
- Use new control flow (@if, @for)
- Use OnPush change detection
- Use inject() instead of constructor injection
- Use functional guards and interceptors
- Lazy load routes and components
- Use async pipe with observables
- Write tests for components and services
- Don't use NgModules (unless needed)
- Don't use *ngIf/*ngFor (use @if/@for)
- Don't use NgClass/NgStyle (use [class]/[style])
- Don't use effects for derived state (use computed)
- Don't use any (use proper types)
- Don't forget to unsubscribe from observables (or use async pipe)
- Don't use impure pipes (performance impact)
- Don't put complex logic in templates
Additional Resources
Official Documentation
- Angular.dev - Official Angular documentation
- Angular Blog - Latest updates and best practices
Related Notes
- RxJS Operators - Common RxJS patterns
- TypeScript - TypeScript features
- Web Development - General web development patterns
Metadata
Last Updated: 2026-03-15
Angular Version: v20+
Author: Generated using Angular MCP Server
This cheatsheet was generated using the Angular CLI MCP Server with official Angular v20 documentation.