All topics
Frontend · Learning hub

Angular notes for developers

Master Angular with a curated set of 10 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
Angular

Components & Modules

Angular Components & Modules Components are the building blocks of Angular applications. Every Angular app has at least one component, the root component. Modul

Angular Components & Modules

Components are the building blocks of Angular applications. Every Angular app has at least one component, the root component. Modules organize components and dependencies into cohesive blocks of functionality.

Component Fundamentals

A component controls a patch of screen called a view. It consists of a TypeScript class, an HTML template, and CSS styles.

// Basic component
import { Component } from '@angular/core';

@Component({
  selector: 'app-hello',
  template: `
    <div>
      <h1>{{ title }}</h1>
      <p>{{ description }}</p>
      <button (click)="handleClick()">Click Me</button>
    </div>
  `,
  styles: [`
    h1 {
      color: blue;
      font-size: 24px;
    }
  `]
})
export class HelloComponent {
  title = 'Hello Angular';
  description = 'Welcome to Angular components';
  
  handleClick() {
    console.log('Button clicked!');
  }
}

// Component with external template and styles
@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent {
  user = {
    name: 'John Doe',
    email: 'john@example.com',
    age: 30
  };
  
  isEditing = false;
  
  toggleEdit() {
    this.isEditing = !this.isEditing;
  }
  
  saveUser() {
    console.log('Saving user:', this.user);
    this.isEditing = false;
  }
}

Component Lifecycle Hooks

Angular provides lifecycle hooks that give visibility into key moments in the component lifecycle.

import { Component, OnInit, OnDestroy, OnChanges, SimpleChanges, 
         AfterViewInit, AfterContentInit, Input } from '@angular/core';

@Component({
  selector: 'app-lifecycle-demo',
  template: `<div>{{ data }}</div>`
})
export class LifecycleDemoComponent implements OnInit, OnDestroy, OnChanges, 
                                               AfterViewInit, AfterContentInit {
  @Input() data: string = '';
  private subscription: any;
  
  // Called once after first ngOnChanges
  ngOnInit(): void {
    console.log('ngOnInit: Component initialized');
    // Initialize data, subscribe to observables
    this.subscription = someObservable$.subscribe(data => {
      this.data = data;
    });
  }
  
  // Called when input properties change
  ngOnChanges(changes: SimpleChanges): void {
    console.log('ngOnChanges:', changes);
    if (changes['data']) {
      console.log('Data changed from', changes['data'].previousValue, 
                  'to', changes['data'].currentValue);
    }
  }
  
  // Called after component's view has been initialized
  ngAfterViewInit(): void {
    console.log('ngAfterViewInit: View initialized');
    // Access @ViewChild elements here
  }
  
  // Called after content has been projected
  ngAfterContentInit(): void {
    console.log('ngAfterContentInit: Content initialized');
    // Access @ContentChild elements here
  }
  
  // Called just before component is destroyed
  ngOnDestroy(): void {
    console.log('ngOnDestroy: Cleaning up');
    // Unsubscribe, detach event handlers
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }
}

Component Communication

@Input() and @Output()

// Child component
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-child',
  template: `
    <div>
      <h2>{{ title }}</h2>
      <p>Count: {{ count }}</p>
      <button (click)="increment()">Increment</button>
      <button (click)="sendMessage()">Send to Parent</button>
    </div>
  `
})
export class ChildComponent {
  // Input from parent
  @Input() title: string = '';
  @Input() count: number = 0;
  
  // Output to parent
  @Output() countChange = new EventEmitter<number>();
  @Output() message = new EventEmitter<string>();
  
  increment() {
    this.count++;
    this.countChange.emit(this.count);
  }
  
  sendMessage() {
    this.message.emit('Hello from child!');
  }
}

// Parent component
@Component({
  selector: 'app-parent',
  template: `
    <app-child
      [title]="parentTitle"
      [count]="parentCount"
      (countChange)="onCountChange($event)"
      (message)="onMessage($event)"
    ></app-child>
    
    <p>Parent count: {{ parentCount }}</p>
    <p>Last message: {{ lastMessage }}</p>
  `
})
export class ParentComponent {
  parentTitle = 'Child Component';
  parentCount = 0;
  lastMessage = '';
  
  onCountChange(newCount: number) {
    this.parentCount = newCount;
  }
  
  onMessage(msg: string) {
    this.lastMessage = msg;
  }
}

ViewChild and ContentChild

import { Component, ViewChild, ContentChild, AfterViewInit, ElementRef } from '@angular/core';

@Component({
  selector: 'app-parent',
  template: `
    <input #myInput type="text" />
    <button (click)="focusInput()">Focus Input</button>
    
    <app-child>
      <p #projectedContent>This is projected content</p>
    </app-child>
  `
})
export class ParentComponent implements AfterViewInit {
  // Access child component or element
  @ViewChild('myInput') inputElement!: ElementRef;
  @ViewChild(ChildComponent) childComponent!: ChildComponent;
  
  // Access projected content
  @ContentChild('projectedContent') content!: ElementRef;
  
  ngAfterViewInit() {
    // Can access ViewChild and ContentChild here
    console.log('Input element:', this.inputElement.nativeElement);
  }
  
  focusInput() {
    this.inputElement.nativeElement.focus();
  }
}

NgModules

NgModules consolidate components, directives, and pipes into cohesive blocks of functionality. Every Angular app has at least one module, the root module.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

// Root module
@NgModule({
  declarations: [
    // Components, directives, pipes that belong to this module
    AppComponent,
    HeaderComponent,
    FooterComponent
  ],
  imports: [
    // Other modules whose exported classes are needed
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule,
    SharedModule,
    FeatureModule
  ],
  providers: [
    // Services available app-wide
    AuthService,
    DataService
  ],
  bootstrap: [AppComponent] // Root component
})
export class AppModule { }

// Feature module
@NgModule({
  declarations: [
    UserListComponent,
    UserDetailComponent
  ],
  imports: [
    CommonModule,
    SharedModule
  ],
  exports: [
    // Make these available to importing modules
    UserListComponent
  ]
})
export class UserModule { }

// Shared module
@NgModule({
  declarations: [
    LoadingSpinnerComponent,
    ErrorMessageComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    // Export both declarations and imported modules
    CommonModule,
    LoadingSpinnerComponent,
    ErrorMessageComponent
  ]
})
export class SharedModule { }

Standalone Components (Angular 14+)

Standalone components eliminate the need for NgModules, making Angular simpler and more modular.

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ChildComponent } from './child.component';

@Component({
  selector: 'app-standalone',
  standalone: true, // Mark as standalone
  imports: [
    CommonModule,
    FormsModule,
    ChildComponent // Import other standalone components
  ],
  template: `
    <div>
      <input [(ngModel)]="name" />
      <p *ngIf="name">Hello, {{ name }}!</p>
      <app-child [data]="name"></app-child>
    </div>
  `
})
export class StandaloneComponent {
  name = '';
}

// Bootstrap standalone component
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';

bootstrapApplication(StandaloneComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient()
  ]
});

Content Projection (ng-content)

// Card component
@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="[card-header]"></ng-content>
      </div>
      <div class="card-body">
        <ng-content></ng-content>
      </div>
      <div class="card-footer">
        <ng-content select="[card-footer]"></ng-content>
      </div>
    </div>
  `
})
export class CardComponent { }

// Usage
@Component({
  template: `
    <app-card>
      <h2 card-header>Card Title</h2>
      <p>This is the main card content.</p>
      <button card-footer>Action</button>
    </app-card>
  `
})
export class AppComponent { }
Angular

Dependency Injection & Services

Angular Dependency Injection & Services Dependency Injection (DI) is a core concept in Angular. It allows you to inject dependencies into components and service

Angular Dependency Injection & Services

Dependency Injection (DI) is a core concept in Angular. It allows you to inject dependencies into components and services rather than creating them manually. Services are singleton objects that encapsulate business logic and data.

Creating Services

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

// Service with providedIn: 'root' (singleton)
@Injectable({
  providedIn: 'root' // Available app-wide
})
export class UserService {
  private apiUrl = 'https://api.example.com/users';
  private usersSubject = new BehaviorSubject<User[]>([]);
  public users$ = this.usersSubject.asObservable();
  
  constructor(private http: HttpClient) {
    this.loadUsers();
  }
  
  private loadUsers() {
    this.http.get<User[]>(this.apiUrl).subscribe(
      users => this.usersSubject.next(users)
    );
  }
  
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }
  
  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
  
  createUser(user: User): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }
  
  updateUser(id: number, user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${id}`, user);
  }
  
  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

interface User {
  id: number;
  name: string;
  email: string;
}

Injecting Services

import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  template: `
    <div>
      <h2>Users</h2>
      <ul>
        <li *ngFor="let user of users">
          {{ user.name }} - {{ user.email }}
        </li>
      </ul>
    </div>
  `
})
export class UserListComponent implements OnInit {
  users: User[] = [];
  
