Toolmingo
Guides13 min read

Angular Cheat Sheet: Components, Services, RxJS & Patterns

A complete Angular cheat sheet — components, directives, pipes, services, dependency injection, RxJS, routing, forms, HTTP, signals, and common patterns with copy-ready examples.

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().

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools