Angular β
Zero to Interview
Ready
The complete Angular guide β from NgModules to Signals, from basic data binding to NgRx and zoneless change detection. Every topic asked in Indian interviews, from TCS & Infosys to Google, Flipkart, and Deutsche Bank tech teams.
Angular Architecture
Angular is a complete platform by Google β not just a library. It includes its own router, HTTP client, forms, DI system, build tools, and testing utilities. Understanding its architecture first makes everything else click.
Components & Templates
Components are the fundamental UI building blocks. Each consists of a TypeScript class, an HTML template, and optional CSS. The @Component decorator connects them and defines how the component appears in HTML.
// βββ Component decorator β connects everything ββββββββββββ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; @Component({ selector: 'app-user-card', // <app-user-card> in templates templateUrl: './user-card.html', // separate HTML file styleUrls: ['./user-card.css'], // component-scoped CSS standalone: true, // Angular 14+ β no NgModule needed }) export class UserCardComponent implements OnInit { // @Input β receive data FROM parent @Input() name!: string; // ! = definitely assigned (TypeScript) @Input() age = 0; // default value @Input({ required: true }) id!: string; // Angular 16+ required input // @Output β emit events TO parent (EventEmitter) @Output() selected = new EventEmitter<string>(); isVisible = true; ngOnInit(): void { console.log(`Component ready: ${this.name}`); } selectUser(): void { this.selected.emit(this.id); // notify parent } } // βββ Template (user-card.html) ββββββββββββββββββββββββββββ <!-- Property binding: [] binds to component property --> <!-- Event binding: () listens to DOM events --> <!-- Two-way binding: [()] = property + event combined -->
<!-- New Angular 17+ Control Flow Syntax (replaces *ngIf, *ngFor) --> @if (isVisible) { <div class="card"> <h2>{{ name }}</h2> <!-- interpolation --> <p [class.adult]="age >= 18">{{age}}</p> <!-- class binding --> <button (click)="selectUser()">Select</button> <!-- event binding --> </div> } @for (item of items; track item.id) { <!-- track = key prop equivalent --> <app-item [data]="item" /> } @switch (status) { @case ('active') { <span>Active</span> } @case ('idle') { <span>Idle</span> } @default { <span>Unknown</span> } } <!-- OLD way (still works, know both) --> <div *ngIf="isVisible">...</div> <li *ngFor="let item of items; trackBy: trackById">...</li>
Data Binding & Directives
| Binding Type | Syntax | Direction | Example |
|---|---|---|---|
| Interpolation | {{ expression }} | Component β DOM | <p>{{ user.name }}</p> |
| Property binding | [property]="value" | Component β DOM | [src]="imageUrl" |
| Event binding | (event)="handler()" | DOM β Component | (click)="submit()" |
| Two-way binding | [(ngModel)]="prop" | Both ways | [(ngModel)]="name" |
| Attribute binding | [attr.aria-label]="label" | Component β DOM attr | [attr.colspan]="span" |
| Class binding | [class.active]="isActive" | Component β CSS class | [class.error]="hasError" |
| Style binding | [style.color]="color" | Component β CSS style | [style.fontSize.px]="size" |
// βββ Structural Directives β change DOM structure βββββββββ // *ngIf, *ngFor, *ngSwitch (old syntax) // @if, @for, @switch (Angular 17+ new syntax β preferred) // βββ Attribute Directives β change element appearance βββββ // Built-in: ngClass, ngStyle <div [ngClass]="{ 'active': isActive, 'error': hasError }"></div> <div [ngStyle]="{ 'color': textColor, 'font-size': '14px' }"></div> // βββ Custom Directive βββββββββββββββββββββββββββββββββββββ @Directive({ selector: '[appHighlight]', standalone: true }) export class HighlightDirective { @Input() appHighlight = 'yellow'; // directive input = selector name constructor(private el: ElementRef) {} @HostListener('mouseenter') onMouseEnter() { this.el.nativeElement.style.background = this.appHighlight; } @HostListener('mouseleave') onMouseLeave() { this.el.nativeElement.style.background = ''; } } // Usage: <p appHighlight="cyan">Hover me</p>
* prefix (*ngIf, *ngFor) or the new @if/@for syntax. Attribute directives change the appearance or behavior of an existing element without adding/removing it β they modify attributes, classes, styles (ngClass, ngStyle, and custom directives). Both extend HTML β structural ones reshape the DOM tree, attribute ones modify existing nodes.NgModules & Standalone Components
Angular originally required NgModules to organize the app. Angular 14+ introduced Standalone Components β the modern approach. Both exist in codebases, so you must know both.
// βββ NgModule approach (classic β still in most codebases) β @NgModule({ declarations: [AppComponent, UserCardComponent], // components/directives/pipes imports: [BrowserModule, RouterModule, HttpClientModule], providers: [UserService], bootstrap: [AppComponent], // root component }) export class AppModule {} // βββ Standalone approach (Angular 14+ β modern, preferred) β @Component({ selector: 'app-root', standalone: true, // no NgModule needed imports: [RouterOutlet, UserCardComponent, AsyncPipe], // import directly template: `<router-outlet />`, }) export class AppComponent {} // βββ Bootstrap standalone app (main.ts) βββββββββββββββββββ bootstrapApplication(AppComponent, { providers: [ provideRouter(routes), provideHttpClient(), provideAnimations(), ] }); // No AppModule! Providers registered directly.
Most new Angular projects use Standalone Components. Legacy codebases still use NgModules. Angular's official recommendation since Angular 17 is to use standalone. If asked "NgModule vs Standalone" β standalone is simpler, no module overhead, better tree-shaking, aligns with the future direction of Angular.
Dependency Injection
Angular's DI system is one of its most powerful features β and the most asked about in senior interviews. Services are registered as providers and injected where needed. Understanding injection hierarchies is critical.
// βββ Basic service with inject() (modern, preferred) ββββββ @Injectable({ providedIn: 'root' }) // singleton across entire app export class UserService { private http = inject(HttpClient); // inject() β Angular 14+ modern way private router = inject(Router); getUsers() { return this.http.get<User[]>('/api/users'); } } // βββ Old constructor injection (still valid) βββββββββββββββ @Component({...}) export class AppComponent { constructor(private userService: UserService) {} // constructor injection } // βββ Injection hierarchies ββββββββββββββββββββββββββββββββ // providedIn: 'root' β ONE instance for entire app // providers: [MyService] in component β new instance for that component subtree // providers: [MyService] in module β shared within that module // βββ InjectionToken β for non-class values βββββββββββββββββ const API_URL = new InjectionToken<string>('API_URL'); // In providers: { provide: API_URL, useValue: 'https://api.example.com' } const apiUrl = inject(API_URL); // inject in component/service // βββ Provider types βββββββββββββββββββββββββββββββββββββββ // useClass: { provide: Logger, useClass: ConsoleLogger } β substitute class // useValue: { provide: API_URL, useValue: 'https://...' } β literal value // useFactory: { provide: X, useFactory: () => new X() } β factory function // useExisting: { provide: A, useExisting: B } β alias
providedIn: 'root' creates a singleton β ONE instance shared across the whole app. Every component/service gets the same instance. Providing in a component's providers: [] creates a NEW instance for that component and all its children, isolated from the rest. Use component-level when you want separate state per component instance (like a form service that tracks one form's state). The DI system checks the hierarchy: component injector β parent component β root injector.Lifecycle Hooks
Angular calls lifecycle hook methods at specific moments. Understanding the ORDER they fire and WHAT to do in each is a universal interview question.
| Hook | When it fires | Use for |
|---|---|---|
| ngOnChanges | Before ngOnInit AND when @Input changes | React to input changes, use SimpleChanges to compare |
| ngOnInit | Once after first ngOnChanges (component initialized) | Fetch data, initialize subscriptions, setup |
| ngDoCheck | Every change detection cycle | Custom change detection (rarely use this) |
| ngAfterContentInit | Once after <ng-content> projected | Access projected content via @ContentChild |
| ngAfterViewInit | Once after component view + children initialized | Access DOM elements via @ViewChild |
| ngAfterViewChecked | After every change detection of view | After view updates (careful β runs often) |
| ngOnDestroy | Just before component removed | Unsubscribe observables, cleanup timers, remove listeners |
export class MyComponent implements OnInit, OnDestroy, OnChanges { @Input() userId!: string; @ViewChild('myCanvas') canvas!: ElementRef; // available in ngAfterViewInit private destroy$ = new Subject<void>(); // for unsubscribing private userService = inject(UserService); ngOnChanges(changes: SimpleChanges) { if (changes['userId']) { // input changed this.loadUser(changes['userId'].currentValue); } } ngOnInit() { this.userService.getUser(this.userId) .pipe(takeUntil(this.destroy$)) // auto-unsubscribe! .subscribe(user => this.user = user); } ngAfterViewInit() { const ctx = this.canvas.nativeElement.getContext('2d'); // DOM ready } ngOnDestroy() { this.destroy$.next(); // complete all takeUntil subscriptions this.destroy$.complete(); } }
NOT unsubscribing from Observables in ngOnDestroy is the #1 source of memory leaks in Angular apps. Always use takeUntil(destroy$), the async pipe (auto-unsubscribes), or the DestroyRef inject pattern (Angular 16+): inject(DestroyRef).onDestroy(() => sub.unsubscribe()).
Services & HTTP Client
import { inject, Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; import { Observable, catchError, map, retry, throwError } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class ProductService { private http = inject(HttpClient); private baseUrl = 'https://api.example.com'; // GET with params getProducts(page = 1, limit = 20): Observable<Product[]> { const params = new HttpParams().set('page', page).set('limit', limit); return this.http.get<Product[]>(`${this.baseUrl}/products`, { params }).pipe( retry(2), // retry twice on failure catchError(this.handleError) // centralized error handling ); } // POST with typed body createProduct(product: Omit<Product, 'id'>): Observable<Product> { return this.http.post<Product>(`${this.baseUrl}/products`, product); } private handleError(err: HttpErrorResponse) { const msg = err.error?.message ?? err.statusText; return throwError(() => new Error(msg)); } } // βββ HTTP Interceptor (Angular 15+) βββββββββββββββββββββββ export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = localStorage.getItem('token'); const authReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req; return next(authReq); }; // Register: provideHttpClient(withInterceptors([authInterceptor]))
Routing & Guards
// βββ Route config (standalone app) βββββββββββββββββββββββ export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'user/:id', component: UserComponent }, // Lazy loading β module loaded on demand { path: 'admin', loadChildren: () => import('./admin/routes').then(m => m.adminRoutes), canActivate: [authGuard], // protect route }, // Lazy standalone component { path: 'dashboard', loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent), }, { path: '**', component: NotFoundComponent }, ]; // βββ Functional Guard (Angular 15+ preferred) βββββββββββββ export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); if (authService.isLoggedIn()) return true; router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); return false; }; // βββ Router hooks in component ββββββββββββββββββββββββββββ export class UserComponent { private route = inject(ActivatedRoute); private router = inject(Router); userId = this.route.snapshot.paramMap.get('id'); // static userId$ = this.route.paramMap.pipe(map(p => p.get('id'))); // reactive navigate() { this.router.navigate(['/users']); } }
Forms β Template-Driven & Reactive
Angular has two form approaches. Template-driven for simple forms. Reactive for complex, dynamic, and testable forms. Reactive forms are preferred in modern Angular and asked more in interviews.
import { FormBuilder, Validators, AbstractControl } from '@angular/forms'; export class LoginComponent { private fb = inject(FormBuilder); form = this.fb.group({ email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]], age: [null, [Validators.min(18), Validators.max(100)]], }); // Typed access (Angular 14+ β typed forms) get email() { return this.form.get('email')!; } onSubmit() { if (this.form.invalid) return; console.log(this.form.value); // { email: '...', password: '...' } } // βββ Custom validator βββββββββββββββββββββββββββββββββββββ noSpaces(control: AbstractControl) { return control.value?.includes(' ') ? { noSpaces: true } : null; } // βββ Cross-field validator (group level) ββββββββββββββββββ passwordMatch(group: AbstractControl) { const pass = group.get('password')?.value; const confirm = group.get('confirm')?.value; return pass === confirm ? null : { mismatch: true }; } // βββ Dynamic FormArray (add/remove items) βββββββββββββββββ addressForm = this.fb.group({ addresses: this.fb.array([]) }); get addresses() { return this.addressForm.get('addresses') as FormArray; } addAddress() { this.addresses.push(this.fb.group({ street: '', city: '' })); } }
RxJS Deep Dive
RxJS is the backbone of Angular. Observables, operators, and subjects are asked heavily in mid-to-senior interviews. The flattening operators (switchMap, mergeMap, concatMap, exhaustMap) are the most important to master.
import { Observable, Subject, BehaviorSubject, combineLatest, forkJoin, merge, switchMap, mergeMap, concatMap, exhaustMap, debounceTime, throttleTime, distinctUntilChanged, takeUntil, take, filter, map, catchError, retry, shareReplay } from 'rxjs'; // βββ Subjects βββββββββββββββββββββββββββββββββββββββββββββ // Subject: multicast, no initial value, only future values // BehaviorSubject: multicast, has initial value, new subscribers get CURRENT // ReplaySubject(n): multicast, replays last n values to new subscribers // AsyncSubject: emits ONLY the last value WHEN COMPLETED const user$ = new BehaviorSubject<User | null>(null); user$.next(loggedInUser); // push new value user$.getValue(); // synchronously get current value // βββ THE MOST IMPORTANT OPERATORS βββββββββββββββββββββββββ // switchMap β cancels previous inner observable when new outer value arrives // BEST for: search autocomplete, navigation searchInput$.pipe( debounceTime(300), // wait 300ms after last keystroke distinctUntilChanged(), // only if value actually changed switchMap(query => this.api.search(query)) // cancel old request! ); // concatMap β waits for previous to complete before next // BEST for: sequential API calls, form submissions clicks$.pipe(concatMap(() => this.api.save())); // save one at a time // mergeMap β runs all in parallel, results in any order // BEST for: parallel requests with no order dependency ids$.pipe(mergeMap(id => this.api.getItem(id))); // all at once // exhaustMap β ignores new outer values while inner is still active // BEST for: login button (ignore double-click) loginClicks$.pipe(exhaustMap(() => this.auth.login(creds))); // ignore extra clicks // βββ Combining observables ββββββββββββββββββββββββββββββββ combineLatest([user$, settings$]).pipe( // emits when ANY changes map(([user, settings]) => ({ ...user, ...settings })) ); forkJoin([api.getUsers(), api.getProducts()]); // wait for ALL to complete // βββ shareReplay β multicast + cache for late subscribers ββ readonly user$ = this.http.get('/api/me').pipe(shareReplay(1)); // Multiple async pipes β only ONE HTTP request!
NgRx State Management
NgRx is Redux for Angular β Actions, Reducers, Effects, Selectors. Used in large enterprise Angular apps. Senior interviews at product companies expect NgRx knowledge.
import { createAction, createReducer, createEffect, createSelector, createFeatureSelector, props, on } from '@ngrx/store'; import { Actions, ofType } from '@ngrx/effects'; // βββ 1. Actions βββββββββββββββββββββββββββββββββββββββββββ export const loadUsers = createAction('[Users] Load Users'); export const loadUsersSuccess = createAction('[Users] Load Success', props<{ users: User[] }>()); export const loadUsersFailure = createAction('[Users] Load Failure', props<{ error: string }>()); // βββ 2. Reducer βββββββββββββββββββββββββββββββββββββββββββ const reducer = createReducer( { users: [] as User[], loading: false, error: null as string | null }, on(loadUsers, state => ({ ...state, loading: true })), on(loadUsersSuccess, (state, { users }) => ({ ...state, loading: false, users })), on(loadUsersFailure, (state, { error }) => ({ ...state, loading: false, error })), ); // βββ 3. Selectors βββββββββββββββββββββββββββββββββββββββββ const selectUsersState = createFeatureSelector<UsersState>('users'); export const selectAllUsers = createSelector(selectUsersState, s => s.users); export const selectLoading = createSelector(selectUsersState, s => s.loading); // Selectors are MEMOIZED β only recompute when inputs change // βββ 4. Effects (side effects) ββββββββββββββββββββββββββββ @Injectable() export class UsersEffects { private actions$ = inject(Actions); private userService = inject(UserService); loadUsers$ = createEffect(() => this.actions$.pipe( ofType(loadUsers), switchMap(() => this.userService.getUsers().pipe( map(users => loadUsersSuccess({ users })), catchError(err => of(loadUsersFailure({ error: err.message }))) )) ) ); } // βββ 5. In Component ββββββββββββββββββββββββββββββββββββββ export class UsersComponent { private store = inject(Store); users$ = this.store.select(selectAllUsers); loading$ = this.store.select(selectLoading); loadUsers() { this.store.dispatch(loadUsers()); } }
Angular Signals
Signals are the most significant Angular feature in years β introduced in Angular 16, stable in 17. They replace many RxJS patterns for simple reactive state, enable fine-grained reactivity, and pave the way for zoneless Angular. This is THE hot interview topic in 2025.
import { signal, computed, effect, toSignal, toObservable } from '@angular/core'; @Component({ ... }) export class CounterComponent { // βββ signal() β reactive value ββββββββββββββββββββββββββββ count = signal(0); // create signal with initial value count(); // READ: call like a function this.count.set(5); // SET: replace value this.count.update(v => v + 1); // UPDATE: based on previous value this.count.mutate(arr => arr.push(1)); // MUTATE: for objects/arrays // βββ computed() β derived signal (memoized) βββββββββββββββ doubled = computed(() => this.count() * 2); // auto-recalculates isEven = computed(() => this.count() % 2 === 0); // βββ effect() β side effects when signals change ββββββββββ constructor() { effect(() => { console.log(`Count changed to ${this.count()}`); // auto-tracks all signals read inside this function }); } // βββ Signal inputs (Angular 17.1+) β replaces @Input ββββββ name = input<string>(''); // signal-based @Input required = input.required<string>(); // required signal input // Usage: <app-user [name]="'Rahul'" /> // βββ RxJS interop βββββββββββββββββββββββββββββββββββββββββ user$ = this.userService.getUser(1); // Observable user = toSignal(user$, { initialValue: null }); // Observable β Signal countSignal = signal(0); count$ = toObservable(countSignal); // Signal β Observable } // βββ In template β direct function call, NO async pipe needed // <p>{{ count() }}</p> (NOT {{ count | async }}) // <p>{{ doubled() }}</p>
Change Detection
Change detection is how Angular knows when to update the DOM. It's the most important performance concept in Angular and heavily asked in senior interviews. OnPush is the key optimization.
@Component({ selector: 'app-user-list', changeDetection: ChangeDetectionStrategy.OnPush, // β key optimization template: ` @for (user of users(); track user.id) { <app-user-item [user]="user" /> } `, }) export class UserListComponent { users = signal<User[]>([]); // signals work perfectly with OnPush // βββ Immutable updates for OnPush to detect change ββββββββ // β this.users.push(newUser) β same reference, OnPush won't detect // β this.users.update(u => [...u, newUser]) β new array reference private cdr = inject(ChangeDetectorRef); updateFromWebSocket(data: User[]) { this.users.set(data); // signals handle this automatically // Without signals: this.cdr.markForCheck(); } }
Lazy Loading, @defer & Optimization
// βββ Route-based lazy loading βββββββββββββββββββββββββββββ { path: 'reports', loadComponent: () => import('./reports.component').then(m => m.ReportsComponent) } // βββ @defer β Angular 17+ declarative lazy loading ββββββββ // In template: @defer (on viewport) { <!-- load when enters viewport --> <app-heavy-chart /> } @loading { <app-skeleton /> } @error { <p>Failed to load chart</p> } @placeholder { <!-- shown before defer triggers --> <div>Chart will appear here</div> } // Defer triggers: // on idle β when browser is idle // on viewport β when element enters viewport // on interaction β on click/hover // on timer(5000) β after 5 seconds // when condition β when expression is truthy // βββ trackBy equivalent in @for βββββββββββββββββββββββββββ @for (item of items; track item.id) { // track by unique property <app-item [data]="item" /> } // βββ Pure Pipe β cached transformation ββββββββββββββββββββ @Pipe({ name: 'filter', pure: true }) // only runs when INPUT changes export class FilterPipe implements PipeTransform { transform(items: any[], search: string) { return items.filter(i => i.name.includes(search)); } }
SSR, Built-in Pipes & Testing
// βββ Built-in Pipes (know all of these) βββββββββββββββββββ {{ 1234.56 | currency:'INR':'symbol':'1.2-2' }} // βΉ1,234.56 {{ today | date:'dd/MM/yyyy' }} // 25/02/2025 {{ text | uppercase }}{{ text | lowercase }} {{ 0.75 | percent }} // 75% {{ obj | json }} // debug {{ obs$ | async }} // subscribe + unsubscribe {{ items | slice:0:5 }} // first 5 items {{ key | keyvalue }} // iterate object as {key,value} // βββ Angular SSR (Angular Universal) βββββββββββββββββββββ // ng add @angular/ssr β adds server-side rendering // Renders Angular on Node.js server, sends HTML to browser // Benefits: SEO, faster LCP, better social sharing // isPlatformBrowser check β run browser code only on client: const isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); if (isBrowser) { localStorage.getItem('token'); } // safe! // βββ Angular Testing ββββββββββββββββββββββββββββββββββββββ describe('UserComponent', () => { let component: UserComponent; let fixture: ComponentFixture<UserComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserComponent, HttpClientTestingModule], providers: [{ provide: UserService, useValue: mockUserService }], }).compileComponents(); fixture = TestBed.createComponent(UserComponent); component = fixture.componentInstance; fixture.detectChanges(); // trigger ngOnInit }); it('should display user name', () => { component.user = { name: 'Rahul', id: '1' }; fixture.detectChanges(); const el = fixture.nativeElement.querySelector('h2'); expect(el.textContent).toContain('Rahul'); }); });
Complete Interview Question Bank
You're ready when you can explain all of these:
- Difference between @Component decorator options and what each property does
- Draw the Angular change detection tree and explain Default vs OnPush
- Write a complete NgRx feature (actions + reducer + effects + selectors)
- Explain switchMap vs mergeMap vs concatMap with real use cases
- Create a functional HTTP interceptor that adds auth token
- Implement a reactive form with custom validators and FormArray
- Explain Signals β signal(), computed(), effect() β and when to use vs RxJS
- Set up lazy loading and @defer blocks for performance
- Fix a memory leak in an Angular component with multiple subscriptions