  // Constructor injection (traditional)
  constructor(private userService: UserService) { }
  
  ngOnInit() {
    this.userService.getUsers().subscribe(
      users => this.users = users
    );
  }
}

// Using inject() function (Angular 14+)
import { inject } from '@angular/core';

@Component({
  selector: 'app-user-detail',
  template: `<div>{{ user?.name }}</div>`
})
export class UserDetailComponent implements OnInit {
  // Modern inject() function
  private userService = inject(UserService);
  private route = inject(ActivatedRoute);
  
  user: User | null = null;
  
  ngOnInit() {
    const id = Number(this.route.snapshot.paramMap.get('id'));
    this.userService.getUser(id).subscribe(
      user => this.user = user
    );
  }
}

Provider Scope

// 1. Root level - singleton across entire app
@Injectable({
  providedIn: 'root'
})
export class GlobalService { }

// 2. Module level - singleton within module
@NgModule({
  providers: [ModuleScopedService]
})
export class FeatureModule { }

// 3. Component level - new instance per component
@Component({
  selector: 'app-example',
  providers: [ComponentScopedService] // New instance
})
export class ExampleComponent { }

// 4. Lazy loaded module - singleton within lazy module
@Injectable({
  providedIn: 'any' // New instance per lazy module
})
export class LazyService { }

Injection Tokens

InjectionTokens allow you to inject values that are not classes, like configuration objects.

import { InjectionToken } from '@angular/core';

// Define configuration interface
export interface AppConfig {
  apiUrl: string;
  production: boolean;
  version: string;
}

// Create injection token
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

// Provide value in module
@NgModule({
  providers: [
    {
      provide: APP_CONFIG,
      useValue: {
        apiUrl: 'https://api.example.com',
        production: false,
        version: '1.0.0'
      }
    }
  ]
})
export class AppModule { }

// Inject in component or service
@Injectable()
export class ApiService {
  constructor(@Inject(APP_CONFIG) private config: AppConfig) {
    console.log('API URL:', this.config.apiUrl);
  }
}

Hierarchical Injectors

// Parent component with service
@Component({
  selector: 'app-parent',
  providers: [SharedService], // Parent instance
  template: `
    <app-child-a></app-child-a>
    <app-child-b></app-child-b>
  `
})
export class ParentComponent {
  constructor(private shared: SharedService) {
    this.shared.setValue('from parent');
  }
}

// Child A - inherits parent's service instance
@Component({
  selector: 'app-child-a',
  template: `<p>{{ shared.getValue() }}</p>`
})
export class ChildAComponent {
  constructor(public shared: SharedService) { }
}

// Child B - has own instance
@Component({
  selector: 'app-child-b',
  providers: [SharedService], // Own instance
  template: `<p>{{ shared.getValue() }}</p>`
})
export class ChildBComponent {
  constructor(public shared: SharedService) {
    this.shared.setValue('child B');
  }
}

Optional and Self Decorators

import { Component, Optional, Self, SkipSelf, Host } from '@angular/core';

@Component({
  selector: 'app-example'
})
export class ExampleComponent {
  constructor(
    // Optional: Don't throw error if not found
    @Optional() private optionalService: OptionalService | null,
    
    // Self: Only look in current component
    @Self() private selfService: SelfService,
    
    // SkipSelf: Skip current component, look up hierarchy
    @SkipSelf() private parentService: ParentService,
    
    // Host: Look up to host component only
    @Host() private hostService: HostService
  ) {
    if (optionalService) {
      optionalService.doSomething();
    }
  }
}

Service Communication Pattern

import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class StateService {
  // BehaviorSubject - has initial value, replays last value
  private dataSubject = new BehaviorSubject<any[]>([]);
  public data$ = this.dataSubject.asObservable();
  
  // Subject - no initial value, no replay
  private eventSubject = new Subject<string>();
  public events$ = this.eventSubject.asObservable();
  
  // Update data
  updateData(data: any[]) {
    this.dataSubject.next(data);
  }
  
  // Get current value
  getCurrentData(): any[] {
    return this.dataSubject.value;
  }
  
  // Emit event
  emitEvent(event: string) {
    this.eventSubject.next(event);
  }
}

// Component A - produces data
@Component({ /* ... */ })
export class ProducerComponent {
  constructor(private state: StateService) { }
  
  updateState() {
    this.state.updateData([1, 2, 3]);
    this.state.emitEvent('data-updated');
  }
}

// Component B - consumes data
@Component({ /* ... */ })
export class ConsumerComponent implements OnInit {
  data: any[] = [];
  
  constructor(private state: StateService) { }
  
  ngOnInit() {
    this.state.data$.subscribe(data => {
      this.data = data;
    });
    
    this.state.events$.subscribe(event => {
      console.log('Event:', event);
    });
  }
}
Angular

RxJS & Observables

Angular RxJS & Observables RxJS (Reactive Extensions for JavaScript) is a library for reactive programming using Observables. Angular extensively uses RxJS for

Angular RxJS & Observables

RxJS (Reactive Extensions for JavaScript) is a library for reactive programming using Observables. Angular extensively uses RxJS for handling asynchronous operations, HTTP requests, and event handling.

Observable Basics

import { Observable, Observer, of, from, interval } from 'rxjs';

// Create observable from scratch
const customObservable$ = new Observable<number>((observer: Observer<number>) => {
  let count = 0;
  const intervalId = setInterval(() => {
    observer.next(count++);
    
    if (count === 5) {
      observer.complete();
      clearInterval(intervalId);
    }
  }, 1000);
  
  // Cleanup function
  return () => {
    clearInterval(intervalId);
  };
});

// Subscribe to observable
const subscription = customObservable$.subscribe({
  next: (value) => console.log('Value:', value),
  error: (err) => console.error('Error:', err),
  complete: () => console.log('Complete')
});

// Unsubscribe
subscription.unsubscribe();

// Create observables from values
const fromValue$ = of(1, 2, 3, 4, 5);
const fromArray$ = from([1, 2, 3, 4, 5]);
const fromPromise$ = from(fetch('/api/data'));
const timer$ = interval(1000); // Emits every second

Common RxJS Operators

Transformation Operators

import { map, pluck, scan, reduce } from 'rxjs/operators';
import { of } from 'rxjs';

// map - transform each value
of(1, 2, 3, 4, 5)
  .pipe(
    map(x => x * 2)
  )
  .subscribe(console.log); // 2, 4, 6, 8, 10

// pluck - extract property
of(
  { name: 'John', age: 30 },
  { name: 'Jane', age: 25 }
)
  .pipe(
    pluck('name')
  )
  .subscribe(console.log); // 'John', 'Jane'

// scan - accumulate values (like reduce but emits intermediate values)
of(1, 2, 3, 4, 5)
  .pipe(
    scan((acc, curr) => acc + curr, 0)
  )
  .subscribe(console.log); // 1, 3, 6, 10, 15

// reduce - accumulate and emit final value
of(1, 2, 3, 4, 5)
  .pipe(
    reduce((acc, curr) => acc + curr, 0)
  )
  .subscribe(console.log); // 15

Filtering Operators

import { filter, take, takeUntil, takeWhile, skip, distinct, distinctUntilChanged } from 'rxjs/operators';
import { interval, Subject } from 'rxjs';

// filter - emit values that pass condition
of(1, 2, 3, 4, 5)
  .pipe(
    filter(x => x % 2 === 0)
  )
  .subscribe(console.log); // 2, 4

// take - emit first N values
interval(1000)
  .pipe(
    take(3)
  )
  .subscribe(console.log); // 0, 1, 2

// takeUntil - emit until notifier emits
const stop$ = new Subject();
interval(1000)
  .pipe(
    takeUntil(stop$)
  )
  .subscribe(console.log);

setTimeout(() => stop$.next(), 3000); // Stops after 3 seconds

// takeWhile - emit while condition is true
of(1, 2, 3, 4, 5)
  .pipe(
    takeWhile(x => x < 4)
  )
  .subscribe(console.log); // 1, 2, 3

// skip - skip first N values
of(1, 2, 3, 4, 5)
  .pipe(
    skip(2)
  )
  .subscribe(console.log); // 3, 4, 5

// distinct - emit unique values
of(1, 2, 2, 3, 3, 4)
  .pipe(
    distinct()
  )
  .subscribe(console.log); // 1, 2, 3, 4

// distinctUntilChanged - emit when value changes from previous
of(1, 1, 2, 2, 3, 3)
  .pipe(
    distinctUntilChanged()
  )
  .subscribe(console.log); // 1, 2, 3

Combination Operators

import { merge, concat, combineLatest, forkJoin, zip } from 'rxjs';
import { delay } from 'rxjs/operators';

// merge - emit values from multiple observables as they occur
const obs1$ = of('A', 'B').pipe(delay(1000));
const obs2$ = of('1', '2').pipe(delay(500));
merge(obs1$, obs2$).subscribe(console.log); // '1', '2', 'A', 'B'

