Angular is a full-featured TypeScript framework by Google used to build large-scale web applications. This cheat sheet covers Angular 17+ — components, services, DI, RxJS, signals, routing, forms, and HTTP — with copy-ready examples.
Quick reference
| Pattern | What it does |
|---|---|
@Component({...}) |
Declares a component |
@Injectable({providedIn:'root'}) |
Singleton service |
signal(0) |
Reactive primitive (Angular 17+) |
computed(() => x() * 2) |
Derived signal |
effect(() => console.log(x())) |
Side effect on signal change |
input() / output() |
New signal-based I/O |
@Input() name = '' |
Classic property binding |
@Output() clicked = new EventEmitter() |
Classic event emitter |
ngOnInit() |
Lifecycle hook — runs after init |
*ngIf="cond" / @if (cond) |
Conditional rendering |
*ngFor="let x of list" / @for |
List rendering |
[(ngModel)]="value" |
Two-way data binding |
[class.active]="bool" |
Conditional class |
(click)="handler()" |
Event binding |
{{ value | currency }} |
Pipe formatting |
HttpClient.get<T>(url) |
HTTP GET request |
Observable.pipe(map, filter) |
RxJS stream transform |
Subject / BehaviorSubject |
Manual event stream |
RouterLink, router.navigate() |
Client-side navigation |
ActivatedRoute |
Current route params/query |
ReactiveFormsModule |
Form with validators |
FormBuilder.group({}) |
Create form group |
AsyncPipe (| async) |
Unwrap Observable in template |
ChangeDetectionStrategy.OnPush |
Performance optimisation |
ng generate component foo |
CLI scaffold component |
Components
Basic component
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-hello',
standalone: true, // Angular 17 — no NgModule needed
imports: [CommonModule],
template: `
<h1>Hello, {{ name }}!</h1>
<button (click)="greet()">Greet</button>
`,
})
export class HelloComponent {
name = 'World';
greet() {
alert(`Hello, ${this.name}`);
}
}
Signal-based component (Angular 17+)
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">−</button>
`,
})
export class CounterComponent {
count = signal(0);
double = computed(() => this.count() * 2);
increment() { this.count.update(n => n + 1); }
decrement() { this.count.update(n => n - 1); }
}
Inputs and outputs
// Classic (still works)
@Component({ selector: 'app-card', standalone: true, template: `<div>{{ title }}</div>` })
export class CardComponent {
@Input() title = '';
@Input({ required: true }) id!: number;
@Output() selected = new EventEmitter<number>();
select() {
this.selected.emit(this.id);
}
}
// New signal-based (Angular 17.1+)
import { input, output } from '@angular/core';
export class CardComponent {
title = input<string>('');
id = input.required<number>();
selected = output<number>();
select() {
this.selected.emit(this.id());
}
}
Usage in parent:
<app-card [title]="'My Card'" [id]="42" (selected)="onSelect($event)" />
Lifecycle hooks
import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ElementRef } from '@angular/core';
@Component({ selector: 'app-life', standalone: true, template: `<div #box></div>` })
export class LifeComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('box') box!: ElementRef;
ngOnInit() {
// Inputs are set — safe to fetch data
}
ngAfterViewInit() {
// DOM is rendered — safe to read element dimensions
console.log(this.box.nativeElement.offsetWidth);
}
ngOnDestroy() {
// Unsubscribe, cancel timers
}
}
| Hook | When it runs |
|---|---|
ngOnChanges |
Before ngOnInit, when @Input value changes |
ngOnInit |
Once after first ngOnChanges |
ngDoCheck |
Every change detection cycle |
ngAfterViewInit |
After component view renders |
ngAfterViewChecked |
After every view check |
ngOnDestroy |
Before component is destroyed |
Templates
Control flow (Angular 17 built-in syntax)
<!-- @if / @else if / @else -->
@if (user.isAdmin) {
<admin-panel />
} @else if (user.isMod) {
<mod-panel />
} @else {
<user-panel />
}
<!-- @for with track -->
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
} @empty {
<li>No items</li>
}
<!-- @switch -->
@switch (status) {
@case ('active') { <span class="green">Active</span> }
@case ('paused') { <span class="yellow">Paused</span> }
@default { <span class="grey">Unknown</span> }
}
Legacy structural directives
<div *ngIf="isVisible; else hidden">Visible</div>
<ng-template #hidden><p>Hidden</p></ng-template>
<li *ngFor="let item of items; let i = index; trackBy: trackById">
{{ i }}: {{ item.name }}
</li>
trackById(index: number, item: { id: number }) {
return item.id;
}
Bindings quick reference
<!-- Property binding -->
<img [src]="imageUrl" [alt]="imageAlt" />
<!-- Event binding -->
<button (click)="save($event)">Save</button>
<input (keyup.enter)="submit()" />
<!-- Two-way binding (requires FormsModule) -->
<input [(ngModel)]="searchTerm" />
<!-- Class and style bindings -->
<div [class.active]="isActive" [class]="dynamicClasses"></div>
<div [style.color]="color" [style]="{ fontSize: '14px' }"></div>
<!-- Template reference variable -->
<input #emailInput type="email" />
<button (click)="log(emailInput.value)">Log</button>
Pipes
<!-- Built-in pipes -->
{{ price | currency:'EUR':'symbol':'1.2-2' }}
{{ date | date:'dd/MM/yyyy HH:mm' }}
{{ name | uppercase }}
{{ name | lowercase }}
{{ name | titlecase }}
{{ bigNumber | number:'1.0-0' }}
{{ jsonValue | json }}
{{ obs$ | async }} <!-- unwraps Observable/Promise -->
{{ items | slice:0:5 }}
{{ str | slice:-3 }}
Custom pipe
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'truncate', standalone: true, pure: true })
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 50): string {
return value.length > limit ? value.slice(0, limit) + '…' : value;
}
}
Usage: {{ longText | truncate:100 }}
Services and Dependency Injection
// service
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' }) // singleton across the app
export class UserService {
private http = inject(HttpClient); // new inject() API (Angular 14+)
getUsers() {
return this.http.get<User[]>('/api/users');
}
getUserById(id: number) {
return this.http.get<User>(`/api/users/${id}`);
}
}
// consuming service in a component
@Component({...})
export class UserListComponent {
private userService = inject(UserService);
users = signal<User[]>([]);
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users.set(users);
});
}
}
Providing at different levels
// App-wide (default)
@Injectable({ providedIn: 'root' })
// Component-level (new instance per component)
@Component({ providers: [SomeService] })
// Route-level (new instance per route)
{ path: 'orders', component: OrdersComponent, providers: [OrdersService] }
HTTP Client
// main.ts — provide HttpClient
import { provideHttpClient, withInterceptors } from '@angular/common/http';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([authInterceptor])),
]
});
// Service methods
export class ApiService {
private http = inject(HttpClient);
private base = '/api';
getAll<T>(path: string) {
return this.http.get<T[]>(`${this.base}${path}`);
}
getOne<T>(path: string, id: number) {
return this.http.get<T>(`${this.base}${path}/${id}`);
}
post<T>(path: string, body: unknown) {
return this.http.post<T>(`${this.base}${path}`, body);
}
put<T>(path: string, id: number, body: unknown) {
return this.http.put<T>(`${this.base}${path}/${id}`, body);
}
delete(path: string, id: number) {
return this.http.delete(`${this.base}${path}/${id}`);
}
}
Functional HTTP interceptor (Angular 15+)
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.token();
if (token) {
req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
}
return next(req);
};
RxJS with Angular
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, BehaviorSubject, combineLatest } from 'rxjs';
import { takeUntil, debounceTime, switchMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({ selector: 'app-search', standalone: true, template: `...` })
export class SearchComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
searchTerm$ = new BehaviorSubject('');
results$ = this.searchTerm$.pipe(
debounceTime(300),
switchMap(term =>
this.api.search(term).pipe(
catchError(() => of([]))
)
)
);
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Common RxJS operators
| Operator | What it does |
|---|---|
map(fn) |
Transform each value |
filter(fn) |
Only pass values that match |
switchMap(fn) |
Switch to new inner observable (cancels previous) |
mergeMap(fn) |
Merge inner observables (concurrent) |
concatMap(fn) |
Queue inner observables |
exhaustMap(fn) |
Ignore new while previous runs |
debounceTime(ms) |
Emit after silence period |
throttleTime(ms) |
Emit once per time window |
distinctUntilChanged() |
Skip duplicate consecutive values |
takeUntil(obs$) |
Complete when trigger fires |
take(n) |
Complete after n values |
catchError(fn) |
Handle errors, return fallback |
retry(n) |
Retry on error |
shareReplay(1) |
Multicast, replay last value |
combineLatest([a$, b$]) |
Emit when any source emits (needs all to emit once) |
forkJoin([a$, b$]) |
Wait for all to complete |
startWith(val) |
Emit initial value immediately |
scan(fn, seed) |
Accumulate like reduce |
Routing
Setup
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent },
{ path: 'users/:id', component: UserDetailComponent },
{
path: 'admin',
canActivate: [authGuard], // functional guard
children: [
{ path: '', component: AdminDashboard },
{ path: 'settings', component: AdminSettings },
]
},
// Lazy-load feature module
{
path: 'shop',
loadChildren: () => import('./shop/shop.routes').then(m => m.shopRoutes),
},
// Lazy-load standalone component
{
path: 'profile',
loadComponent: () => import('./profile.component').then(m => m.ProfileComponent),
},
{ path: '**', component: NotFoundComponent },
];
// main.ts
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes, withComponentInputBinding())]
});
Navigation
<!-- Template -->
<a routerLink="/users">Users</a>
<a [routerLink]="['/users', user.id]">User detail</a>
<a routerLink="/about" routerLinkActive="active">About</a>
<router-outlet />
// Component
import { Router, ActivatedRoute } from '@angular/router';
export class DetailComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
ngOnInit() {
// route params
this.route.paramMap.subscribe(p => {
const id = p.get('id');
});
// query params
this.route.queryParams.subscribe(q => {
const page = q['page'];
});
// snapshot (synchronous, does not update on re-use)
const id = this.route.snapshot.paramMap.get('id');
}
goBack() {
this.router.navigate(['/users']);
}
goToUser(id: number) {
this.router.navigate(['/users', id], { queryParams: { tab: 'info' } });
}
}
Functional guards (Angular 15+)
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isLoggedIn()
? true
: router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
};
Reactive Forms
import { Component } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-signup',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="email" type="email" />
<div *ngIf="email.invalid && email.touched">
<span *ngIf="email.errors?.['required']">Required</span>
<span *ngIf="email.errors?.['email']">Invalid email</span>
</div>
<input formControlName="password" type="password" />
<div *ngIf="password.invalid && password.touched">
<span *ngIf="password.errors?.['minlength']">Min 8 chars</span>
</div>
<button type="submit" [disabled]="form.invalid">Sign Up</button>
</form>
`,
})
export class SignupComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
get email() { return this.form.controls.email; }
get password() { return this.form.controls.password; }
submit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}
Custom validator
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function noSpacesValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const hasSpace = (control.value as string)?.includes(' ');
return hasSpace ? { noSpaces: true } : null;
};
}
// Usage
username: ['', [Validators.required, noSpacesValidator()]],
Signals (Angular 17+)
import { signal, computed, effect, Signal } from '@angular/core';
// Writable signal
const count = signal(0);
count.set(5); // replace value
count.update(n => n + 1); // update from previous
// Read signal
console.log(count()); // 5
// Computed (lazy, memoised)
const double = computed(() => count() * 2);
// Effect (runs when dependencies change)
effect(() => {
console.log(`Count is ${count()}`);
// Cleanup
return () => { /* teardown */ };
});
// Signal from component input (Angular 17.1+)
export class CardComponent {
title = input.required<string>();
subtitle = input<string>('');
onSelect = output<void>();
}
toSignal / toObservable
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
export class Component {
private http = inject(HttpClient);
// Convert Observable → Signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] as User[] }
);
// Convert Signal → Observable
count = signal(0);
count$ = toObservable(this.count);
}
Directives
Custom attribute directive
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true,
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
private el = inject(ElementRef);
@HostListener('mouseenter') onEnter() {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
}
@HostListener('mouseleave') onLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}
Usage: <p appHighlight="lightblue">Hover me</p>
Custom structural directive
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';
@Directive({ selector: '[appUnless]', standalone: true })
export class UnlessDirective {
private template = inject(TemplateRef);
private vc = inject(ViewContainerRef);
@Input() set appUnless(condition: boolean) {
if (!condition) {
this.vc.createEmbeddedView(this.template);
} else {
this.vc.clear();
}
}
}
Usage: <div *appUnless="isLoading">Content</div>
Change detection
// Default: check every component on every event
// OnPush: only check when:
// - @Input reference changes
// - signal inside reads
// - event inside component fires
// - async pipe emits
// - markForCheck() called manually
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})
export class PureComponent {
private cdr = inject(ChangeDetectorRef);
// Force check when needed
loadData() {
this.service.getData().subscribe(data => {
this.data = data;
this.cdr.markForCheck();
});
}
}
Common patterns
Loading state pattern with signals
@Component({ standalone: true, template: `
@if (loading()) {
<spinner />
} @else if (error()) {
<p class="error">{{ error() }}</p>
} @else {
<user-list [users]="users()" />
}
` })
export class UsersPageComponent {
private userService = inject(UserService);
users = signal<User[]>([]);
loading = signal(true);
error = signal<string | null>(null);
ngOnInit() {
this.userService.getUsers().subscribe({
next: users => { this.users.set(users); this.loading.set(false); },
error: err => { this.error.set(err.message); this.loading.set(false); },
});
}
}
Global state with a signal store
// store/cart.store.ts
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CartStore {
private items = signal<CartItem[]>([]);
readonly count = computed(() => this.items().length);
readonly total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.qty, 0)
);
readonly snapshot = this.items.asReadonly();
add(item: CartItem) {
this.items.update(list => [...list, item]);
}
remove(id: number) {
this.items.update(list => list.filter(i => i.id !== id));
}
clear() {
this.items.set([]);
}
}
Common mistakes
| Mistake | Fix |
|---|---|
| Subscribing in a component without unsubscribing | Use takeUntilDestroyed() or AsyncPipe |
Mutating @Input objects directly |
Return new reference: this.items = [...this.items, newItem] |
Using ngOnInit to read @Input with input() signals |
Read signal value directly — it's available immediately |
Forgetting track in @for loops |
Always track by unique id for performance |
Using *ngFor + *ngIf on same element |
Wrap with <ng-container> or use @if/@for blocks |
| Nested subscriptions (subscribe inside subscribe) | Use switchMap, mergeMap, or combineLatest |
Calling signal.set() inside computed() |
computed() must be pure — use effect() for side effects |
Importing CommonModule in standalone with Angular 17 |
@if/@for are built-in — CommonModule optional for most things |
Angular CLI cheat sheet
# New project
ng new my-app --standalone --routing --style=scss
# Generate
ng generate component features/users/user-list # or: ng g c
ng generate service services/auth # or: ng g s
ng generate directive directives/highlight # or: ng g d
ng generate pipe pipes/truncate # or: ng g p
ng generate guard guards/auth # or: ng g g
ng generate interface models/user # or: ng g i
# Development
ng serve # http://localhost:4200 (auto-reload)
ng serve --open # open browser
ng serve --port 4201 # custom port
# Build
ng build # development
ng build --configuration production # production (minified, AOT)
# Testing
ng test # Karma unit tests (watch mode)
ng e2e # end-to-end tests
# Update
ng update @angular/core @angular/cli
# Analyse bundle
ng build --stats-json && npx webpack-bundle-analyzer dist/my-app/stats.json
FAQ
What is the difference between signal() and BehaviorSubject?
signal() is synchronous and integrates with Angular's change detection without RxJS. BehaviorSubject is RxJS-based and ideal when you need operators like debounceTime or switchMap. Use toSignal() / toObservable() to bridge them.
Should I use standalone components or NgModules?
Standalone (no NgModule) is the modern default since Angular 15 and is what the CLI generates. NgModules are still supported but are being phased out in favour of standalone.
When should I use OnPush change detection?
Use it on every component that receives data only via @Input or signals. It significantly reduces the number of change detection cycles in large apps with many components.
What is the difference between switchMap, mergeMap, and concatMap?
switchMap cancels the previous inner observable when a new value arrives (good for search). mergeMap runs all concurrently (good for parallel requests). concatMap queues them in order (good for sequential writes).
How do I share state between components that are not parent/child?
Use a singleton service (providedIn: 'root') that holds signals or a BehaviorSubject. Inject it wherever you need it.
What replaced APP_INITIALIZER in standalone Angular?
Use provideAppInitializer(() => inject(ConfigService).load()) in the providers array of bootstrapApplication().