24 min read 4822 words Updated Sep 24, 2026 Created Sep 24, 2026
##elseBlock##emailInput##form##ngModel##thenBlock##userCard##username#angular#cheatsheet#frontend#reference#review#typescript#web-development

Angular Cheatsheet v20+

Version

This cheatsheet covers Angular v20+ features including standalone components, signals, new control flow, and modern patterns.


Table of Contents


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 {}
Selector Best Practices
  • 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

Angular Signals (v16+)

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.
    });
  }
}
Effect Use Cases

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

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" />
Model vs Input/Output

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
Always use 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" />
Modern Alternatives
Prefer native syntax over NgClass/NgStyle:

  • [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;
  }
}
Impure Pipes

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

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.
  }
}
Lifecycle Order
  1. ngOnChanges (if inputs change)
  2. ngOnInit
  3. ngDoCheck (custom change detection)
  4. ngAfterContentInit
  5. ngAfterContentChecked
  6. ngAfterViewInit
  7. ngAfterViewChecked
  8. 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();
  }
}
OnPush with Signals

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

Deprecated

The trigger/state/transition animation system is deprecated in v20.2. Use CSS animations or the new animate.enter/leave API instead.

/* 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

TypeSyntaxExample
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

FeatureNew SyntaxOld 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

ApproachUse CaseComplexity
Template-DrivenSimple formsLow
ReactiveComplex formsMedium
Signal FormsModern, reactiveMedium

Best Practices

DO ✅
  • 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 ❌
  • 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


Metadata

Last Updated: 2026-03-15
Angular Version: v20+
Author: Generated using Angular MCP Server

Auto-Generated

This cheatsheet was generated using the Angular CLI MCP Server with official Angular v20 documentation.