// concat - emit values sequentially (wait for previous to complete)
concat(obs1$, obs2$).subscribe(console.log); // 'A', 'B', '1', '2'

// combineLatest - emit when any observable emits (after all emit once)
const age$ = of(30, 31, 32);
const name$ = of('John', 'Jane');
combineLatest([age$, name$]).subscribe(console.log);
// [32, 'John'], [32, 'Jane']

// forkJoin - wait for all to complete, emit last values
const req1$ = of('result1').pipe(delay(1000));
const req2$ = of('result2').pipe(delay(2000));
forkJoin([req1$, req2$]).subscribe(console.log);
// ['result1', 'result2'] after 2 seconds

// zip - emit pairs of values at same index
const nums$ = of(1, 2, 3);
const chars$ = of('A', 'B', 'C');
zip(nums$, chars$).subscribe(console.log);
// [1, 'A'], [2, 'B'], [3, 'C']

Advanced Operators

switchMap, mergeMap, concatMap

import { switchMap, mergeMap, concatMap, exhaustMap } from 'rxjs/operators';
import { fromEvent, of } from 'rxjs';
import { ajax } from 'rxjs/ajax';

// switchMap - cancel previous, switch to new observable
// Use for: Search, typeahead
const searchInput = document.getElementById('search');
fromEvent(searchInput, 'input')
  .pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(event => {
      const query = (event.target as HTMLInputElement).value;
      return ajax.getJSON(`/api/search?q=${query}`);
    })
  )
  .subscribe(results => console.log(results));

// mergeMap - run all observables in parallel
// Use for: Independent requests
of(1, 2, 3)
  .pipe(
    mergeMap(id => ajax.getJSON(`/api/users/${id}`))
  )
  .subscribe(user => console.log(user));

// concatMap - run observables sequentially (queue)
// Use for: Ordered operations
of(1, 2, 3)
  .pipe(
    concatMap(id => ajax.post(`/api/process/${id}`, {}))
  )
  .subscribe(result => console.log(result));

// exhaustMap - ignore new observables while current is active
// Use for: Login, submit buttons
const loginButton = document.getElementById('login');
fromEvent(loginButton, 'click')
  .pipe(
    exhaustMap(() => ajax.post('/api/login', credentials))
  )
  .subscribe(response => console.log(response));

Error Handling

import { catchError, retry, retryWhen, tap } from 'rxjs/operators';
import { throwError, of, timer } from 'rxjs';

// catchError - handle errors and continue
this.http.get('/api/data')
  .pipe(
    catchError(error => {
      console.error('Error:', error);
      return of([]); // Return fallback value
    })
  )
  .subscribe(data => console.log(data));

// retry - retry on error
this.http.get('/api/data')
  .pipe(
    retry(3) // Retry up to 3 times
  )
  .subscribe();

// retryWhen - custom retry logic
this.http.get('/api/data')
  .pipe(
    retryWhen(errors =>
      errors.pipe(
        tap(error => console.log('Retrying...', error)),
        delay(1000), // Wait 1 second between retries
        take(3) // Max 3 retries
      )
    )
  )
  .subscribe();

Subjects

import { Subject, BehaviorSubject, ReplaySubject, AsyncSubject } from 'rxjs';

// Subject - no initial value, no replay
const subject = new Subject<number>();
subject.subscribe(v => console.log('Sub 1:', v));
subject.next(1); // Sub 1: 1
subject.subscribe(v => console.log('Sub 2:', v));
subject.next(2); // Sub 1: 2, Sub 2: 2

// BehaviorSubject - has initial value, replays last
const behavior = new BehaviorSubject<number>(0);
behavior.subscribe(v => console.log('Sub 1:', v)); // Sub 1: 0
behavior.next(1); // Sub 1: 1
behavior.subscribe(v => console.log('Sub 2:', v)); // Sub 2: 1

// ReplaySubject - replays last N values
const replay = new ReplaySubject<number>(2); // Buffer size 2
replay.next(1);
replay.next(2);
replay.next(3);
replay.subscribe(v => console.log('Sub:', v)); // Sub: 2, Sub: 3

// AsyncSubject - emits last value on complete
const async = new AsyncSubject<number>();
async.subscribe(v => console.log('Sub:', v));
async.next(1);
async.next(2);
async.next(3);
async.complete(); // Sub: 3
Angular

Routing & Navigation

Angular Routing & Navigation Angular Router enables navigation from one view to the next as users perform tasks. It interprets browser URLs and navigates to cli

Angular Routing & Navigation

Angular Router enables navigation from one view to the next as users perform tasks. It interprets browser URLs and navigates to client-generated views.

Router Setup

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { UserListComponent } from './users/user-list.component';
import { UserDetailComponent } from './users/user-detail.component';
import { NotFoundComponent } from './not-found/not-found.component';

const routes: Routes = [
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { 
    path: 'users', 
    component: UserListComponent,
    children: [
      { path: ':id', component: UserDetailComponent }
    ]
  },
  { path: '**', component: NotFoundComponent } // Wildcard route
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

// Standalone app setup
import { provideRouter } from '@angular/router';
import { bootstrapApplication } from '@angular/platform-browser';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes)
  ]
});

Navigation

import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-nav',
  template: `
    <!-- Declarative navigation -->
    <nav>
      <a routerLink="/home" routerLinkActive="active">Home</a>
      <a routerLink="/about" routerLinkActive="active">About</a>
      <a [routerLink]="['/users', userId]" routerLinkActive="active">User</a>
      <a routerLink="/users/123" [queryParams]="{tab: 'posts'}" 
         [fragment]="'comments'">User with Query</a>
    </nav>
    
    <button (click)="navigateToUser()">Go to User</button>
    <button (click)="goBack()">Go Back</button>
    
    <!-- Router outlet -->
    <router-outlet></router-outlet>
  `
})
export class NavComponent {
  userId = 123;
  
  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) { }
  
  navigateToUser() {
    // Programmatic navigation
    this.router.navigate(['/users', this.userId], {
      queryParams: { tab: 'posts' },
      fragment: 'comments'
    });
  }
  
  goBack() {
    window.history.back();
  }
}

Route Parameters

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap } from 'rxjs/operators';

@Component({
  selector: 'app-user-detail',
  template: `
    <div *ngIf="user">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
      <p>Tab: {{ activeTab }}</p>
    </div>
  `
})
export class UserDetailComponent implements OnInit {
  user: any;
  activeTab: string = '';
  
  constructor(
    private route: ActivatedRoute,
    private userService: UserService
  ) { }
  
  ngOnInit() {
    // Snapshot - one-time access
    const id = this.route.snapshot.paramMap.get('id');
    const tab = this.route.snapshot.queryParamMap.get('tab');
    const fragment = this.route.snapshot.fragment;
    
    // Observable - reactive to changes
    this.route.paramMap.pipe(
      switchMap((params: ParamMap) => {
        const userId = params.get('id');
        return this.userService.getUser(userId);
      })
    ).subscribe(user => this.user = user);
    
    // Query params
    this.route.queryParams.subscribe(params => {
      this.activeTab = params['tab'] || 'profile';
    });
    
    // Fragment
    this.route.fragment.subscribe(fragment => {
      if (fragment) {
        document.getElementById(fragment)?.scrollIntoView();
      }
    });
  }
}

Route Guards

import { Injectable } from '@angular/core';
import { CanActivate, CanActivateChild, CanDeactivate, 
         ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';

// CanActivate - protect route access
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) { }
  
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean | Observable<boolean> {
    if (this.authService.isLoggedIn()) {
      return true;
    }
    
    // Store intended URL
    this.router.navigate(['/login'], {
      queryParams: { returnUrl: state.url }
    });
    return false;
  }
}

// CanActivateChild - protect child routes
@Injectable({ providedIn: 'root' })
export class AdminGuard implements CanActivateChild {
  constructor(private authService: AuthService) { }
  
  canActivateChild(): boolean {
    return this.authService.isAdmin();
  }
}

// CanDeactivate - prevent leaving route
export interface CanComponentDeactivate {
  canDeactivate: () => boolean | Observable<boolean>;
}

@Injectable({ providedIn: 'root' })
export class UnsavedChangesGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(
    component: CanComponentDeactivate
  ): boolean | Observable<boolean> {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

// Component implementing CanComponentDeactivate
@Component({
  selector: 'app-edit-user'
})
export class EditUserComponent implements CanComponentDeactivate {
  hasUnsavedChanges = false;
  
  canDeactivate(): boolean {
    if (this.hasUnsavedChanges) {
      return confirm('You have unsaved changes. Leave anyway?');
    }
    return true;
  }
}

// Apply guards to routes
const routes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    canActivateChild: [AdminGuard],
    children: [
      { path: 'users', component: UsersComponent },
      { 
        path: 'edit/:id', 
        component: EditUserComponent,
        canDeactivate: [UnsavedChangesGuard]
      }
    ]
  }
];

Lazy Loading

const routes: Routes = [
  {
    path: 'users',
    loadChildren: () => import('./users/users.module').then(m => m.UsersModule)
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
    canLoad: [AuthGuard] // Prevent loading module
  }
];

// users-routing.module.ts
const routes: Routes = [
  { path: '', component: UserListComponent },
  { path: ':id', component: UserDetailComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class UsersRoutingModule { }

Resolver

import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserResolver implements Resolve<User> {
  constructor(private userService: UserService) { }
  
  resolve(route: ActivatedRouteSnapshot): Observable<User> {
    const id = route.paramMap.get('id');
    return this.userService.getUser(id);
  }
}

// Use in route
const routes: Routes = [
  {
    path: 'users/:id',
    component: UserDetailComponent,
    resolve: { user: UserResolver }
  }
];

// Access resolved data in component
@Component({ /* ... */ })
export class UserDetailComponent implements OnInit {
  user: User;
  
  constructor(private route: ActivatedRoute) { }
  
  ngOnInit() {
    this.user = this.route.snapshot.data['user'];
    
    // Or with observable
    this.route.data.subscribe(data => {
      this.user = data['user'];
    });
  }
}
Angular

State Management (NgRx & Signals)

Angular State Management Angular provides multiple approaches to state management: services with RxJS, NgRx (Redux pattern), and Signals (Angular 16+). Choose b

Angular State Management

Angular provides multiple approaches to state management: services with RxJS, NgRx (Redux pattern), and Signals (Angular 16+). Choose based on your application complexity.

Service-Based State (Simple)

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

interface AppState {
  user: User | null;
  isLoading: boolean;
  error: string | null;
}

@Injectable({ providedIn: 'root' })
export class StateService {
  private state: AppState = {
    user: null,
    isLoading: false,
    error: null
  };
  
  private stateSubject = new BehaviorSubject<AppState>(this.state);
  public state$ = this.stateSubject.asObservable();
  
  // Selectors
  get user$(): Observable<User | null> {
    return this.state$.pipe(map(state => state.user));
  }
  
  get isLoading$(): Observable<boolean> {
    return this.state$.pipe(map(state => state.isLoading));
  }
  
  // Actions
  setUser(user: User) {
    this.state = { ...this.state, user };
    this.stateSubject.next(this.state);
  }
  
  setLoading(isLoading: boolean) {
    this.state = { ...this.state, isLoading };
    this.stateSubject.next(this.state);
  }
  
  setError(error: string) {
    this.state = { ...this.state, error };
    this.stateSubject.next(this.state);
  }
}

NgRx (Redux Pattern)

NgRx is a Redux-inspired state management library for Angular. It provides a predictable state container with actions, reducers, effects, and selectors.

// 1. Define State
export interface TodoState {
  todos: Todo[];
  loading: boolean;
  error: string | null;
}

export const initialState: TodoState = {
  todos: [],
  loading: false,
  error: null
};

// 2. Create Actions
import { createAction, props } from '@ngrx/store';

export const loadTodos = createAction('[Todo] Load Todos');
export const loadTodosSuccess = createAction(
  '[Todo] Load Todos Success',
  props<{ todos: Todo[] }>()
);
export const loadTodosFailure = createAction(
  '[Todo] Load Todos Failure',
  props<{ error: string }>()
);
export const addTodo = createAction(
  '[Todo] Add Todo',
  props<{ todo: Todo }>()
);
export const deleteTodo = createAction(
  '[Todo] Delete Todo',
  props<{ id: string }>()
);

// 3. Create Reducer
import { createReducer, on } from '@ngrx/store';

export const todoReducer = createReducer(
  initialState,
  on(loadTodos, (state) => ({
    ...state,
    loading: true,
    error: null
  })),
  on(loadTodosSuccess, (state, { todos }) => ({
    ...state,
    todos,
    loading: false
  })),
  on(loadTodosFailure, (state, { error }) => ({
    ...state,
    error,
    loading: false
  })),
  on(addTodo, (state, { todo }) => ({
    ...state,
    todos: [...state.todos, todo]
  })),
  on(deleteTodo, (state, { id }) => ({
    ...state,
    todos: state.todos.filter(t => t.id !== id)
  }))
);

// 4. Create Selectors
import { createFeatureSelector, createSelector } from '@ngrx/store';

export const selectTodoState = createFeatureSelector<TodoState>('todos');

export const selectAllTodos = createSelector(
  selectTodoState,
  (state) => state.todos
);

export const selectActiveTodos = createSelector(
  selectAllTodos,
  (todos) => todos.filter(t => !t.completed)
);

export const selectCompletedTodos = createSelector(
  selectAllTodos,
  (todos) => todos.filter(t => t.completed)
);

export const selectTodosLoading = createSelector(
  selectTodoState,
  (state) => state.loading
);

NgRx Effects

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, mergeMap, catchError, tap } from 'rxjs/operators';
import * as TodoActions from './todo.actions';

@Injectable()
export class TodoEffects {
  loadTodos$ = createEffect(() =>
    this.actions$.pipe(
      ofType(TodoActions.loadTodos),
      mergeMap(() =>
        this.todoService.getTodos().pipe(
          map(todos => TodoActions.loadTodosSuccess({ todos })),
          catchError(error =>
            of(TodoActions.loadTodosFailure({ error: error.message }))
          )
        )
      )
    )
  );
  
  addTodo$ = createEffect(() =>
    this.actions$.pipe(
      ofType(TodoActions.addTodo),
      mergeMap(action =>
        this.todoService.createTodo(action.todo).pipe(
          map(todo => TodoActions.addTodoSuccess({ todo })),
          catchError(error => of(TodoActions.addTodoFailure({ error })))
        )
      )
    )
  );
  
  // Non-dispatching effect (for side effects only)
  logActions$ = createEffect(
    () =>
      this.actions$.pipe(
        tap(action => console.log('Action:', action))
      ),
    { dispatch: false }
  );
  
  constructor(
    private actions$: Actions,
    private todoService: TodoService
  ) { }
}

Using NgRx in Components

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import * as TodoActions from './store/todo.actions';
import * as TodoSelectors from './store/todo.selectors';

@Component({
  selector: 'app-todo-list',
  template: `
    <div>
      <h2>Todos</h2>
      <div *ngIf="loading$ | async">Loading...</div>
      <ul>
        <li *ngFor="let todo of todos$ | async">
          {{ todo.text }}
          <button (click)="delete(todo.id)">Delete</button>
        </li>
      </ul>
      <button (click)="load()">Load Todos</button>
    </div>
  `
})
export class TodoListComponent implements OnInit {
  todos$: Observable<Todo[]>;
  loading$: Observable<boolean>;
  
  constructor(private store: Store) {
    this.todos$ = this.store.select(TodoSelectors.selectAllTodos);
    this.loading$ = this.store.select(TodoSelectors.selectTodosLoading);
  }
  
  ngOnInit() {
    this.load();
  }
  
  load() {
    this.store.dispatch(TodoActions.loadTodos());
  }
  
  delete(id: string) {
    this.store.dispatch(TodoActions.deleteTodo({ id }));
  }
}

Signals (Angular 16+)

Signals provide a reactive primitive for managing state with fine-grained reactivity and better performance than Zone.js.

import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>Count: {{ count() }}</p>
      <p>Double: {{ doubleCount() }}</p>
      <button (click)="increment()">+</button>
      <button (click)="decrement()">-</button>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
export class CounterComponent {
  // Signal - writable
  count = signal(0);
  
  // Computed signal - derived
  doubleCount = computed(() => this.count() * 2);
  
  // Effect - side effects
  constructor() {
    effect(() => {
      console.log('Count changed:', this.count());
      localStorage.setItem('count', this.count().toString());
    });
  }
  
  increment() {
    this.count.update(value => value + 1);
  }
  
  decrement() {
    this.count.update(value => value - 1);
  }
  
  reset() {
    this.count.set(0);
  }
}

// Signal-based service
import { Injectable, signal, computed } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class TodoSignalService {
  private todos = signal<Todo[]>([]);
  
  // Public readonly version
  readonly allTodos = this.todos.asReadonly();
  
  // Computed signals
  readonly activeTodos = computed(() => 
    this.todos().filter(t => !t.completed)
  );
  
  readonly completedTodos = computed(() => 
    this.todos().filter(t => t.completed)
  );
  
  readonly stats = computed(() => ({
    total: this.todos().length,
    active: this.activeTodos().length,
    completed: this.completedTodos().length
  }));
  
  addTodo(todo: Todo) {
    this.todos.update(todos => [...todos, todo]);
  }
  
  deleteTodo(id: string) {
    this.todos.update(todos => todos.filter(t => t.id !== id));
  }
  
  toggleTodo(id: string) {
    this.todos.update(todos =>
      todos.map(t => 
        t.id === id ? { ...t, completed: !t.completed } : t
      )
    );
  }
}
Angular

Testing with Jasmine & Karma

Testing Angular Applications Angular uses Jasmine as the testing framework and Karma as the test runner. Testing is built into the Angular CLI from the start. U

Testing Angular Applications

Angular uses Jasmine as the testing framework and Karma as the test runner. Testing is built into the Angular CLI from the start.

Unit Testing Components

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';

describe('CounterComponent', () => {
  let component: CounterComponent;
  let fixture: ComponentFixture<CounterComponent>;
  let compiled: HTMLElement;
  
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [CounterComponent]
    }).compileComponents();
    
    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    compiled = fixture.nativeElement;
    fixture.detectChanges();
  });
  
  it('should create', () => {
    expect(component).toBeTruthy();
  });
  
  it('should display initial count', () => {
    const countElement = compiled.querySelector('.count');
    expect(countElement?.textContent).toContain('0');
  });
  
  it('should increment count when button clicked', () => {
    const button = compiled.querySelector('.increment-btn') as HTMLButtonElement;
    button.click();
    fixture.detectChanges();
    
    expect(component.count).toBe(1);
    expect(compiled.querySelector('.count')?.textContent).toContain('1');
  });
  
  it('should decrement count', () => {
    component.count = 5;
    fixture.detectChanges();
    
    component.decrement();
    fixture.detectChanges();
    
    expect(component.count).toBe(4);
  });
});

Testing Services

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });
  
  afterEach(() => {
    httpMock.verify();
  });
  
  it('should fetch users', () => {
    const mockUsers = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Jane' }
    ];
    
    service.getUsers().subscribe(users => {
      expect(users.length).toBe(2);
      expect(users).toEqual(mockUsers);
    });
    
    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
  
  it('should handle error', () => {
    service.getUsers().subscribe(
      () => fail('should have failed'),
      (error) => {
        expect(error.status).toBe(404);
      }
    );
    
    const req = httpMock.expectOne('/api/users');
    req.flush('Not found', { status: 404, statusText: 'Not Found' });
  });
});

Testing with Dependencies

describe('UserListComponent', () => {
  let component: UserListComponent;
  let fixture: ComponentFixture<UserListComponent>;
  let userService: jasmine.SpyObj<UserService>;
  
  beforeEach(async () => {
    // Create spy
    const userServiceSpy = jasmine.createSpyObj('UserService', ['getUsers']);
    
    await TestBed.configureTestingModule({
      declarations: [UserListComponent],
      providers: [
        { provide: UserService, useValue: userServiceSpy }
      ]
    }).compileComponents();
    
    fixture = TestBed.createComponent(UserListComponent);
    component = fixture.componentInstance;
    userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
  });
  
  it('should load users on init', () => {
    const mockUsers = [{ id: 1, name: 'John' }];
    userService.getUsers.and.returnValue(of(mockUsers));
    
    component.ngOnInit();
    
    expect(userService.getUsers).toHaveBeenCalled();
    expect(component.users).toEqual(mockUsers);
  });
});

Testing Directives

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  @Input() appHighlight = '';
  
  constructor(private el: ElementRef) { }
  
  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.appHighlight || 'yellow');
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.highlight('');
  }
  
  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

describe('HighlightDirective', () => {
  let fixture: ComponentFixture<TestComponent>;
  let des: DebugElement[];
  
  @Component({
    template: `
      <p appHighlight>Default</p>
      <p appHighlight="red">Red</p>
    `
  })
  class TestComponent { }
  
  beforeEach(() => {
    fixture = TestBed.configureTestingModule({
      declarations: [HighlightDirective, TestComponent]
    }).createComponent(TestComponent);
    
    fixture.detectChanges();
    des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
  });
  
  it('should have 2 highlighted elements', () => {
    expect(des.length).toBe(2);
  });
  
  it('should color first element yellow on mouseenter', () => {
    des[0].triggerEventHandler('mouseenter', null);
    expect(des[0].nativeElement.style.backgroundColor).toBe('yellow');
  });
});

Testing Async Operations

import { fakeAsync, tick, flush, waitForAsync } from '@angular/core/testing';

describe('Async Tests', () => {
  // fakeAsync - control time
  it('should work with fakeAsync', fakeAsync(() => {
    let value = false;
    
    setTimeout(() => {
      value = true;
    }, 1000);
    
    expect(value).toBe(false);
    tick(1000); // Advance time by 1000ms
    expect(value).toBe(true);
  }));
  
  // waitForAsync - wait for promises
  it('should work with async', waitForAsync(() => {
    const promise = new Promise(resolve => {
      setTimeout(() => resolve('done'), 1000);
    });
    
    promise.then(value => {
      expect(value).toBe('done');
    });
  }));
  
  // Test observables
  it('should handle observables', fakeAsync(() => {
    let result: string;
    
    of('test').pipe(delay(1000)).subscribe(value => {
      result = value;
    });
    
    tick(1000);
    expect(result!).toBe('test');
  }));
});
Angular

Interview Questions

Angular Interview Questions Comprehensive Angular interview questions covering architecture, components, services, RxJS, and best practices: Fundamentals 1. Wha

Angular Interview Questions

Comprehensive Angular interview questions covering architecture, components, services, RxJS, and best practices:

Fundamentals

1. What is Angular and its key features?

Angular is a TypeScript-based framework for building web applications. Key features: Component-based architecture, dependency injection, RxJS integration, TypeScript support, comprehensive CLI, built-in routing, forms, and HTTP client.

2. What are the main building blocks of Angular?

  • Modules (NgModule)

  • Components

  • Templates

  • Services

  • Directives

  • Pipes

3. What is dependency injection in Angular?

DI is a design pattern where dependencies are provided to a class rather than created by it. Angular has a hierarchical injector system that provides dependencies at different levels (root, module, component).

4. What is the difference between @Component and @Directive?

@Component extends @Directive with a template. Components have their own view, while directives modify the behavior or appearance of existing elements.

5. What are lifecycle hooks in Angular?

Lifecycle hooks are methods called at specific times: ngOnInit, ngOnChanges, ngDoCheck, ngAfterContentInit, ngAfterContentChecked, ngAfterViewInit, ngAfterViewChecked, ngOnDestroy.

Components & Data Binding

6. What are the different types of data binding?

  • Interpolation: {{ value }}

  • Property binding: [property]="value"

  • Event binding: (event)="handler()"

  • Two-way binding: [(ngModel)]="value"

7. What is @Input() and @Output()?

@Input() receives data from parent. @Output() emits events to parent using EventEmitter.

8. What is ViewChild and ContentChild?

ViewChild accesses child components/elements in the component template. ContentChild accesses content projected into the component via ng-content.

Services & DI

9. What is providedIn: "root"?

Makes service available app-wide as a singleton. Angular creates one instance shared across the application. Alternative to providing in module.

10. What is the hierarchical injector?

Angular has a tree of injectors matching the component tree. Child injectors can override parent providers. Services can be provided at root, module, or component level.

RxJS & Observables

11. What is the difference between Promise and Observable?

  • Observable: Lazy, cancellable, multiple values, operators

  • Promise: Eager, not cancellable, single value

12. What is the difference between switchMap, mergeMap, and concatMap?

  • switchMap: Cancels previous, switches to new

  • mergeMap: Runs all in parallel

  • concatMap: Queues sequentially

13. What is a Subject?

A Subject is both an Observable and Observer. It can multicast to multiple observers. Types: Subject, BehaviorSubject, ReplaySubject, AsyncSubject.

Routing

14. What are route guards?

Guards control navigation: CanActivate (access route), CanActivateChild (child routes), CanDeactivate (leave route), CanLoad (lazy load), Resolve (pre-fetch data).

15. What is lazy loading?

Lazy loading loads modules on-demand rather than at app start. Uses loadChildren in routes to load modules when navigating to them.

Advanced

16. What are standalone components?

Standalone components (Angular 14+) don't need NgModules. They import dependencies directly, simplifying Angular architecture.

17. What are Signals?

Signals (Angular 16+) are reactive primitives for fine-grained reactivity. They provide better performance than Zone.js and simpler state management.

18. What is Change Detection?

Change detection checks if data changed and updates the view. Strategies: Default (check entire tree) and OnPush (check only when inputs change or events occur).

19. What is NgRx?

NgRx is a Redux-inspired state management library. Uses Actions, Reducers, Effects, and Selectors for predictable state management.

20. What are pipes?

Pipes transform data in templates. Built-in: date, currency, uppercase, async. Custom pipes implement PipeTransform interface. Pure pipes (default) only re-run when input reference changes.

Forms & Validation

21. What is the difference between template-driven and reactive forms?

  • Template-driven: Uses directives, simpler, good for basic forms

  • Reactive: Model-driven, better for complex forms, easier to test, more control

22. What is FormArray?

FormArray manages an array of FormControl, FormGroup, or FormArray instances. Useful for dynamic forms where the number of controls is not known in advance.

23. How do you create custom validators?

Implement ValidatorFn for sync validators or AsyncValidatorFn for async validators. Return ValidationErrors object or null.

Performance & Optimization

24. When should you use OnPush change detection?

Use OnPush for components that only depend on inputs and don't have internal state changes. It significantly improves performance by reducing change detection cycles.

25. What is trackBy and why is it important?

trackBy is a function used with *ngFor to help Angular identify which items changed, added, or removed. It prevents unnecessary DOM recreations by tracking items by unique identifier.

26. What is NgZone and when to use runOutsideAngular?

NgZone wraps async operations and notifies Angular when to run change detection. Use runOutsideAngular() for high-frequency events or heavy computations that don't need to update the UI immediately.

Practical Scenarios

27. How do you prevent memory leaks?

Unsubscribe from observables in ngOnDestroy, use async pipe (auto unsubscribes), use takeUntil operator, or use SubSink/Subscription.add() for multiple subscriptions.

28. How do you share data between components?

  • Parent-Child: @Input() and @Output()

  • Sibling: Shared service with Subject/BehaviorSubject

  • Any: State management (NgRx, Signals)

29. What is AOT compilation?

Ahead-of-Time compilation compiles Angular templates during the build process (vs Just-in-Time at runtime). Benefits: Faster rendering, smaller bundle size, early error detection, better security.

30. How do you optimize bundle size?

  • Use production build with AOT

  • Lazy load feature modules

  • Use tree-shakeable providers (providedIn)

  • Remove unused code and dependencies

  • Use standalone components

Angular

Forms (Template-driven & Reactive)

Angular Forms Angular provides two approaches to handling user input through forms: template-driven forms and reactive forms. Both capture user input events, va

Angular Forms

Angular provides two approaches to handling user input through forms: template-driven forms and reactive forms. Both capture user input events, validate input, and create a form model.

Template-Driven Forms

Template-driven forms rely on directives in the template to create and manipulate the form model. They are simpler and work well for basic forms.

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-template-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
      <div>
        <label>Name:
          <input
            type="text"
            name="name"
            [(ngModel)]="user.name"
            required
            minlength="3"
            #name="ngModel"
          />
        </label>
        <div *ngIf="name.invalid && name.touched">
          <small *ngIf="name.errors?.['required']">Name is required</small>
          <small *ngIf="name.errors?.['minlength']">
            Name must be at least 3 characters
          </small>
        </div>
      </div>
      
      <div>
        <label>Email:
          <input
            type="email"
            name="email"
            [(ngModel)]="user.email"
            required
            email
            #email="ngModel"
          />
        </label>
        <div *ngIf="email.invalid && email.touched">
          <small *ngIf="email.errors?.['required']">Email is required</small>
          <small *ngIf="email.errors?.['email']">Invalid email format</small>
        </div>
      </div>
      
      <div>
        <label>Age:
          <input
            type="number"
            name="age"
            [(ngModel)]="user.age"
            required
            min="18"
            max="100"
          />
        </label>
      </div>
      
      <button type="submit" [disabled]="userForm.invalid">Submit</button>
    </form>
    
    <div *ngIf="submitted">
      <h3>Submitted Data:</h3>
      <pre>{{ user | json }}</pre>
    </div>
  `
})
export class TemplateFormComponent {
  user = {
    name: '',
    email: '',
    age: null
  };
  
  submitted = false;
  
  onSubmit(form: any) {
    if (form.valid) {
      console.log('Form submitted:', this.user);
      this.submitted = true;
    }
  }
}

Reactive Forms

Reactive forms provide a model-driven approach to handling form inputs. They offer more robust validation, better testability, and are more suitable for complex forms.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({
  selector: 'app-reactive-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
      <div>
        <label>Name:
          <input type="text" formControlName="name" />
        </label>
        <div *ngIf="name?.invalid && name?.touched">
          <small *ngIf="name?.errors?.['required']">Name is required</small>
          <small *ngIf="name?.errors?.['minlength']">
            Name must be at least 3 characters
          </small>
        </div>
      </div>
      
      <div>
        <label>Email:
          <input type="email" formControlName="email" />
        </label>
        <div *ngIf="email?.invalid && email?.touched">
          <small *ngIf="email?.errors?.['required']">Email is required</small>
          <small *ngIf="email?.errors?.['email']">Invalid email</small>
        </div>
      </div>
      
      <div>
        <label>Password:
          <input type="password" formControlName="password" />
        </label>
        <div *ngIf="password?.invalid && password?.touched">
          <small *ngIf="password?.errors?.['required']">Password is required</small>
          <small *ngIf="password?.errors?.['minlength']">
            Password must be at least 8 characters
          </small>
        </div>
      </div>
      
      <div formGroupName="address">
        <h3>Address</h3>
        <label>Street:
          <input type="text" formControlName="street" />
        </label>
        <label>City:
          <input type="text" formControlName="city" />
        </label>
        <label>Zip:
          <input type="text" formControlName="zip" />
        </label>
      </div>
      
      <button type="submit" [disabled]="userForm.invalid">Submit</button>
    </form>
    
    <div>
      <p>Form Status: {{ userForm.status }}</p>
      <p>Form Value: {{ userForm.value | json }}</p>
    </div>
  `
})
export class ReactiveFormComponent implements OnInit {
  userForm!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    // Using FormBuilder (recommended)
    this.userForm = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(8)]],
      address: this.fb.group({
        street: [''],
        city: ['', Validators.required],
        zip: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]] 
      })
    });
    
    // Or using FormControl directly
    // this.userForm = new FormGroup({
    //   name: new FormControl('', [Validators.required]),
    //   email: new FormControl('', [Validators.required, Validators.email])
    // });
  }
  
  // Getters for easy access in template
  get name() { return this.userForm.get('name'); }
  get email() { return this.userForm.get('email'); }
  get password() { return this.userForm.get('password'); }
  
  onSubmit() {
    if (this.userForm.valid) {
      console.log('Form submitted:', this.userForm.value);
      // Reset form
      this.userForm.reset();
    } else {
      // Mark all as touched to show validation errors
      this.userForm.markAllAsTouched();
    }
  }
}

Custom Validators

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// Sync validator
export function forbiddenNameValidator(forbiddenName: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = forbiddenName.test(control.value);
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

// Password match validator
export function passwordMatchValidator(): ValidatorFn {
  return (formGroup: AbstractControl): ValidationErrors | null => {
    const password = formGroup.get('password');
    const confirmPassword = formGroup.get('confirmPassword');
    
    if (!password || !confirmPassword) {
      return null;
    }
    
    return password.value === confirmPassword.value
      ? null
      : { passwordMismatch: true };
  };
}

// Async validator (e.g., check username availability)
export function usernameAvailableValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }
    
    return userService.checkUsername(control.value).pipe(
      map(available => available ? null : { usernameTaken: true }),
      catchError(() => of(null))
    );
  };
}

// Usage
this.userForm = this.fb.group({
  username: ['', 
    [Validators.required, forbiddenNameValidator(/admin/i)],
    [usernameAvailableValidator(this.userService)] // async
  ],
  password: ['', [Validators.required, Validators.minLength(8)]],
  confirmPassword: ['', Validators.required]
}, { 
  validators: passwordMatchValidator() // form-level validator
});

Dynamic Forms & FormArray

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';

@Component({
  selector: 'app-dynamic-form',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div formArrayName="skills">
        <h3>Skills</h3>
        <div *ngFor="let skill of skills.controls; let i = index" [formGroupName]="i">
          <input formControlName="name" placeholder="Skill name" />
          <input formControlName="years" type="number" placeholder="Years" />
          <button type="button" (click)="removeSkill(i)">Remove</button>
        </div>
        <button type="button" (click)="addSkill()">Add Skill</button>
      </div>
      
      <button type="submit" [disabled]="form.invalid">Submit</button>
    </form>
    
    <pre>{{ form.value | json }}</pre>
  `
})
export class DynamicFormComponent implements OnInit {
  form!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit() {
    this.form = this.fb.group({
      name: ['', Validators.required],
      skills: this.fb.array([
        this.createSkill()
      ])
    });
  }
  
  get skills(): FormArray {
    return this.form.get('skills') as FormArray;
  }
  
  createSkill(): FormGroup {
    return this.fb.group({
      name: ['', Validators.required],
      years: [0, [Validators.required, Validators.min(0)]]
    });
  }
  
  addSkill() {
    this.skills.push(this.createSkill());
  }
  
  removeSkill(index: number) {
    this.skills.removeAt(index);
  }
  
  onSubmit() {
    if (this.form.valid) {
      console.log('Submitted:', this.form.value);
    }
  }
}

Form State & Validation

// Form control states
const control = this.form.get('email');

// Value
console.log(control?.value);

// Validity
console.log(control?.valid);      // true if valid
console.log(control?.invalid);    // true if invalid
console.log(control?.errors);     // validation errors object

// Touch state
console.log(control?.touched);    // true after blur
console.log(control?.untouched);  // opposite of touched
console.log(control?.dirty);      // true after value change
console.log(control?.pristine);   // opposite of dirty

// Programmatic updates
control?.setValue('new value');           // Set value
control?.patchValue('partial');           // Patch value
control?.markAsTouched();                 // Mark as touched
control?.markAsDirty();                   // Mark as dirty
control?.updateValueAndValidity();        // Recalculate validity

// Listen to value changes
control?.valueChanges.subscribe(value => {
  console.log('Value changed:', value);
});

// Listen to status changes
control?.statusChanges.subscribe(status => {
  console.log('Status changed:', status); // 'VALID', 'INVALID', 'PENDING'
});

// Disable/Enable
control?.disable();
control?.enable();

// Reset
this.form.reset();
this.form.reset({ email: 'default@example.com' });

// Patch form
this.form.patchValue({
  name: 'John',
  email: 'john@example.com'
});

// Set entire form value
this.form.setValue({
  name: 'John',
  email: 'john@example.com',
  address: {
    street: '123 Main St',
    city: 'NYC',
    zip: '10001'
  }
});

Template-Driven vs Reactive Forms

**Template-Driven**: Good for simple forms, less code, easier to understand. **Reactive**: Better for complex forms, easier to test, more control, dynamic validation, better for large-scale apps.

Angular

Directives & Pipes

Angular Directives & Pipes Directives and pipes are powerful features in Angular. Directives add behavior to elements, while pipes transform data in templates.

Angular Directives & Pipes

Directives and pipes are powerful features in Angular. Directives add behavior to elements, while pipes transform data in templates.

Built-in Directives

Structural Directives

<!-- *ngIf - conditional rendering -->
<div *ngIf="isVisible">Visible content</div>
<div *ngIf="user; else loading">Hello, {{ user.name }}</div>
<ng-template #loading>Loading...</ng-template>

<!-- *ngIf with as for variable assignment -->
<div *ngIf="user$ | async as user">
  {{ user.name }}
</div>

<!-- *ngFor - loop over collections -->
<ul>
  <li *ngFor="let item of items; let i = index; let first = first; let last = last; let even = even">
    {{ i + 1 }}. {{ item.name }}
    <span *ngIf="first">(First)</span>
    <span *ngIf="last">(Last)</span>
    <span *ngIf="even">(Even)</span>
  </li>
</ul>

<!-- *ngFor with trackBy for performance -->
<div *ngFor="let item of items; trackBy: trackByFn">
  {{ item.name }}
</div>

<!-- *ngSwitch - switch statement -->
<div [ngSwitch]="status">
  <p *ngSwitchCase="'loading'">Loading...</p>
  <p *ngSwitchCase="'success'">Success!</p>
  <p *ngSwitchCase="'error'">Error occurred</p>
  <p *ngSwitchDefault>Unknown status</p>
</div>

Attribute Directives

<!-- ngClass - dynamic classes -->
<div [ngClass]="'active'">Single class</div>
<div [ngClass]="['class1', 'class2']">Multiple classes</div>
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">Conditional classes</div>

<!-- ngStyle - dynamic styles -->
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">Styled text</div>
<div [ngStyle]="styleObject">Object styles</div>

<!-- ngModel - two-way binding -->
<input [(ngModel)]="name" />
<p>Hello, {{ name }}</p>

Custom Attribute Directives

import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core';

// Highlight directive
@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input() appHighlight = 'yellow';
  @Input() defaultColor = 'transparent';
  
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) { }
  
  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.appHighlight);
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(this.defaultColor);
  }
  
  private highlight(color: string) {
    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color);
  }
}

// Usage
<p appHighlight="lightblue">Hover over me!</p>
<p [appHighlight]="color" [defaultColor]="'white'">Dynamic color</p>

// Tooltip directive
@Directive({
  selector: '[appTooltip]',
  standalone: true
})
export class TooltipDirective {
  @Input() appTooltip = '';
  private tooltipElement: HTMLElement | null = null;
  
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) { }
  
  @HostListener('mouseenter') onMouseEnter() {
    this.showTooltip();
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.hideTooltip();
  }
  
  private showTooltip() {
    this.tooltipElement = this.renderer.createElement('span');
    const text = this.renderer.createText(this.appTooltip);
    this.renderer.appendChild(this.tooltipElement, text);
    this.renderer.appendChild(document.body, this.tooltipElement);
    this.renderer.addClass(this.tooltipElement, 'tooltip');
    
    // Position tooltip
    const hostPos = this.el.nativeElement.getBoundingClientRect();
    const top = hostPos.bottom + 10;
    const left = hostPos.left;
    
    this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
    this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
  }
  
  private hideTooltip() {
    if (this.tooltipElement) {
      this.renderer.removeChild(document.body, this.tooltipElement);
      this.tooltipElement = null;
    }
  }
}

Custom Structural Directives

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

// Unless directive (opposite of *ngIf)
@Directive({
  selector: '[appUnless]',
  standalone: true
})
export class UnlessDirective {
  private hasView = false;
  
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) { }
  
  @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;
    }
  }
}

// Usage
<p *appUnless="isHidden">Visible when isHidden is false</p>

// Repeat directive
@Directive({
  selector: '[appRepeat]',
  standalone: true
})
export class RepeatDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) { }
  
  @Input() set appRepeat(times: number) {
    this.viewContainer.clear();
    for (let i = 0; i < times; i++) {
      this.viewContainer.createEmbeddedView(this.templateRef, {
        $implicit: i,
        index: i
      });
    }
  }
}

// Usage
<p *appRepeat="3; let i = index">Item {{ i }}</p>

Built-in Pipes

<!-- String pipes -->
<p>{{ 'hello world' | uppercase }}</p>  <!-- HELLO WORLD -->
<p>{{ 'HELLO WORLD' | lowercase }}</p>  <!-- hello world -->
<p>{{ 'hello world' | titlecase }}</p>  <!-- Hello World -->

<!-- Number pipes -->
<p>{{ 12345.6789 | number }}</p>                    <!-- 12,345.679 -->
<p>{{ 12345.6789 | number:'1.2-4' }}</p>           <!-- 12,345.6789 -->
<p>{{ 0.259 | percent }}</p>                       <!-- 26% -->
<p>{{ 0.259 | percent:'1.2-2' }}</p>              <!-- 25.90% -->
<p>{{ 1234.56 | currency }}</p>                    <!-- $1,234.56 -->
<p>{{ 1234.56 | currency:'EUR' }}</p>             <!-- €1,234.56 -->
<p>{{ 1234.56 | currency:'USD':'symbol':'1.0-0' }}</p>  <!-- $1,235 -->

<!-- Date pipes -->
<p>{{ today | date }}</p>                          <!-- Dec 15, 2023 -->
<p>{{ today | date:'short' }}</p>                  <!-- 12/15/23, 3:30 PM -->
<p>{{ today | date:'fullDate' }}</p>               <!-- Friday, December 15, 2023 -->
<p>{{ today | date:'yyyy-MM-dd' }}</p>             <!-- 2023-12-15 -->
<p>{{ today | date:'hh:mm:ss a' }}</p>             <!-- 03:30:45 PM -->

<!-- JSON pipe -->
<pre>{{ user | json }}</pre>

<!-- Async pipe - unwraps observables/promises -->
<div *ngIf="user$ | async as user">
  {{ user.name }}
</div>

<!-- Slice pipe -->
<p>{{ [1,2,3,4,5] | slice:1:3 }}</p>  <!-- [2,3] -->
<p>{{ 'Hello World' | slice:0:5 }}</p>  <!-- Hello -->

<!-- KeyValue pipe -->
<div *ngFor="let item of object | keyvalue">
  {{ item.key }}: {{ item.value }}
</div>

Custom Pipes

import { Pipe, PipeTransform } from '@angular/core';

// Exponential pipe
@Pipe({
  name: 'exponential',
  standalone: true
})
export class ExponentialPipe implements PipeTransform {
  transform(value: number, exponent: number = 1): number {
    return Math.pow(value, exponent);
  }
}

// Usage: {{ 2 | exponential:3 }}  // 8

// Truncate pipe
@Pipe({
  name: 'truncate',
  standalone: true
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 50, trail: string = '...'): string {
    return value.length > limit ? value.substring(0, limit) + trail : value;
  }
}

// Usage: {{ longText | truncate:20:'...'  }}

// Filter pipe (impure pipe - use with caution)
@Pipe({
  name: 'filter',
  standalone: true,
  pure: false  // Impure pipe - runs on every change detection
})
export class FilterPipe implements PipeTransform {
  transform(items: any[], searchText: string, property: string): any[] {
    if (!items || !searchText) {
      return items;
    }
    
    return items.filter(item => 
      item[property].toLowerCase().includes(searchText.toLowerCase())
    );
  }
}

// Usage: <div *ngFor="let user of users | filter:searchText:'name'">

// Time ago pipe
@Pipe({
  name: 'timeAgo',
  standalone: true
})
export class TimeAgoPipe implements PipeTransform {
  transform(value: Date | string): string {
    const date = new Date(value);
    const now = new Date();
    const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
    
    const intervals: { [key: string]: number } = {
      year: 31536000,
      month: 2592000,
      week: 604800,
      day: 86400,
      hour: 3600,
      minute: 60,
      second: 1
    };
    
    for (const [name, count] of Object.entries(intervals)) {
      const interval = Math.floor(seconds / count);
      if (interval >= 1) {
        return interval === 1 
          ? `${interval} ${name} ago`
          : `${interval} ${name}s ago`;
      }
    }
    
    return 'just now';
  }
}

// Safe HTML pipe
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Pipe({
  name: 'safeHtml',
  standalone: true
})
export class SafeHtmlPipe implements PipeTransform {
  constructor(private sanitizer: DomSanitizer) {}
  
  transform(value: string): SafeHtml {
    return this.sanitizer.bypassSecurityTrustHtml(value);
  }
}

// Usage: <div [innerHTML]="htmlContent | safeHtml"></div>

Pure vs Impure Pipes

**Pure pipes** (default): Only re-run when input reference changes. More performant. **Impure pipes**: Run on every change detection cycle. Use sparingly as they can impact performance.

Angular

Change Detection & Performance

Angular Change Detection & Performance Understanding and optimizing change detection is crucial for building performant Angular applications. Angular uses Zone.

Angular Change Detection & Performance

Understanding and optimizing change detection is crucial for building performant Angular applications. Angular uses Zone.js to detect changes and update the view.

How Change Detection Works

Change detection is the mechanism Angular uses to keep the component tree in sync with the data model. When an event occurs (user input, HTTP response, timer), Angular checks if any data changed and updates the DOM if necessary.

import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';

// Default strategy - checks entire tree
@Component({
  selector: 'app-default',
  template: `
    <h2>{{ title }}</h2>
    <p>Count: {{ count }}</p>
    <button (click)="increment()">Increment</button>
  `
  // changeDetection: ChangeDetectionStrategy.Default  (default)
})
export class DefaultComponent {
  title = 'Default Strategy';
  count = 0;
  
  increment() {
    this.count++;
    // Angular automatically detects this change
  }
}

// OnPush strategy - only checks when inputs change or events occur
@Component({
  selector: 'app-onpush',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <h2>{{ title }}</h2>
    <p>Count: {{ count }}</p>
    <button (click)="increment()">Increment</button>
    <button (click)="updateAsync()">Update Async</button>
  `
})
export class OnPushComponent {
  @Input() data: any;
  title = 'OnPush Strategy';
  count = 0;
  
  constructor(private cdr: ChangeDetectorRef) {}
  
  // Button click triggers change detection
  increment() {
    this.count++;
  }
  
  // Async operation - need to manually trigger
  updateAsync() {
    setTimeout(() => {
      this.count++;
      this.cdr.markForCheck(); // Mark for check
    }, 1000);
  }
  
  // Or detect changes immediately
  forceUpdate() {
    this.count++;
    this.cdr.detectChanges(); // Run change detection now
  }
}

Change Detection Strategies

// Default: Checks component and all children on every change
@Component({
  changeDetection: ChangeDetectionStrategy.Default
})

// OnPush: Only checks when:
// 1. Input reference changes
// 2. Event handler fires
// 3. Observable emits (with async pipe)
// 4. Manually triggered (markForCheck/detectChanges)
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})

// Example: OnPush component
@Component({
  selector: 'app-user-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
  `
})
export class UserCardComponent {
  @Input() user!: User;
  
  // ❌ Won't trigger change detection
  updateWrong() {
    this.user.name = 'New Name';
    // Mutating input property - reference stays same
  }
  
  // ✅ Will trigger change detection
  updateCorrect() {
    this.user = { ...this.user, name: 'New Name' };
    // New reference created
  }
}

// Parent component
@Component({
  template: `<app-user-card [user]="currentUser"></app-user-card>`
})
export class ParentComponent {
  currentUser = { name: 'John', email: 'john@example.com' };
  
  // ❌ Won't trigger child change detection
  updateWrong() {
    this.currentUser.name = 'Jane';
  }
  
  // ✅ Will trigger child change detection
  updateCorrect() {
    this.currentUser = { ...this.currentUser, name: 'Jane' };
  }
}

Manual Change Detection Control

import { Component, ChangeDetectorRef, ApplicationRef } from '@angular/core';

@Component({
  selector: 'app-manual',
  template: `<p>{{ count }}</p>`
})
export class ManualComponent {
  count = 0;
  
  constructor(
    private cdr: ChangeDetectorRef,
    private appRef: ApplicationRef
  ) {}
  
  // Mark component and ancestors for check
  markForCheck() {
    this.count++;
    this.cdr.markForCheck();
  }
  
  // Run change detection immediately for this component
  detectChanges() {
    this.count++;
    this.cdr.detectChanges();
  }
  
  // Detach from change detection tree
  detach() {
    this.cdr.detach();
    // Component no longer checked automatically
  }
  
  // Reattach to change detection tree
  reattach() {
    this.cdr.reattach();
  }
  
  // Run change detection for entire application
  triggerAppCheck() {
    this.appRef.tick();
  }
}

Performance Optimization Techniques

TrackBy Function

@Component({
  template: `
    <!-- Without trackBy - recreates all DOM nodes on array change -->
    <div *ngFor="let item of items">
      {{ item.name }}
    </div>
    
    <!-- With trackBy - only updates changed items -->
    <div *ngFor="let item of items; trackBy: trackByFn">
      {{ item.name }}
    </div>
  `
})
export class ListComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];
  
  // TrackBy function
  trackByFn(index: number, item: any): any {
    return item.id; // Unique identifier
  }
  
  // Or track by index
  trackByIndex(index: number): number {
    return index;
  }
}

Pure Pipes for Expensive Operations

// Pure pipe - cached result
@Pipe({
  name: 'expensiveFilter',
  pure: true  // default
})
export class ExpensiveFilterPipe implements PipeTransform {
  transform(items: any[], searchText: string): any[] {
    console.log('Pipe running'); // Logs only when inputs change
    return items.filter(item => 
      item.name.toLowerCase().includes(searchText.toLowerCase())
    );
  }
}

// Usage
<div *ngFor="let item of items | expensiveFilter:searchText">
  {{ item.name }}
</div>

Lazy Loading & Code Splitting

// Lazy load modules
const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
  },
  {
    path: 'users',
    loadComponent: () => import('./users/user-list.component').then(m => m.UserListComponent)
  }
];

// Preloading strategies
import { PreloadAllModules, RouterModule } from '@angular/router';

RouterModule.forRoot(routes, {
  preloadingStrategy: PreloadAllModules  // Preload all lazy routes
});

Virtual Scrolling

import { ScrollingModule } from '@angular/cdk/scrolling';

@Component({
  standalone: true,
  imports: [ScrollingModule],
  template: `
    <cdk-virtual-scroll-viewport itemSize="50" style="height: 500px">
      <div *cdkVirtualFor="let item of items" class="item">
        {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `
})
export class VirtualScrollComponent {
  items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }));
}

Zone.js and NgZone

import { Component, NgZone } from '@angular/core';

@Component({
  selector: 'app-zone-demo'
})
export class ZoneDemoComponent {
  constructor(private ngZone: NgZone) {}
  
  // Run outside Angular zone - no change detection
  performHeavyTask() {
    this.ngZone.runOutsideAngular(() => {
      // Heavy computation or animation
      setInterval(() => {
        // This won't trigger change detection
        console.log('Running outside zone');
      }, 100);
    });
  }
  
  // Run inside Angular zone - triggers change detection
  updateUI() {
    this.ngZone.run(() => {
      // Update component data
      this.count++;
    });
  }
  
  // Handle events outside zone
  setupEventListener() {
    this.ngZone.runOutsideAngular(() => {
      document.addEventListener('mousemove', (event) => {
        // High-frequency event - outside zone for performance
        // Update when needed
        if (/* some condition */) {
          this.ngZone.run(() => {
            // Update component state
          });
        }
      });
    });
  }
}

Performance Best Practices

  • Use OnPush strategy for components with input properties

  • Use trackBy for *ngFor to avoid unnecessary DOM updates

  • Use pure pipes for expensive transformations

  • Lazy load modules and use code splitting

  • Use virtual scrolling for large lists

  • Run heavy computations outside Angular zone

  • Use async pipe for observables (auto unsubscribe)

  • Detach from change detection for static content

Keep your Angular knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever