Ch. 04

Angular

Components, dependency injection, change detection, signals and the RxJS operators that come up in every Angular round.

62 interview questions32 quiz questions3 notes
your progress0%

Notes in this chapter

Filter all notes →

Top 62 Angular interview questions most asked first

  1. 1.What is Angular, and what are the main building blocks of an Angular application?easy

    Angular is a TypeScript-based framework from Google for building single-page applications. Unlike a view library, it's batteries-included: the router, forms, HttpClient, dependency injection, testing utilities, i18n, SSR and the CLI all ship as first-party packages. It's a complete rewrite of AngularJS (1.x), not an upgrade of it.

    The main building blocks:

    • Components: a TypeScript class plus a template and styles. The UI is a tree of components.
    • Directives and pipes: add behavior to elements and format values in templates.
    • Services and dependency injection: shared logic and state, supplied by injectors.
    • Router: maps URLs to components, with lazy loading.
    • Change detection: keeps the DOM in sync with state, increasingly driven by signals.

    Templates are compiled ahead of time into efficient JavaScript instructions. Modern Angular is standalone-first: components declare their own imports, and the app starts with bootstrapApplication() instead of a root NgModule.

    What interviewers listen for
    • TypeScript framework for SPAs, batteries included
    • Components, directives, pipes, services with DI, router
    • Templates are compiled ahead of time
    • Standalone-first, with signals for reactivity
    • A rewrite of AngularJS, not a version bump

    Likely follow-up: What is the difference between a framework and a library like React?

  2. 2.What is a component in Angular, and what goes into the @Component decorator?easy

    A component is the basic UI building block: a TypeScript class that holds state and behavior, paired with a template that describes the DOM and optional styles. Angular creates an instance wherever its selector matches in a template.

    The @Component metadata tells the compiler how to treat the class:

    • selector: how it's used in templates, usually an element name like app-user-card.
    • template or templateUrl: the HTML.
    • styles or styleUrl: CSS, scoped to this component by default.
    • imports: the components, directives and pipes the template uses.
    • Optional settings such as providers, changeDetection, encapsulation and host bindings.

    In current Angular, components are standalone by default, so each one declares its own template dependencies instead of relying on an NgModule's declarations. Keep the class focused on presentation; data access and shared state belong in services.

    What interviewers listen for
    • Class for state and behavior, template for the view
    • selector, template, styles, imports are the core metadata
    • Standalone by default: imports lists template dependencies
    • Keep data access in services, not components

    Likely follow-up: What kinds of selectors can a component use?

  3. 3.What types of data binding does Angular support?easy

    Data flows between the class and the template in four ways:

    • Interpolation {{ expr }}: one-way, class to view. Renders the value as text.
    • Property binding [prop]="expr": one-way, class to view. Sets a DOM or component property, e.g. [disabled]="saving". Variants: [attr.aria-expanded] for attributes, [class.active] and [style.width.px].
    • Event binding (event)="handler($event)": one-way, view to class, for DOM events and component outputs.
    • Two-way binding [(value)]="x": sugar for a property binding plus a valueChange event binding, as used by [(ngModel)] or a model() input.

    A detail interviewers like: property binding targets DOM properties, not HTML attributes. Attributes only initialize properties, so an attribute with no matching property, such as data-*, is bound with [attr.name]. Template expressions should be cheap and side-effect free, because they're re-evaluated during change detection.

    <h2>{{ title }}</h2>
    <img [src]="avatarUrl" [alt]="name" />
    <button
      [attr.aria-expanded]="open"
      [class.active]="open"
      (click)="toggle()">
      Menu
    </button>
    <input [(ngModel)]="query" />
    What interviewers listen for
    • Interpolation and property binding: class to view
    • Event binding: view to class
    • Two-way [()] is property binding plus a Change event
    • Property bindings set DOM properties; use [attr.x] for attributes

    Likely follow-up: What is the difference between an HTML attribute and a DOM property?

  4. 4.What are directives, and what is the difference between structural and attribute directives?easy

    A directive is a class that adds behavior to elements in a template. There are three kinds:

    • Components: directives that have their own template.
    • Attribute directives change the appearance or behavior of the element they're on without adding or removing it: ngClass, ngStyle, ngModel, or a custom appHighlight.
    • Structural directives change the DOM layout by creating and destroying views: *ngIf, *ngFor, *ngSwitchCase. The asterisk is sugar: Angular wraps the element in an <ng-template>, and the directive uses TemplateRef and ViewContainerRef to decide when to render it.

    Only one structural directive can sit on an element; nest them with <ng-container>. In modern templates, the built-in control flow blocks @if, @for and @switch replace the common structural directives: they're part of the template syntax, need no import, and NgIf/NgFor are now deprecated. Custom structural directives are still useful for things like permission checks.

    What interviewers listen for
    • Components are directives with a template
    • Attribute directives modify an existing element
    • Structural directives add or remove DOM via ng-template
    • * is sugar for <ng-template>; one per element
    • @if/@for/@switch replace *ngIf/*ngFor/ngSwitch

    Likely follow-up: How would you write a custom structural directive?

  5. 5.What are the component lifecycle hooks, and in what order are they called?easy

    Lifecycle hooks are methods Angular calls at specific moments. After the constructor, the order is:

    • ngOnChanges: before ngOnInit when the component has bound inputs, then every time an input changes. Receives SimpleChanges.
    • ngOnInit: once, after the first inputs are set. Initialization logic goes here.
    • ngDoCheck: on every change detection run, for custom change checks.
    • ngAfterContentInit, then ngAfterContentChecked: projected content is initialized, then checked.
    • ngAfterViewInit, then ngAfterViewChecked: the component's view and its child views are initialized (view queries resolved), then checked.
    • ngOnDestroy: once, before the component is destroyed. Clean up here.

    On later passes only ngOnChanges (if an input changed), ngDoCheck and the two Checked hooks run again. Modern additions: inject(DestroyRef).onDestroy() registers cleanup from anywhere in an injection context, and afterNextRender()/afterEveryRender() are for DOM work and run only in the browser.

    What interviewers listen for
    • constructor, ngOnChanges, ngOnInit, ngDoCheck
    • Content hooks run before view hooks
    • Init hooks run once; Checked hooks every pass
    • ngOnDestroy or DestroyRef for cleanup
    • afterNextRender for browser-only DOM work

    Likely follow-up: Why would ngOnChanges not fire when you mutate an input object?

  6. 6.What is the difference between the constructor and ngOnInit?easy

    The constructor is a plain TypeScript class feature. Angular calls it when it instantiates the component, before any inputs are bound. It's the place to inject dependencies and set simple defaults, and nothing that depends on inputs: reading a required input() there throws, and a classic @Input() still holds its default or undefined.

    ngOnInit is an Angular lifecycle hook that runs once, after the first ngOnChanges, when input values are available. Initialization that depends on inputs belongs there, like starting a fetch based on an id input.

    Rules of thumb:

    • Keep constructors light. Heavy work there is harder to test and runs before Angular has finished setting the component up.
    • The view doesn't exist yet in either one; use ngAfterViewInit or afterNextRender() for DOM access.
    • With signals, many ngOnInit jobs become a computed() derived from inputs, or an effect() created in the constructor.
    What interviewers listen for
    • Constructor: class feature, runs before inputs are set
    • ngOnInit: Angular hook, runs once after first inputs
    • Constructor for injection; ngOnInit for input-dependent setup
    • Neither has the rendered view yet
  7. 7.What is dependency injection in Angular, and how does it work?mid

    Dependency injection is a pattern where a class declares what it needs and the framework supplies it, instead of the class constructing its dependencies itself. In Angular you request a token (usually a class) with inject(UserService) or a constructor parameter, and an injector finds a provider that says how to create the value.

    Providers are registered in a few places:

    • @Injectable({ providedIn: 'root' }) for an app-wide singleton.
    • The providers array of the ApplicationConfig, a route, or a component.

    Injectors form a hierarchy. Angular first walks the element injectors up the component tree, then the environment injectors (route, root, platform). The first provider found wins, and its injector owns the instance. If nothing is found, you get a no-provider error unless the dependency was marked optional.

    The payoff is loose coupling, easy testing (swap a real service for a fake in TestBed), and control over each dependency's scope and lifetime.

    What interviewers listen for
    • Classes ask for tokens; injectors resolve them via providers
    • Register with providedIn, app config, route or component providers
    • Lookup: element injectors first, then environment injectors
    • First provider found wins; missing provider throws
    • Improves testability and decoupling

    Likely follow-up: What does providedIn: 'root' do? · How can the same service end up with two instances?

  8. 8.What is a service in Angular, and what does providedIn: 'root' do?easy

    A service is a class for logic that doesn't belong to one component's view: fetching data, shared state, logging, auth. It's marked with @Injectable() so Angular can create it and inject its own dependencies.

    @Injectable({ providedIn: 'root' }) registers the service with the root environment injector, which gives you:

    • One instance for the whole app, shared by everything that injects it.
    • Lazy creation: it's instantiated the first time something injects it.
    • Tree-shaking: if nothing injects it, it's dropped from the bundle. A class listed in a providers array is always bundled.

    "Singleton" really means one instance per injector. If you also list the service in a component's providers, that component and its children get their own separate instance, and a route's providers does the same for that route's subtree. That's handy for deliberately scoped state, but it's also the usual cause of "my service state keeps resetting".

    What interviewers listen for
    • Service: injectable class for shared logic or state
    • providedIn: 'root': one lazy, tree-shakable app-wide instance
    • A singleton is only single per injector
    • Component or route providers create extra instances
  9. 9.What is the difference between an Observable and a Promise?easy
    • Values: a Promise settles once, with one value or an error. An Observable can emit zero, one or many values over time, then complete or error.
    • Laziness: a Promise's executor runs as soon as it's created. An Observable does nothing until you subscribe, and a cold Observable runs its producer again for each subscriber. That's why subscribing twice to http.get() sends two requests.
    • Cancellation: a Promise can't be cancelled. Unsubscribing from an Observable runs its teardown logic, and HttpClient aborts the request.
    • Timing: Promise callbacks always run asynchronously, as microtasks. Observables can emit synchronously (of(1)) or asynchronously.
    • Composition: Observables come with operators such as map, switchMap, retry and debounceTime.

    Angular uses Observables for HttpClient, router events and form valueChanges. To bridge the two, firstValueFrom() and lastValueFrom() turn an Observable into a Promise, and from(promise) goes the other way.

    What interviewers listen for
    • Promise: one value; Observable: zero to many over time
    • Promise is eager; Observable is lazy until subscribed
    • Observables can be cancelled by unsubscribing
    • Operators compose streams; firstValueFrom bridges to Promises

    Likely follow-up: What is the difference between a hot and a cold Observable?

  10. 10.How does change detection work in Angular?mid

    Change detection keeps the DOM in sync with component state. Each compiled template has an update function that re-evaluates its bindings, compares each result with the previous value (by identity, not deep equality), and touches only the DOM that changed. A pass walks the component tree top-down from the root, so parents are checked before their children.

    Two questions matter: when a pass runs, and which components it checks.

    • When: classically, Zone.js patched async APIs (events, timers, promises, XHR) and triggered ApplicationRef.tick() after each task. Zoneless apps, the default in recent versions, schedule a pass only when Angular is notified: a signal read in a template changes, a template listener fires, markForCheck() is called (the async pipe does this), or an input is set.
    • Which: the eager strategy checks every component on every pass. OnPush components are checked only when marked dirty, and signals let Angular refresh just the views that read the changed signal.

    In development mode Angular runs an extra check and throws NG0100 (ExpressionChangedAfterItHasBeenChecked) if a binding changed in between.

    What interviewers listen for
    • Re-evaluates template bindings and patches changed DOM
    • Top-down traversal; values compared by identity
    • Zone.js triggered passes; zoneless relies on notifications
    • OnPush and signals narrow which views get checked

    Likely follow-up: What does OnPush change? · What triggers change detection in a zoneless app?

  11. 11.How do Angular components communicate with each other?easy

    It depends on how the components are related:

    • Parent to child: inputs. The child declares input() (classic @Input()) and the parent binds [todo]="item".
    • Child to parent: outputs. The child declares output() (classic @Output() with an EventEmitter) and calls emit(); the parent listens with (toggled)="onToggle($event)".
    • Both ways: model() enables [(value)] two-way binding.
    • Parent calling into a child: a view query such as viewChild(TodoItem), or a template reference variable, gives the parent the child instance.
    • Siblings or unrelated components: a shared service that holds state in signals or a BehaviorSubject, provided at the right level.
    • Across routes: route params, query params and navigation state.

    Content projection with <ng-content> is the other composition tool: the parent passes markup instead of data. When lots of state is shared across a large app, a store (NgRx or a signal-based store) formalizes the service approach.

    What interviewers listen for
    • Parent to child: input() or @Input()
    • Child to parent: output() or @Output() events
    • Parent reaches a child instance with viewChild() or a template ref
    • Unrelated components share a service with signals or subjects

    Likely follow-up: When would you reach for a state management library instead of a service?

  12. 12.What are pipes, what is the difference between pure and impure pipes, and how do you create a custom one?easy

    Pipes transform values for display in templates: {{ price | currency:'EUR' }}, {{ createdAt | date:'short' }}. Built-ins include date, currency, decimal, percent, uppercase, titlecase, slice, json, keyvalue and async.

    • Pure pipes (the default) re-run transform only when the input value or an argument changes: a different primitive or a new object reference. Mutating an array in place doesn't re-run them. That makes them cheap, effectively memoized per binding.
    • Impure pipes (pure: false) run on every change detection pass, so they must be fast. async, json and keyvalue are impure because their output can change while the input reference stays the same.

    A custom pipe is a class decorated with @Pipe({ name }) that implements PipeTransform. Pipes are standalone by default, so you add them to a component's imports. Prefer a pure pipe (or a computed() signal) over calling a method in the template, which re-runs on every check.

    @Pipe({ name: 'truncate' })
    export class TruncatePipe implements PipeTransform {
      transform(value: string, limit = 20): string {
        return value.length > limit ? value.slice(0, limit) + '…' : value;
      }
    }
    
    // In a component with imports: [TruncatePipe]
    // {{ post.title | truncate:40 }}
    What interviewers listen for
    • Pipes format values in templates with |
    • Pure (default): re-run only when input or args change by reference
    • Impure: run every change detection pass (async, json)
    • Custom: @Pipe + PipeTransform, then add to imports

    Likely follow-up: Why does a pure pipe not update when you push to an array?

  13. 13.What are standalone components, and how do they differ from NgModules?easy

    NgModules were Angular's original unit of compilation. A module listed components in declarations, pulled in dependencies through imports, shared things with exports and registered providers. A component could only use what its module made available, which led to lots of indirection and "shared modules".

    A standalone component lists its own template dependencies in its imports array, so it's self-contained. It's the default in current Angular, and the rest of the app follows the same style:

    • Bootstrap with bootstrapApplication(App, appConfig) instead of bootstrapModule(AppModule).
    • Configure with provider functions such as provideRouter(routes) and provideHttpClient() instead of RouterModule.forRoot() and HttpClientModule.
    • Lazy load a single component with loadComponent.

    The benefits are less boilerplate, dependencies visible in one place, and simpler lazy loading and testing. The two styles interoperate: a standalone component can import an NgModule, and an NgModule can import a standalone component. The CLI ships a migration: ng generate @angular/core:standalone.

    What interviewers listen for
    • NgModules group declarations, imports, exports, providers
    • Standalone components declare their own imports
    • Standalone is the default in current Angular
    • bootstrapApplication and provide*() functions replace root modules
    • Both styles interoperate; the CLI can migrate
  14. 14.What does the OnPush change detection strategy do, and when does an OnPush component update?mid

    With ChangeDetectionStrategy.OnPush, Angular checks a component only when its view has been marked dirty. Otherwise the traversal skips it and, unless something inside was flagged, its whole subtree. A component is marked dirty when:

    • an input receives a new reference (via a template binding or setInput()),
    • an event handled by a template or host listener fires in the component or a descendant,
    • the async pipe receives a value,
    • markForCheck() is called,
    • a signal read in its template changes.

    What does not update it: mutating an input object in place, or assigning a plain field inside a setTimeout, a promise or a subscribe callback. That's why OnPush code uses immutable updates (spread, map, filter) and signals or the async pipe for async data.

    The payoff is fewer checks in large trees and a more predictable data flow. In the newest Angular versions OnPush is the default when changeDetection isn't set, and the check-always strategy is named Eager (previously Default).

    @Component({
      selector: 'app-user-card',
      changeDetection: ChangeDetectionStrategy.OnPush,
      template: `<h3>{{ user().name }}</h3>`,
    })
    export class UserCard {
      readonly user = input.required<User>();
    }
    
    // In the parent, bound as [user]="user":
    this.user.name = 'Grace';                    // same reference: card stays stale
    this.user = { ...this.user, name: 'Grace' }; // new reference: card updates
    What interviewers listen for
    • Checked only when marked dirty; clean subtrees are skipped
    • Dirty on new input reference, handled event, async pipe, signal
    • Mutating inputs in place does not update the view
    • Pairs with immutable data and signals

    Likely follow-up: How do you update an OnPush component from a setTimeout callback?

  15. 15.What is the difference between switchMap, mergeMap, concatMap and exhaustMap?mid

    All four map each source value to an inner Observable, subscribe to it and flatten its values into one stream. They differ only in what happens when a new value arrives while the previous inner Observable is still running:

    • switchMap cancels the previous inner and switches to the new one. Use it when only the latest result matters: typeahead search, loading details for the current route param.
    • mergeMap runs inners concurrently, optionally with a concurrency limit. Use it for independent parallel work where order doesn't matter.
    • concatMap queues values and runs inners one at a time, in order. Use it for sequential writes such as autosave.
    • exhaustMap ignores new values until the current inner completes. Use it to prevent double submits on a login or save button.

    Two points that impress interviewers: don't use switchMap for writes, because unsubscribing doesn't undo a request the server already received; and put catchError on the inner Observable, so one failed request doesn't kill the outer stream.

    results$ = this.query.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
    );
    
    saved$ = this.drafts$.pipe(concatMap((draft) => this.api.save(draft)));
    
    loggedIn$ = this.submit$.pipe(exhaustMap(() => this.auth.login(this.form.getRawValue())));
    What interviewers listen for
    • All flatten inner Observables; they differ on overlap
    • switchMap cancels the previous inner: typeahead, route params
    • mergeMap runs in parallel; concatMap queues in order
    • exhaustMap ignores new values while busy: double submits
    • Avoid switchMap for writes; catch errors inside the inner

    Likely follow-up: What happens to an HTTP request when switchMap unsubscribes from it?

  16. 16.What is the difference between Subject, BehaviorSubject, ReplaySubject and AsyncSubject?mid

    All four are both an Observable and an Observer, and they're multicast: every subscriber shares the same execution. The difference is what a late subscriber receives:

    • Subject: no initial value and no memory. Subscribers only get values emitted after they subscribe.
    • BehaviorSubject: requires an initial value and always holds the current one. A new subscriber immediately gets the latest value, and you can read it synchronously with .value or getValue(). The classic choice for state in services.
    • ReplaySubject(bufferSize, windowTime): no initial value; replays the last N values (optionally only those within a time window) to each new subscriber.
    • AsyncSubject: emits only its last value, and only when it completes. Subscribers that arrive after completion get that value too, a bit like a Promise.

    Expose subjects from services through asObservable() so consumers can't call next(). In modern Angular, a signal() often replaces a BehaviorSubject for synchronous state.

    const s = new Subject<number>();
    s.next(1);
    s.subscribe(console.log); // logs 2 only: 1 was emitted before subscribing
    s.next(2);
    
    const b = new BehaviorSubject(0);
    b.next(1);
    b.subscribe(console.log); // logs 1 immediately, then 2
    b.next(2);
    
    const r = new ReplaySubject<number>(2);
    r.next(1); r.next(2); r.next(3);
    r.subscribe(console.log); // logs 2, 3
    What interviewers listen for
    • Subject: late subscribers get only future values
    • BehaviorSubject: initial value, replays current value, .value
    • ReplaySubject: replays the last N values
    • AsyncSubject: emits the last value on completion
    • Expose via asObservable()

    Likely follow-up: When would you use a signal instead of a BehaviorSubject?

  17. 17.How do you set up routing in an Angular application?easy

    You define a Routes array and register it with provideRouter(routes) in the application config (classic apps use RouterModule.forRoot(routes)). Each route maps a path to a component or to a lazy loadComponent/loadChildren, and can have children, redirectTo, guards, resolvers and data.

    In templates:

    • <router-outlet /> marks where the routed component renders.
    • routerLink="/users" or [routerLink]="['/users', id]" navigates without a full page reload, and routerLinkActive adds a class to the active link.

    In code, inject(Router).navigate(['/users', id]) navigates, and ActivatedRoute exposes paramMap, queryParamMap and data, as Observables or through snapshot. With withComponentInputBinding(), params, query params and resolved data are bound straight to component inputs.

    Order matters: the router takes the first route that matches, so put specific paths before general ones and the ** wildcard last. An empty-path redirect needs pathMatch: 'full'.

    export const routes: Routes = [
      { path: '', redirectTo: 'users', pathMatch: 'full' },
      { path: 'users', component: UserList },
      { path: 'users/:id', component: UserDetail },
      { path: 'admin', loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES) },
      { path: '**', component: NotFound },
    ];
    
    bootstrapApplication(App, {
      providers: [provideRouter(routes, withComponentInputBinding())],
    });
    What interviewers listen for
    • Routes array registered with provideRouter()
    • <router-outlet> renders; routerLink navigates
    • ActivatedRoute or input binding for params
    • First match wins; wildcard ** goes last

    Likely follow-up: What is the difference between paramMap and snapshot.paramMap?

  18. 18.What is the difference between template-driven and reactive forms, and when would you use each?easy

    Both are built on the same model classes (FormControl, FormGroup), but they differ in where the form model lives.

    • Template-driven (FormsModule): you describe the form in the template with ngModel and validation attributes such as required and minlength, and directives create the controls implicitly. The data model is mutable and bound two-way, and setup is asynchronous because the controls are created as the view renders. Good for simple forms: login, a search box, a settings toggle.
    • Reactive (ReactiveFormsModule): you build the model explicitly in the class with FormGroup, FormControl, FormArray or FormBuilder, and bind it with [formGroup] and formControlName. Access is synchronous, values are strongly typed, valueChanges and statusChanges are Observables, and validators are plain functions. That makes them easier to unit test and better for dynamic or complex forms.

    Most teams default to reactive forms for anything non-trivial. Recent Angular versions also add Signal Forms, a signal-based API in @angular/forms/signals.

    What interviewers listen for
    • Template-driven: ngModel, model implicit in the template
    • Reactive: explicit model in the class, bound with [formGroup]
    • Reactive is synchronous, typed and easier to test
    • Template-driven suits simple forms; reactive suits complex ones

    Likely follow-up: How do you add a field dynamically in a reactive form?

  19. 19.What are route guards, and what types does Angular provide?mid

    Guards are functions the router runs to decide whether a navigation may proceed. Modern Angular uses functional guards, which can call inject():

    • canActivate: may the user enter this route? (auth, roles)
    • canActivateChild: the same check for every child route.
    • canDeactivate: may the user leave? Typically "you have unsaved changes".
    • canMatch: may this route definition be used at all? If it returns false, the router tries the next matching route, and a lazy route's code isn't downloaded. It replaces the deprecated canLoad.

    A guard returns true/false, a UrlTree or RedirectCommand to redirect, or an Observable or Promise of one of those. Returning a UrlTree is better than calling router.navigate() inside the guard, because the router handles the redirect as part of the same navigation.

    Class-based guards implementing CanActivate still work, but functions are simpler and composable. And guards are a UX feature, not security: the server must still enforce authorization.

    export const authGuard: CanActivateFn = () => {
      const auth = inject(AuthService);
      return auth.isLoggedIn() || inject(Router).createUrlTree(['/login']);
    };
    
    export const unsavedChangesGuard: CanDeactivateFn<EditPage> = (page) =>
      !page.dirty() || confirm('Discard unsaved changes?');
    
    export const routes: Routes = [
      { path: 'settings', component: EditPage, canActivate: [authGuard], canDeactivate: [unsavedChangesGuard] },
    ];
    What interviewers listen for
    • canActivate, canActivateChild, canDeactivate, canMatch
    • Functional guards use inject() for dependencies
    • Return boolean, UrlTree/RedirectCommand, or async versions
    • canMatch replaces canLoad and skips lazy downloads
    • Guards are UX; the server enforces security

    Likely follow-up: In what order do guards and resolvers run?

  20. 20.How does lazy loading work in Angular, and how do you lazy load a route?mid

    Lazy loading splits the app into chunks that are downloaded only when needed, which shrinks the initial bundle and speeds up the first load. The build creates a separate chunk for every dynamic import().

    For routes:

    • loadComponent: () => import('./settings/settings').then((m) => m.Settings) lazy loads one standalone component. If the file uses a default export, () => import('./settings/settings') is enough.
    • loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES) lazy loads a whole set of child routes (in NgModule apps, a routing module).

    Related tools:

    • Preloading: withPreloading(PreloadAllModules) downloads lazy chunks in the background after the app starts, so later navigations are instant.
    • canMatch guards keep a chunk from being downloaded for users who can't use it.
    • Route providers give a lazy section its own environment injector and scoped services.
    • @defer lazy loads parts of a template rather than whole routes.

    Don't statically import a lazy component elsewhere, or it ends up back in the main bundle.

    What interviewers listen for
    • Dynamic import() creates separate chunks
    • loadComponent for one component, loadChildren for route sets
    • Preloading downloads chunks in the background
    • canMatch prevents downloading unauthorized chunks
    • @defer lazy loads template sections

    Likely follow-up: How would you write a custom preloading strategy?

  21. 21.What are signals in Angular? Explain signal, computed and effect.mid

    A signal is a reactive wrapper around a value that notifies its consumers when the value changes. You read it by calling it, count(), which lets Angular track who depends on it.

    • signal(initial) creates a writable signal, changed with set() or update(fn). asReadonly() exposes a read-only view of it.
    • computed(() => ...) derives a read-only value. It's lazy and memoized: it recalculates only after a signal it read changes, and it tracks dependencies dynamically.
    • effect(() => ...) runs a side effect whenever the signals it read change. It runs at least once, runs asynchronously as part of change detection, needs an injection context, and is destroyed with the component or service that created it. Use it to sync with the outside world (storage, logging, a chart library), not to copy one signal into another; that's a computed().

    Signals compare with Object.is by default, so update immutably. Templates are reactive consumers too: a signal change refreshes only the views that read it, which makes OnPush and zoneless change detection practical. untracked() reads a signal without tracking it.

    export class Cart {
      readonly items = signal<Item[]>([]);
      readonly total = computed(() => this.items().reduce((sum, i) => sum + i.price, 0));
    
      constructor() {
        effect(() => localStorage.setItem('cart', JSON.stringify(this.items())));
      }
    
      add(item: Item) {
        this.items.update((list) => [...list, item]); // new array, not push()
      }
    }
    What interviewers listen for
    • signal() holds writable state; read it by calling it
    • computed() is lazy, memoized and read-only
    • effect() is for side effects, not for deriving state
    • Object.is equality by default: update immutably
    • Only views that read a changed signal are refreshed

    Likely follow-up: How do signals differ from RxJS Observables? · What is linkedSignal for?

  22. 22.What are HTTP interceptors, and how do you write one in modern Angular?mid

    An interceptor is middleware for HttpClient: every request passes through the chain on the way out, and every response on the way back. Typical uses are auth headers, logging, global error handling, retries, caching and loading indicators.

    In modern Angular an interceptor is a function of type HttpInterceptorFn. It receives the request and a next handler and returns next(req), optionally transformed with RxJS operators. It runs in an injection context, so it can call inject() (at the top, not inside async callbacks). Register it with provideHttpClient(withInterceptors([...])).

    Key details:

    • Requests are immutable: use req.clone({ setHeaders: {...} }) to change one.
    • Order matters: requests flow through interceptors in the listed order, responses in reverse.
    • HttpContext tokens pass per-request flags, such as "skip auth for this call".

    Class-based interceptors (implementing HttpInterceptor, registered with the HTTP_INTERCEPTORS multi-provider) still work, but need withInterceptorsFromDi().

    export const authInterceptor: HttpInterceptorFn = (req, next) => {
      const token = inject(AuthService).token();
      const router = inject(Router);
      const authReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;
    
      return next(authReq).pipe(
        catchError((err: HttpErrorResponse) => {
          if (err.status === 401) router.navigate(['/login']);
          return throwError(() => err);
        }),
      );
    };
    
    // app.config.ts: provideHttpClient(withInterceptors([authInterceptor]))
    What interviewers listen for
    • Middleware around every HttpClient request and response
    • Functional HttpInterceptorFn registered via withInterceptors
    • Requests are immutable: modify with req.clone()
    • Request order as listed; responses in reverse

    Likely follow-up: How would you retry failed requests or refresh an expired token in an interceptor?

  23. 23.How do you prevent memory leaks from RxJS subscriptions in Angular?mid

    A subscription to a long-lived Observable keeps running after the component is destroyed: its callback still fires and holds a reference to the component, so it can't be garbage collected. Typical culprits are interval, fromEvent, subjects in root services, store selectors, router events and valueChanges. HTTP calls complete after one response, so they leak less, but cancelling them on destroy still avoids work for a dead component.

    Options, most preferred first:

    • Don't subscribe manually: the async pipe and toSignal() both unsubscribe automatically.
    • takeUntilDestroyed() from @angular/core/rxjs-interop completes the stream when the component, directive or service is destroyed. It needs an injection context; in ngOnInit or later, pass it a DestroyRef.
    • take(1) or first() when you only need one value.
    • Classic: a destroy$ Subject used with takeUntil(this.destroy$) and completed in ngOnDestroy, or keeping the Subscription and calling unsubscribe().

    Put takeUntil or takeUntilDestroyed last in the pipe, so no later operator keeps an inner subscription alive.

    export class Dashboard implements OnInit {
      private readonly destroyRef = inject(DestroyRef);
      private readonly route = inject(ActivatedRoute);
    
      constructor() {
        interval(5000).pipe(takeUntilDestroyed()).subscribe(() => this.refresh());
      }
    
      ngOnInit() {
        this.route.paramMap
          .pipe(takeUntilDestroyed(this.destroyRef)) // not an injection context: pass DestroyRef
          .subscribe((params) => this.load(params.get('id')));
      }
    }
    What interviewers listen for
    • Long-lived subscriptions outlive the component and leak
    • Prefer async pipe or toSignal(): automatic cleanup
    • takeUntilDestroyed() needs an injection context or DestroyRef
    • Classic: takeUntil(destroy$) last in the pipe

    Likely follow-up: Do you need to unsubscribe from HttpClient calls?

  24. 24.What does the async pipe do, and why is it preferred over subscribing manually?easy

    The async pipe subscribes to an Observable (or a Promise) from the template and returns its latest value. It:

    • Unsubscribes automatically when the component is destroyed, or when the bound expression switches to a different Observable, so there's no leak.
    • Marks the component for check on every emission, so it works with OnPush components.
    • Returns null until the first value arrives.

    It's preferred because there's less boilerplate, no manual subscription management, and the data stays a declarative stream instead of being copied into mutable fields.

    The main pitfall: each | async creates its own subscription. Writing user$ | async three times against a cold HTTP Observable sends three requests. Subscribe once with @if (user$ | async; as user) or @let user = user$ | async;, or share the stream with shareReplay. In signal-based code, toSignal() does the same job from the class.

    What interviewers listen for
    • Subscribes in the template and returns the latest value
    • Unsubscribes automatically on destroy
    • Calls markForCheck(), so it works with OnPush
    • Each | async is a separate subscription: subscribe once
  25. 25.What happens when an Angular application starts? How is it bootstrapped?easy

    The CLI build uses main.ts as the entry point, and index.html contains the root element, such as <app-root>.

    In a standalone app, main.ts calls bootstrapApplication(App, appConfig). Angular then:

    • creates the platform and the root environment injector from the providers in appConfig (provideRouter, provideHttpClient and so on); providedIn: 'root' services are created on demand later,
    • runs app initializers registered with provideAppInitializer() and waits for any async ones,
    • creates the root component, renders it into the element that matches its selector, and starts change detection so later state changes reach the DOM.

    In NgModule apps, main.ts calls platformBrowser().bootstrapModule(AppModule) (older code uses platformBrowserDynamic()), and AppModule lists the root component in its bootstrap array.

    With SSR, the server renders the same app to HTML first, and the browser hydrates that markup instead of rendering from scratch.

    What interviewers listen for
    • main.ts calls bootstrapApplication(App, appConfig)
    • Root injector is built from appConfig providers
    • App initializers run before the root component renders
    • NgModule apps use bootstrapModule(AppModule)
  26. 26.What is Angular's built-in control flow (@if, @for, @switch), and how does it differ from *ngIf and *ngFor?easy

    Built-in control flow is template syntax for conditionals and loops that replaces the structural directives:

    • @if (cond) { } @else if (other) { } @else { }, with as to alias the value, e.g. @if (user$ | async; as user).
    • @for (item of items; track item.id) { } @empty { }, with the implicit variables $index, $count, $first, $last, $even and $odd.
    • @switch (status) { @case ('active') { } @default { } }.

    How it differs from *ngIf, *ngFor and ngSwitch:

    • It's built into the compiler, so there's nothing to import and less runtime overhead.
    • track is required in @for, which forces you to think about item identity; the old trackBy was optional and often forgotten.
    • @empty and @else if are built in, and type narrowing works better inside @if.
    • No <ng-template> or <ng-container> wrappers are needed.

    NgIf, NgFor and NgSwitch are deprecated, and the CLI can migrate templates with ng generate @angular/core:control-flow.

    @if (loading()) {
      <app-spinner />
    } @else {
      @for (user of users(); track user.id; let i = $index) {
        <p>{{ i + 1 }}. {{ user.name }}</p>
      } @empty {
        <p>No users found.</p>
      }
    }
    What interviewers listen for
    • @if, @for, @switch are built into the template syntax
    • No imports needed; less overhead than directives
    • track is required in @for; @empty is built in
    • NgIf/NgFor/NgSwitch are deprecated; CLI migration exists

    Likely follow-up: What should you pass to track, and what happens if you track by $index?

  27. 27.What are input(), output() and model(), and how do they compare with @Input() and @Output()?easy

    They're the signal-based way to declare a component's public API.

    • input<T>(defaultValue) and input.required<T>() return a read-only InputSignal. You read it as this.name() in the class and name() in the template, and derive from it with computed() instead of ngOnChanges. Options include alias and transform, e.g. transform: booleanAttribute.
    • output<T>() returns an OutputEmitterRef: call this.rated.emit(value), and the parent listens with (rated)="...". Outputs don't bubble like DOM events.
    • model<T>() is a writable input signal for two-way binding. When the component calls set() or update(), Angular emits a matching <name>Change event, so the parent can write [(value)]="score".

    The classic equivalents are @Input() name!: string, with ngOnChanges or a setter to react to changes, and @Output() rated = new EventEmitter<number>(). The new APIs add compile-time checking of required inputs, better type inference and built-in reactivity through computed and effect, and they fit naturally with OnPush and zoneless change detection.

    @Component({
      selector: 'app-rating',
      template: `
        @for (star of stars; track star) {
          <button [disabled]="disabled()" (click)="value.set(star); rated.emit(star)">★</button>
        }
      `,
    })
    export class Rating {
      readonly value = model(0); // parent: [(value)]="score"
      readonly disabled = input(false, { transform: booleanAttribute });
      readonly rated = output<number>();
      protected readonly stars = [1, 2, 3, 4, 5];
    }
    What interviewers listen for
    • input() returns a read-only signal; input.required() for mandatory
    • output() emits events with .emit(); no bubbling
    • model() is a writable input enabling [(x)]
    • Replace ngOnChanges with computed() on inputs

    Likely follow-up: What does an input transform do?

  28. 28.What is content projection, and how does <ng-content> work?mid

    Content projection lets a parent pass markup into a child component, much like children in React. The child's template marks where it goes with <ng-content>.

    • Single slot: <ng-content /> renders everything the parent put between the child's tags.
    • Multi-slot: <ng-content select="[card-title]" /> takes only matching content (a CSS selector on element, attribute or class), and a plain <ng-content /> catches the rest. ngProjectAs lets content match a different selector.
    • Fallback content: markup inside <ng-content> renders when nothing is projected into that slot.

    A key detail: projected content is created and owned by the parent. It's instantiated even if the child hides the <ng-content>, so wrapping it in @if doesn't prevent creation, and the same content can't be projected twice. For conditional, lazy or repeated rendering, the parent passes an <ng-template> and the child renders it with ngTemplateOutlet.

    The child reaches projected components with contentChild()/contentChildren() (classic @ContentChild), ready by ngAfterContentInit.

    @Component({
      selector: 'app-card',
      template: `
        <header><ng-content select="[card-title]" /></header>
        <section><ng-content /></section>
        <footer><ng-content select="app-card-actions">No actions</ng-content></footer>
      `,
    })
    export class Card {}
    
    // <app-card>
    //   <h2 card-title>Invoice #42</h2>
    //   <p>Due next week.</p>
    // </app-card>
    What interviewers listen for
    • <ng-content> is a slot for markup passed by the parent
    • select creates named slots; default slot catches the rest
    • Projected content is always created, even if hidden
    • Use ng-template + ngTemplateOutlet for conditional or repeated content

    Likely follow-up: How do you access a projected component from the child?

  29. 29.What is the difference between <ng-template>, <ng-container> and <ng-content>?mid
    • <ng-template> defines a template fragment that is not rendered on its own. Something has to instantiate it: a structural directive, ngTemplateOutlet, the else of *ngIf, or ViewContainerRef.createEmbeddedView(). It can declare context variables with let-, so one fragment can render with different data, and a template reference such as #row gives you its TemplateRef.
    • <ng-container> is a grouping element that renders no DOM node. Use it to apply a structural directive without a wrapper <div>, to nest structural directives (only one is allowed per element), or as the host for *ngTemplateOutlet.
    • <ng-content> is a projection slot in a child component's template, where the parent's markup is inserted.

    A common pattern combines them: a reusable list or table component accepts a TemplateRef (as an input, or through contentChild(TemplateRef)) so the parent decides how each item looks, and the component renders it with ngTemplateOutlet plus a context object.

    <ng-template #userRow let-user let-i="index">
      <li>{{ i + 1 }}. {{ user.name }}</li>
    </ng-template>
    
    <ul>
      @for (u of users(); track u.id; let i = $index) {
        <ng-container *ngTemplateOutlet="userRow; context: { $implicit: u, index: i }" />
      }
    </ul>
    What interviewers listen for
    • ng-template: a fragment rendered only when instantiated
    • let- variables receive values from the context object
    • ng-container: grouping without an extra DOM element
    • ng-content: slot for projected parent markup
  30. 30.What is the difference between ViewChild and ContentChild, and when are their results available?mid

    Both are queries that give a component a reference to something rendered in its tree. They differ in where they look:

    • View queries (viewChild(), viewChildren(), classic @ViewChild/@ViewChildren) search the component's own template.
    • Content queries (contentChild(), contentChildren(), classic @ContentChild/@ContentChildren) search the projected content the parent passed in through <ng-content>.

    You can query by template reference name ('box'), by component or directive class, or by token, and pick what you get back with read (ElementRef, TemplateRef, ViewContainerRef).

    Timing: with decorators, view results are ready in ngAfterViewInit and content results in ngAfterContentInit, or already in ngOnInit with static: true if the element isn't inside a conditional block. The plural forms return a QueryList with a changes Observable. Signal queries return signals that update automatically, so you read them in computed(), effect() or afterNextRender() without worrying about hook timing, and viewChild.required() removes the undefined case.

    @Component({
      selector: 'app-search',
      template: `<input #box /><ng-content />`,
    })
    export class Search {
      readonly box = viewChild.required<ElementRef<HTMLInputElement>>('box');
      readonly tabs = contentChildren(Tab); // projected <app-tab> components
    
      constructor() {
        afterNextRender(() => this.box().nativeElement.focus());
      }
    }
    What interviewers listen for
    • View queries search the own template
    • Content queries search projected content
    • Decorators: ready in ngAfterViewInit / ngAfterContentInit
    • Signal queries (viewChild()) update automatically
    • read chooses ElementRef, TemplateRef or ViewContainerRef
  31. 31.What is the difference between AOT and JIT compilation in Angular?easy

    Angular templates aren't run as HTML. The Angular compiler turns each template into JavaScript instructions that create and update the DOM. The question is when that happens.

    • JIT (just-in-time) compiles in the browser at runtime. The compiler (@angular/compiler) ships with the app, and templates are compiled as the app starts.
    • AOT (ahead-of-time) compiles during the build, so the browser downloads ready-to-run code.

    AOT advantages:

    • Faster startup: no compile step in the browser.
    • Smaller bundles: the compiler isn't shipped, and unused code tree-shakes better.
    • Errors at build time: broken bindings, unknown elements and, with strictTemplates, type errors in templates fail the build instead of the user's browser.
    • Security: templates aren't compiled from strings at runtime, which removes a class of template-injection risks.

    Since the Ivy compiler became standard, AOT is the default for both ng build and ng serve, so JIT is mostly a legacy concern today.

    What interviewers listen for
    • JIT compiles templates in the browser at runtime
    • AOT compiles templates during the build
    • AOT: faster startup, smaller bundles, no shipped compiler
    • AOT catches template errors at build time
    • AOT is the default for build and serve
  32. 32.How would you create a custom attribute directive, for example one that highlights an element on hover?mid

    Create a class with @Directive and an attribute selector such as [appHighlight]. Like components, directives are standalone by default, so a component adds it to imports to use it.

    • Take configuration through inputs. Giving an input the same alias as the selector lets you write appHighlight="lightblue".
    • React to events and update the host element with the host metadata, which angular.dev recommends over the classic @HostListener and @HostBinding decorators.
    • If you must touch the DOM directly, inject ElementRef and use Renderer2 rather than manipulating nativeElement yourself; it's safer with SSR.

    A structural directive is built differently: it injects TemplateRef and ViewContainerRef, and calls createEmbeddedView() or clear() to add or remove the template, for example an *appHasRole="'admin'" directive. Directives can also be composed onto components with hostDirectives.

    @Directive({
      selector: '[appHighlight]',
      host: {
        '(mouseenter)': 'hovered.set(true)',
        '(mouseleave)': 'hovered.set(false)',
        '[style.backgroundColor]': 'background()',
      },
    })
    export class Highlight {
      readonly color = input('', { alias: 'appHighlight' });
      protected readonly hovered = signal(false);
      protected readonly background = computed(() => (this.hovered() ? this.color() || 'yellow' : null));
    }
    // <p appHighlight="lightblue">Hover me</p>
    What interviewers listen for
    • @Directive with an attribute selector like [appHighlight]
    • Inputs for config; alias an input to the selector
    • host bindings and listeners instead of direct DOM access
    • Structural directives use TemplateRef + ViewContainerRef

    Likely follow-up: What are hostDirectives used for?

  33. 33.Which RxJS operators do you use most often in Angular, and what do they do?easy

    Operators are pure functions chained with pipe(). Each returns a new Observable and leaves the source untouched. The everyday set:

    • Transform: map changes each value; scan accumulates state, like reduce over time.
    • Filter: filter drops values; take(n) and first() complete after n values or the first match; distinctUntilChanged suppresses consecutive duplicates.
    • Timing: debounceTime(ms) emits only after the source has been quiet for that long; throttleTime limits the rate.
    • Side effects: tap for logging or setting a loading flag without changing the value; finalize runs on complete, error or unsubscribe.
    • Flattening: switchMap, mergeMap, concatMap, exhaustMap map values to inner Observables such as HTTP calls.
    • Combining: combineLatest, forkJoin, merge, withLatestFrom, startWith.
    • Errors: catchError to recover, retry to resubscribe.
    • Sharing: shareReplay to multicast and cache.

    The typeahead search is the classic example that uses most of them together.

    results$ = this.search.valueChanges.pipe(
      map((q) => q.trim()),
      filter((q) => q.length >= 2),
      debounceTime(300),
      distinctUntilChanged(),
      tap(() => this.loading.set(true)),
      switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
      tap(() => this.loading.set(false)),
    );
    What interviewers listen for
    • Operators are pure functions composed with pipe()
    • map, filter, tap for transform, filter, side effects
    • debounceTime + distinctUntilChanged for user input
    • switchMap family for HTTP; catchError/retry for errors

    Likely follow-up: What is the difference between debounceTime and throttleTime?

  34. 34.What is the difference between hot and cold Observables, and why does it matter in Angular?hard

    The difference is where the producer lives.

    • A cold Observable creates its producer inside subscribe, so every subscriber gets its own, independent execution from the beginning. HttpClient requests, of, timer, interval and defer are cold.
    • A hot Observable's producer exists independently of any one subscription. Subscribers share it and only receive values emitted after they subscribe. Subjects, DOM events and a shared WebSocket stream are hot.

    Why it matters:

    • Subscribing twice to http.get(), for example two | async pipes, sends two requests.
    • A late subscriber to a hot stream misses earlier values.

    To turn a cold Observable into a shared (multicast) one, use share(), where late subscribers only get future values, or shareReplay({ bufferSize: 1, refCount: true }), which also replays the latest value to late subscribers. That's the usual way to cache an HTTP result that several components read. A BehaviorSubject or a signal in a service is the other common way to share state.

    What interviewers listen for
    • Cold: producer per subscriber; each gets its own execution
    • Hot: shared producer; late subscribers miss earlier values
    • HTTP Observables are cold: two subscribes, two requests
    • share() / shareReplay() multicast a cold source
  35. 35.What is the difference between forkJoin and combineLatest?mid

    Both combine several Observables into one, but they emit at different times.

    • forkJoin waits for every source to complete, then emits once with the last value of each (as an array or an object) and completes. It's the RxJS version of Promise.all, ideal for parallel HTTP calls. If any source errors, it errors; if any source completes without emitting, it completes without emitting anything. Don't use it with streams that never complete, such as valueChanges.
    • combineLatest emits every time any source emits, once each source has emitted at least once, with the latest value from each. It's for derived state from long-lived streams, such as data plus filters. Seed slow sources with startWith if you need an early first emission.

    Related operators: zip pairs values by index, withLatestFrom samples other streams only when the main one emits, and merge interleaves values without combining them. In signal-based code, computed() often replaces combineLatest for synchronous state.

    What interviewers listen for
    • forkJoin: one emission after all sources complete
    • forkJoin is Promise.all for Observables: parallel HTTP
    • combineLatest: emits on every change once all have emitted
    • forkJoin never emits for never-completing streams

    Likely follow-up: When would you use withLatestFrom instead of combineLatest?

  36. 36.How do you write custom synchronous and asynchronous validators for reactive forms?mid

    A synchronous validator is a ValidatorFn: a function that takes an AbstractControl and returns null when valid, or an errors object such as { forbiddenValue: true }. Wrap it in a factory function when it needs parameters. For cross-field rules like "passwords match", put the validator on the FormGroup and read the child controls.

    An async validator (AsyncValidatorFn) returns an Observable or Promise of ValidationErrors | null, typically from an HTTP call such as "is this username taken?". Details worth knowing:

    • Async validators run only after all sync validators pass.
    • While one runs, the control's status is PENDING.
    • The Observable must complete; HTTP calls do, but for other streams add first().
    • Use updateOn: 'blur', or debounce inside the validator, to avoid a request per keystroke.

    Show errors with control.hasError('taken') or control.errors?.['taken']. In template-driven forms, the same logic is wrapped in a directive that provides NG_VALIDATORS or NG_ASYNC_VALIDATORS.

    export const forbiddenValue = (bad: string): ValidatorFn => (control) =>
      control.value === bad ? { forbiddenValue: { bad } } : null;
    
    export const passwordsMatch: ValidatorFn = (group) =>
      group.get('password')?.value === group.get('confirm')?.value ? null : { mismatch: true };
    
    export const usernameAvailable = (api: UserApi): AsyncValidatorFn => (control) =>
      api.isTaken(control.value).pipe(map((taken) => (taken ? { taken: true } : null)));
    
    username = new FormControl('', {
      validators: [Validators.required, forbiddenValue('admin')],
      asyncValidators: [usernameAvailable(inject(UserApi))],
      updateOn: 'blur',
    });
    What interviewers listen for
    • ValidatorFn returns null or an errors object
    • Cross-field validators go on the FormGroup
    • Async validators return an Observable or Promise and must complete
    • Async runs only after sync validators pass; status is PENDING
    • Throttle async checks with updateOn: 'blur'
  37. 37.What are the different provider types (useClass, useValue, useFactory, useExisting), and when do you need an InjectionToken?hard

    A provider tells an injector how to produce the value for a token:

    • useClass: instantiate a class, possibly a different one than the token, e.g. { provide: Logger, useClass: ConsoleLogger }. A bare providers: [Logger] is shorthand for this.
    • useValue: supply a ready-made value, such as a config object or a test double.
    • useFactory: call a function when the token is first requested. The factory can call inject() (classic code lists its dependencies in deps), which makes it right for values computed from the environment.
    • useExisting: an alias that points one token at another, so both return the same instance. useClass here would create a second one.
    • multi: true: several providers add to one token, and injecting it returns an array, as with NG_VALIDATORS.

    An InjectionToken is needed for anything that isn't a class, because TypeScript interfaces and types disappear at runtime. new InjectionToken<AppConfig>('APP_CONFIG') gives you a typed, unique token; the string is only a debug label. A token can also carry a tree-shakable default with { providedIn: 'root', factory: () => ... }.

    export const API_URL = new InjectionToken<string>('API_URL', {
      providedIn: 'root',
      factory: () => '/api', // default when nobody provides it
    });
    
    providers: [
      { provide: Logger, useClass: environment.production ? RemoteLogger : ConsoleLogger },
      { provide: API_URL, useValue: 'https://api.example.com' },
      { provide: STORAGE, useFactory: () => (isPlatformBrowser(inject(PLATFORM_ID)) ? localStorage : memoryStorage) },
      { provide: AuditLogger, useExisting: Logger },
    ]
    
    const apiUrl = inject(API_URL); // typed as string
    What interviewers listen for
    • useClass instantiates, useValue supplies a value
    • useFactory computes the value and can call inject()
    • useExisting aliases the same instance
    • InjectionToken for non-class values like config
    • multi: true collects several providers into an array
  38. 38.Explain hierarchical injectors in Angular. How does Angular decide which instance of a service a component gets?hard

    Angular has two injector trees:

    • Element injectors follow the component tree. A component or directive with providers (or viewProviders) gets one on its host element.
    • Environment injectors hold app-wide services: the root injector (providedIn: 'root' plus the providers passed to bootstrapApplication), child injectors for routes with providers and lazy-loaded routes, then the platform injector, and finally the NullInjector, which throws.

    When a component injects a token, Angular walks up the element injectors from the requesting element, then up the environment injectors. The first provider found wins, and that injector owns the instance.

    Consequences:

    • Component providers create a new instance per component instance, shared by its descendants and destroyed with it. Good for per-widget state such as an editor or a wizard.
    • Route providers share one instance across that route's subtree.
    • viewProviders hides the service from projected content.
    • Resolution modifiers adjust the search: optional returns null instead of throwing, self looks only locally, skipSelf starts at the parent, and host stops at the host component.
    What interviewers listen for
    • Element injectors follow the component tree
    • Environment injectors: route, root, platform, then NullInjector
    • Nearest provider wins and owns the instance
    • Component providers: one instance per component
    • Modifiers: optional, self, skipSelf, host

    Likely follow-up: What is the difference between providers and viewProviders?

  39. 39.What is the inject() function, how does it differ from constructor injection, and what is an injection context?hard

    inject(Token) resolves a dependency from the current injector, exactly like a constructor parameter, but as a function call. Both go through the same injectors, so the result is the same; the differences are about where you can use it.

    inject() only works inside an injection context:

    • a constructor or a field initializer of a class Angular creates (component, directive, pipe, service),
    • a provider useFactory or an InjectionToken factory,
    • functional guards, resolvers and interceptors,
    • code wrapped in runInInjectionContext(injector, fn).

    Calling it anywhere else, such as in a click handler, ngOnInit or a setTimeout, throws NG0203. Inject in a field and use the value later.

    Why it's now preferred:

    • It works in functions, which is what made functional guards and interceptors possible.
    • Inheritance is simpler: subclasses don't have to forward dependencies to super().
    • Options are typed: inject(Logger, { optional: true }) has the type Logger | null.
    • You can build reusable helpers that call inject() internally.
    What interviewers listen for
    • Same resolution as constructor injection, as a function
    • Only valid in an injection context, else NG0203
    • Enables functional guards, resolvers, interceptors
    • Simplifies inheritance; typed options object
    • runInInjectionContext for code outside those contexts
  40. 40.What is ChangeDetectorRef, and what is the difference between markForCheck() and detectChanges()?hard

    ChangeDetectorRef gives a component manual control over its own change detection. Its main methods:

    • markForCheck() marks this view and all its ancestors dirty, so they're checked in the next pass. It doesn't run a check itself; in a zoneless app it also schedules that next pass. It's the fix when an OnPush component assigns a plain field in a callback Angular doesn't know about. The async pipe calls it for you.
    • detectChanges() checks this view and its children synchronously, right now. Use it with detach(), or in tests. Reaching for it to silence NG0100 errors is usually a sign of broken data flow.
    • detach() removes the view from the tree so passes skip it; reattach() puts it back. That suits widgets that refresh on their own schedule, like a high-frequency ticker.
    • checkNoChanges() is the dev-mode verification check.

    In modern code you rarely need any of these: signals, the async pipe and toSignal() notify Angular automatically. Turning that plain field into a signal removes the need for markForCheck().

    What interviewers listen for
    • markForCheck(): marks view and ancestors for the next pass
    • detectChanges(): checks this subtree synchronously now
    • detach()/reattach() take a view out of normal checking
    • Signals and the async pipe make manual calls rare
  41. 41.How does two-way binding work under the hood, and how do you make your own component support [( )] syntax?mid

    The "banana in a box" syntax is pure sugar. [(size)]="fontSize" compiles to a property binding plus an event binding:

    [size]="fontSize" (sizeChange)="fontSize = $event"

    So any component with an input x and an output named xChange supports two-way binding. The data flow is still one-way in each direction: the value goes down through the input, and changes come back up through the event.

    • Modern: model() declares both at once. size = model(16) is a writable signal; calling this.size.set(20) updates it locally and emits sizeChange. The parent can bind a plain field or a writable signal, passing the signal itself without calling it: [(size)]="fontSize".
    • Classic: @Input() size plus @Output() sizeChange = new EventEmitter<number>(), emitting manually.

    [(ngModel)] follows the same pattern: [ngModel] plus (ngModelChange). Splitting it is handy when you want to transform the value, e.g. (ngModelChange)="name = $event.trim()".

    What interviewers listen for
    • [(x)] is [x] plus (xChange)
    • Any input x with an output xChange supports it
    • model() declares the input and Change output together
    • Parent can bind a writable signal directly
  42. 42.What is a route resolver, and what are the trade-offs of using one?mid

    A resolver loads data before a route activates, so the component renders with its data already available. In modern Angular it's a function of type ResolveFn<T> that can call inject() and returns a value, a Promise or an Observable. You register it under the route's resolve key.

    How it behaves:

    • Resolvers run after all guards have passed.
    • The router takes the first value an Observable emits. If it completes without emitting, the navigation is cancelled.
    • If it errors, the navigation fails, so catch errors and return a fallback or a RedirectCommand to a "not found" page.
    • The component reads the result from ActivatedRoute.data, or directly as an input with withComponentInputBinding().

    The trade-off is perceived performance: nothing on screen changes until the data arrives, so the app can feel frozen on slow connections. Show a global progress bar from router events, or load the data in the component instead (with httpResource() or an Observable) and render a skeleton. Resolvers fit best when the page is meaningless without its data.

    What interviewers listen for
    • Loads data before the route activates
    • Functional ResolveFn with inject(); runs after guards
    • Uses the first emitted value; no value cancels navigation
    • Read via ActivatedRoute.data or component inputs
    • Trade-off: navigation waits, so the UI can feel stuck
  43. 43.What are @defer blocks, and how do they work?mid

    @defer lazy loads a section of a template. The components, directives and pipes used only inside the block are split into a separate chunk and loaded when a trigger fires, which keeps them out of the initial bundle.

    Triggers:

    • on idle (the default): when the browser is idle.
    • on viewport, on interaction, on hover: based on the placeholder or a referenced element.
    • on immediate, on timer(2s), or when condition for a custom expression.
    • prefetch on ... downloads the code earlier than it's rendered.

    Companion blocks: @placeholder shows before loading starts (with an optional minimum display time), @loading shows while the chunk downloads (after and minimum prevent flicker), and @error shows if loading fails.

    Gotchas: dependencies must be standalone, and if they're referenced anywhere else in the same file (another part of the template, or a viewChild query) they're bundled eagerly. During SSR, the server renders the @placeholder. With incremental hydration, hydrate on triggers let the server render the real content and hydrate it later.

    @defer (on viewport; prefetch on idle) {
      <app-comments [postId]="postId()" />
    } @placeholder (minimum 300ms) {
      <p>Comments</p>
    } @loading (after 100ms; minimum 500ms) {
      <app-spinner />
    } @error {
      <p>Could not load comments.</p>
    }
    What interviewers listen for
    • Lazy loads template dependencies into a separate chunk
    • Triggers: idle (default), viewport, interaction, hover, timer, when
    • @placeholder, @loading, @error sub-blocks
    • Dependencies must be standalone and not used elsewhere eagerly
    • SSR renders the placeholder unless using incremental hydration

    Likely follow-up: When would you pick @defer over lazy loading a route?

  44. 44.What is view encapsulation in Angular, and what are the available modes?mid

    View encapsulation controls whether a component's styles stay inside that component. It's set with the encapsulation option:

    • ViewEncapsulation.Emulated (the default): Angular adds generated attributes to the component's elements (like _ngcontent-... and _nghost-... on the host) and rewrites its CSS selectors to include them. Styles don't leak out to other components, but global styles still apply inside.
    • ViewEncapsulation.ShadowDom: uses the browser's native Shadow DOM, so isolation works in both directions. Only inherited properties and CSS custom properties cross the boundary.
    • ViewEncapsulation.None: no scoping; the styles become global once the component loads.

    Related selectors: :host styles the component's own host element, and :host-context(.dark) applies styles based on an ancestor. ::ng-deep pushes a rule into child components; it's deprecated but still supported. For theming children, CSS custom properties are the cleaner option, since they pass through every encapsulation mode.

    What interviewers listen for
    • Emulated (default): attribute-scoped CSS, styles do not leak out
    • ShadowDom: native isolation in both directions
    • None: component styles become global
    • :host styles the host; ::ng-deep is deprecated
    • CSS custom properties theme across boundaries
  45. 45.What are template reference variables, and what can they refer to?easy

    A template reference variable is declared with #name on an element, and gives you a handle you can use elsewhere in the same template. What it refers to depends on where it's placed:

    • On a plain element: the DOM element, e.g. an HTMLInputElement.
    • On a component: the component instance, so the template can call its public methods.
    • With a directive's exportAs name: that directive instance, e.g. #f="ngForm" or #m="matMenu".
    • On an <ng-template>: its TemplateRef.

    Common uses are reading an input's value without binding it to a field, letting one element control another, and naming elements for viewChild('name').

    Scope: a variable is visible throughout its template, but one declared inside an @if or @for block (or an <ng-template>) isn't visible outside that block. For values computed in the template, @let declares a local variable instead.

    <input #box placeholder="New todo" (keyup.enter)="add(box.value)" />
    <button (click)="add(box.value)">Add</button>
    
    <app-video-player #player src="intro.mp4" />
    <button (click)="player.play()">Play</button>
    
    <form #f="ngForm" (ngSubmit)="save(f.value)">
      <input name="email" ngModel required />
      <button [disabled]="f.invalid">Save</button>
    </form>
    What interviewers listen for
    • Declared with #name in the template
    • Element: DOM node; component: its instance
    • #f="ngForm" binds a directive via exportAs
    • On ng-template it is a TemplateRef
    • Not visible outside the block it was declared in
  46. 46.How would you optimize the performance of a slow Angular application?hard

    First measure: the Angular DevTools profiler shows which components are checked and how long each pass takes, Lighthouse covers load metrics, and bundle analysis shows what's in each chunk. Then work on two fronts.

    Load time

    • Lazy load routes with loadComponent/loadChildren, and heavy template sections with @defer.
    • Enforce bundle budgets in angular.json, and keep large libraries out of the main chunk.
    • Use SSR or prerendering with hydration for a faster first paint.
    • Use NgOptimizedImage (ngSrc, plus priority on the LCP image) for sizing and lazy loading of images.

    Runtime

    • OnPush and signals, so change detection only touches affected views. Zoneless removes Zone.js overhead.
    • A stable track expression in @for (trackBy for *ngFor), so DOM nodes are reused instead of recreated.
    • Pure pipes or computed() instead of method calls in templates, which re-run on every check.
    • Virtual scrolling (CDK cdk-virtual-scroll-viewport) for long lists.
    • Debounce noisy input, cancel stale requests with switchMap, and clean up subscriptions.
    • In Zone.js apps, run high-frequency work outside Angular with NgZone.runOutsideAngular().
    What interviewers listen for
    • Measure first: DevTools profiler, Lighthouse, bundle analysis
    • Lazy load routes and @defer heavy sections
    • OnPush, signals, zoneless to reduce change detection work
    • track in @for; no method calls in templates
    • Virtual scrolling, NgOptimizedImage, SSR

    Likely follow-up: What does the track expression in @for actually do?

  47. 47.How do you handle errors in an Angular application, both for HTTP calls and globally?mid

    Errors are handled at several levels:

    • Per request: HttpClient errors with an HttpErrorResponse. status holds the HTTP status code, and 0 usually means a network or CORS failure. Use catchError to return a fallback (of([])) or rethrow a friendlier error with throwError(() => ...). Inside a switchMap, catch on the inner Observable so the outer stream survives.
    • Retries: retry({ count: 2, delay: 1000 }) resubscribes, which re-sends the request. Use it for transient failures and idempotent requests, not blindly for writes.
    • Cross-cutting: an interceptor centralizes behavior like redirecting to login on 401 or showing a toast.
    • Global: uncaught errors are sent to ErrorHandler, which logs to the console by default. Provide your own implementation to report to a monitoring service. provideBrowserGlobalErrorListeners() also forwards the window's error and unhandledrejection events to it.

    With signals, toSignal() rethrows the Observable's error when the signal is read, and resource() exposes an error() signal and an 'error' status for the template to render.

    What interviewers listen for
    • catchError with a fallback or a rethrow; catch inside inner streams
    • retry with count and delay for transient failures
    • Interceptors centralize cross-cutting error handling
    • Custom ErrorHandler for global reporting
  48. 48.When would you use signals and when would you use RxJS Observables in an Angular app?mid

    They solve different problems, and they interoperate.

    Signals model state: a value that always exists and changes over time. Reads are synchronous, computed() derivations are memoized and glitch-free, there's no subscription to manage, and templates that read them get fine-grained change detection. Use them for component state, derived values, inputs and shared state in services.

    Observables model events and async streams: things that happen over time, possibly with no current value. They shine at time and concurrency logic: debouncing, cancellation with switchMap, retries, combining several async sources, WebSockets. HttpClient, the router and reactive forms all expose Observables.

    A common split is RxJS at the edges for async orchestration and signals for the state the template reads. Bridge the two with toSignal() and toObservable(), and use resource() or httpResource() for signal-driven data fetching.

    Signals don't replace all of RxJS: a signal has no notion of completion or error events, and if you set it twice synchronously, consumers only see the last value.

    What interviewers listen for
    • Signals: synchronous state and derived values
    • Observables: events, async streams, time and concurrency
    • Signals need no subscriptions and give fine-grained updates
    • Bridge with toSignal() / toObservable()

    Likely follow-up: How does toSignal() handle errors from the Observable?

  49. 49.What role does Zone.js play in Angular, and what changes with zoneless change detection?hard

    Zone.js monkey-patches the browser's async APIs (addEventListener, setTimeout, Promise, XHR and more), so Angular hears about every task that runs. When a task finishes, NgZone triggers ApplicationRef.tick(), a change detection pass from the root. That's why mutating any field in any callback "just works".

    The costs: extra bundle size and startup work, a pass after every task even if nothing changed (a mousemove handler, a polling timer), noisier stack traces, and native async/await can't be patched, so the CLI has to downlevel it.

    Zoneless change detection drops Zone.js. Angular schedules a pass only when it's notified: a signal read in a template changes, a template or host listener runs, markForCheck() is called (the async pipe does this), ComponentRef.setInput() runs, or a view is attached or removed. Recent Angular versions are zoneless by default; older apps opt in with provideZonelessChangeDetection() and remove zone.js from the polyfills.

    Migration checklist: keep async state in signals or the async pipe, make components OnPush-compatible, replace NgZone.onStable with afterNextRender(), and use PendingTasks so SSR waits for async work.

    What interviewers listen for
    • Zone.js patches async APIs and triggers tick()
    • Costs: bundle size, unnecessary passes, async/await downleveling
    • Zoneless: signals, listeners, markForCheck, setInput notify
    • Plain field changes in callbacks no longer render
    • provideZonelessChangeDetection(); default in recent versions
  50. 50.How do toSignal() and toObservable() work, and what should you watch out for?hard

    Both come from @angular/core/rxjs-interop and bridge the two reactive models.

    toSignal(obs$):

    • subscribes immediately (not lazily on first read) and exposes the latest value as a read-only signal;
    • unsubscribes automatically when its injection context (component, directive, service) is destroyed, so it must be called in an injection context or be given an injector;
    • is undefined until the first emission, unless you pass initialValue, or requireSync: true for sources that emit synchronously such as a BehaviorSubject;
    • rethrows the Observable's error whenever the signal is read, so handle errors in the pipe.

    toObservable(sig):

    • pushes values through an effect, so emissions are asynchronous, and several synchronous set() calls collapse into a single emission of the latest value;
    • also needs an injection context.

    The classic pattern is signal, then toObservable, then RxJS operators such as debounceTime and switchMap, then toSignal for the template. For "refetch when a signal changes", rxResource() or httpResource() express it more directly.

    export class UserSearch {
      private readonly api = inject(UserApi);
      readonly query = signal('');
    
      readonly results = toSignal(
        toObservable(this.query).pipe(
          debounceTime(300),
          distinctUntilChanged(),
          switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
        ),
        { initialValue: [] },
      );
    }
    What interviewers listen for
    • toSignal subscribes immediately and cleans up on destroy
    • undefined until first value unless initialValue/requireSync
    • toSignal rethrows errors on read
    • toObservable emits asynchronously via an effect
    • Both need an injection context
  51. 51.How do typed reactive forms work? Why is a FormControl often typed as string | null?mid

    FormControl, FormGroup and FormArray carry the type of their value, inferred from the initial values, so form.value, valueChanges and setValue() are all type-checked.

    The details interviewers probe:

    • new FormControl('') is FormControl<string | null>, because reset() sets a control to null by default. Pass { nonNullable: true }, or build the form with NonNullableFormBuilder (fb.nonNullable), and reset() returns to the initial value, so the type is just string.
    • form.value is a Partial<...> because disabled controls are left out of it. form.getRawValue() includes every control and has the full type.
    • form.controls.email is fully typed, so a misspelled key is a compile error.
    • FormRecord handles dynamic keys with a single value type, and FormArray<FormControl<string>> a typed list.
    • UntypedFormControl and UntypedFormGroup are escape hatches for migrating older code.
    export class Signup {
      private readonly fb = inject(NonNullableFormBuilder);
    
      readonly form = this.fb.group({
        email: ['', [Validators.required, Validators.email]],
        age: [18],
      });
      // form.controls.email: FormControl<string>
      // form.value:          Partial<{ email: string; age: number }>
      // form.getRawValue():  { email: string; age: number }
      clear() {
        this.form.reset(); // back to '' and 18, never null
      }
    }
    What interviewers listen for
    • Controls and groups carry their value type
    • reset() sets null by default, hence string | null
    • nonNullable / NonNullableFormBuilder remove null
    • value is Partial (disabled omitted); getRawValue() is complete
    • FormRecord for dynamic keys
  52. 52.What is ControlValueAccessor, and how do you make a custom component work with formControlName and ngModel?hard

    ControlValueAccessor is the bridge between Angular forms and a form control's UI. Built-in accessors connect <input>, <select> and checkboxes. For a custom widget, such as a star rating or a date picker, you implement it yourself:

    • writeValue(value): the form pushes a value into the component (initial value, setValue, reset).
    • registerOnChange(fn): save fn and call it when the user changes the value, to update the form model.
    • registerOnTouched(fn): save it and call it on blur or interaction, so touched and updateOn: 'blur' work.
    • setDisabledState(isDisabled) (optional): react to control.disable().

    Register the component as an accessor with an NG_VALUE_ACCESSOR provider, using multi: true and useExisting. forwardRef is needed because the class is referenced inside its own decorator, before it's defined.

    After that, the component works with formControlName, [formControl] and [(ngModel)], and validators, touched state and CSS status classes all come for free. To bundle a built-in validation rule with the component, also provide NG_VALIDATORS and implement Validator.

    @Component({
      selector: 'app-star-rating', templateUrl: './star-rating.html',
      providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarRating), multi: true }],
    })
    export class StarRating implements ControlValueAccessor {
      protected readonly value = signal(0);
      protected readonly disabled = signal(false);
      private onChange = (_: number) => {}; private onTouched = () => {};
      writeValue(v: number | null) { this.value.set(v ?? 0); }
      registerOnChange(fn: (v: number) => void) { this.onChange = fn; }
      registerOnTouched(fn: () => void) { this.onTouched = fn; }
      setDisabledState(d: boolean) { this.disabled.set(d); }
      protected select(n: number) { this.value.set(n); this.onChange(n); this.onTouched(); }
    }
    What interviewers listen for
    • Bridges the forms API and a custom UI control
    • writeValue, registerOnChange, registerOnTouched, setDisabledState
    • Provide NG_VALUE_ACCESSOR with useExisting and multi: true
    • forwardRef because the class is not yet defined
    • Then works with formControlName and ngModel
  53. 53.How do you manage state in an Angular application? When would you use NgRx instead of services?hard

    I pick the lightest tool that fits:

    • Local state: signals inside the component.
    • Shared state: a service with signals. Keep a private writable signal, expose read-only state through asReadonly() and computed() selectors, and make methods the only way to change it. Scope it with providedIn: 'root' or component and route providers. Before signals, the same pattern used a private BehaviorSubject.
    • NgRx Store: the Redux pattern. Actions describe events, pure reducers produce new state, selectors derive memoized slices, and effects handle side effects such as HTTP. You get a single source of truth, one-way data flow, Redux DevTools with time travel and shared conventions, at the cost of boilerplate and indirection.
    • NgRx SignalStore (@ngrx/signals): a lighter, signal-based store built from features like withState, withComputed and withMethods.

    I start with services and signals, and move to NgRx when many features share complex state, changes need to be traceable, or a large team benefits from enforced conventions. Server data (caching, refetching) often fits httpResource() or a query library better than a global store.

    @Injectable({ providedIn: 'root' })
    export class TodoStore {
      private readonly todosState = signal<Todo[]>([]);
    
      readonly todos = this.todosState.asReadonly();
      readonly remaining = computed(() => this.todos().filter((t) => !t.done).length);
    
      add(title: string) {
        this.todosState.update((list) => [...list, { id: crypto.randomUUID(), title, done: false }]);
      }
      toggle(id: string) {
        this.todosState.update((list) => list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
      }
    }
    What interviewers listen for
    • Local state in component signals
    • Shared state: service with private signal, public computed()
    • NgRx: actions, reducers, selectors, effects; DevTools
    • NgRx trades boilerplate for predictability at scale
    • Start simple; adopt a store when complexity demands it
  54. 54.What are server-side rendering and hydration in Angular, and what do you need to watch out for?hard

    SSR renders the app to HTML on the server (Node) for each request, so users see content before the JavaScript loads, which helps first paint, LCP and SEO. Prerendering (SSG) does the same at build time. You add it with ng new --ssr or ng add @angular/ssr, and a server routes config picks a render mode per route.

    Hydration (provideClientHydration()) lets the browser reuse the server-rendered DOM instead of re-creating it, avoiding flicker and wasted work. Useful features:

    • withEventReplay() records clicks made before hydration finishes and replays them.
    • withIncrementalHydration() with @defer (hydrate on viewport) keeps parts of the page dehydrated until needed.
    • HttpClient's transfer cache reuses GET responses fetched on the server, so the client doesn't request them again.

    Watch out for:

    • Browser-only APIs (window, localStorage) don't exist on the server. Use afterNextRender() or isPlatformBrowser().
    • Direct DOM manipulation causes hydration mismatches. Use Angular APIs, or opt out with ngSkipHydration.
    • The server waits for the app to become stable, so pending async work delays the response.
    What interviewers listen for
    • SSR renders HTML on the server; SSG prerenders at build time
    • Hydration reuses server DOM instead of re-rendering
    • Event replay and incremental hydration via @defer (hydrate on ...)
    • Browser APIs go in afterNextRender() or behind isPlatformBrowser
    • Direct DOM manipulation breaks hydration
  55. 55.How does Angular protect against XSS, and when would you use DomSanitizer?mid

    Angular treats every bound value as untrusted by default:

    • Interpolation {{ }} and text bindings are escaped, so <script> shows up as text.
    • [innerHTML] is sanitized: scripts, event-handler attributes and other dangerous markup are stripped, with a dev-mode warning.
    • URL bindings such as [href] and [src] are sanitized, so a javascript: URL is neutralized.
    • Resource URLs, such as an <iframe> src, can't be bound to untrusted values at all; Angular throws.

    DomSanitizer has bypassSecurityTrustHtml, ...Url, ...ResourceUrl, ...Style and ...Script, which mark a value as trusted and skip sanitization. Use them only for content you fully control or have sanitized yourself, such as an embed URL you built from a video id, never for raw user input.

    You lose this protection by going around Angular: writing to nativeElement.innerHTML, building templates from user input, or misusing the bypass methods. Add a CSP and Trusted Types for defense in depth. For CSRF, HttpClient reads an XSRF-TOKEN cookie and sends it as an X-XSRF-TOKEN header on mutating requests to relative URLs.

    export class VideoEmbed {
      private readonly sanitizer = inject(DomSanitizer);
      readonly videoId = input.required<string>();
    
      // We build the URL ourselves from a validated id, so trusting it is safe
      readonly src = computed(() =>
        this.sanitizer.bypassSecurityTrustResourceUrl(
          `https://www.youtube.com/embed/${encodeURIComponent(this.videoId())}`,
        ),
      );
    }
    // <iframe [src]="src()"></iframe>
    What interviewers listen for
    • Bound values are untrusted and escaped or sanitized by default
    • [innerHTML] is sanitized; resource URLs need explicit trust
    • DomSanitizer.bypassSecurityTrust* only for controlled content
    • Direct DOM writes bypass protection
    • CSP, Trusted Types, and XSRF support in HttpClient
  56. 56.How do you unit test Angular components and services? What are TestBed and component harnesses?mid

    TestBed creates a small Angular environment for a test. You configure it like an app, with imports (the standalone component under test) and providers (fakes for its dependencies), then:

    • TestBed.createComponent(Comp) returns a ComponentFixture, with componentInstance, nativeElement and componentRef.setInput() for inputs.
    • Trigger rendering with fixture.detectChanges(), or in zoneless tests await fixture.whenStable().
    • Services are tested with TestBed.inject(Service), or even as plain classes.
    • For HTTP, add provideHttpClient() and provideHttpClientTesting(), then use HttpTestingController to expectOne() a request, flush() a response and verify() that nothing else was sent.

    Component harnesses (from @angular/cdk/testing) are page-object APIs for components, e.g. MatButtonHarness. Tests call await button.click() instead of querying DOM internals, so they keep working when a component's markup changes. Angular Material ships harnesses for its components, and you can write your own.

    New CLI projects use Vitest as the default runner; older projects typically run Karma with Jasmine. End-to-end tests usually use Playwright or Cypress.

    it('shows the user name', async () => {
      TestBed.configureTestingModule({
        imports: [UserCard],
        providers: [{ provide: UserService, useValue: { current: signal({ name: 'Ada' }) } }],
      });
    
      const fixture = TestBed.createComponent(UserCard);
      fixture.componentRef.setInput('compact', true);
      await fixture.whenStable();
    
      expect(fixture.nativeElement.textContent).toContain('Ada');
    });
    What interviewers listen for
    • TestBed configures imports and fake providers
    • createComponent returns a fixture; setInput for inputs
    • detectChanges() or whenStable() to render
    • HttpTestingController to expect and flush requests
    • Harnesses test through a stable API, not DOM details
  57. 57.What is the Angular CLI, and which commands do you use most?easy

    The Angular CLI (ng) scaffolds, builds, serves, tests and updates Angular projects, and applies code changes through schematics.

    • ng new my-app: create a workspace with a configured project.
    • ng serve: start the dev server with rebuilds on save.
    • ng generate (ng g): scaffold a component, service, directive, pipe, guard, interceptor and more, e.g. ng g c user-card.
    • ng build: a production build by default, with AOT compilation, minification, tree-shaking, hashed file names and bundle budget checks.
    • ng test: run unit tests.
    • ng add <package>: install a library and run its setup schematic, e.g. ng add @angular/material or ng add @angular/ssr.
    • ng update: upgrade Angular and run migrations that rewrite your code for breaking changes.

    Project configuration lives in angular.json: builders, build configurations (development, production), fileReplacements for environments, budgets and assets. New projects use the esbuild-based application builder with a Vite-powered dev server.

    What interviewers listen for
    • ng new, ng serve, ng generate, ng build, ng test
    • ng build is a production build: AOT, minified, tree-shaken
    • ng add installs and configures libraries
    • ng update upgrades and runs migrations
    • angular.json holds builders, configurations and budgets
  58. 58.What is linkedSignal, and how is it different from computed?hard

    linkedSignal creates a writable signal whose value is derived from other signals but can also be overridden locally.

    • Like computed(), it recomputes whenever the signals it depends on change.
    • Unlike computed(), you can call set() and update() on it. A local value holds until the source changes again, and then the computation resets it.

    Typical cases are a selection that should reset when its list of options changes, and an editable draft of an input value that resets when a new record arrives.

    There are two forms:

    • Shorthand: linkedSignal(() => this.options()[0]).
    • With { source, computation }: the computation receives the new source value and previous ({ source, value }), so it can keep the user's choice when it's still valid.

    It replaces the anti-pattern of an effect() that copies one signal into another writable signal. If the value never needs a manual override, use computed().

    export class ShippingPicker {
      readonly options = input.required<ShippingOption[]>();
    
      // Resets to the first option whenever options change; the user can still pick another
      readonly selected = linkedSignal(() => this.options()[0]);
    
      // Keeps the user's choice if it still exists in the new list
      readonly kept = linkedSignal<ShippingOption[], ShippingOption>({
        source: this.options,
        computation: (opts, previous) => opts.find((o) => o.id === previous?.value.id) ?? opts[0],
      });
    }
    What interviewers listen for
    • Writable signal derived from a source
    • Local set() holds until the source changes
    • computed() is read-only; linkedSignal is overridable
    • previous lets you preserve a still-valid value
    • Replaces effects that copy one signal into another
  59. 59.What are resource() and httpResource(), and what problem do they solve?hard

    They bring async data loading into the signal model, so you don't have to hand-wire "when this signal changes, fetch, track loading, handle errors".

    resource() takes a params function that reads signals and a loader that returns a Promise. Whenever the params change, it runs the loader again and aborts the previous run through the abortSignal it passes in. If params returns undefined, the loader doesn't run and the resource stays idle. It exposes signals and methods:

    • value(), hasValue(), error(), isLoading(),
    • status(): 'idle', 'loading', 'reloading', 'resolved', 'error' or 'local' (after a local set()),
    • reload(), set() and update().

    Variants: rxResource() from @angular/core/rxjs-interop takes an Observable-returning stream function, and httpResource() from @angular/common/http takes a reactive URL or request and goes through HttpClient, so interceptors and testing tools apply.

    Pitfalls: value() throws while the resource is in the error state, so check hasValue() or error() in the template. Resources are for reading data; send writes (POST, PUT, DELETE) with HttpClient directly.

    export class UserDetail {
      readonly id = input.required<string>();
    
      readonly user = httpResource<User>(() => `/api/users/${this.id()}`);
    }
    
    // Template:
    // @if (user.isLoading()) { <app-spinner /> }
    // @else if (user.error()) { <p>Could not load user.</p> }
    // @else if (user.hasValue()) { <h2>{{ user.value().name }}</h2> }
    What interviewers listen for
    • Signal-driven async loading with status, value and error signals
    • Re-runs when params change and aborts the previous load
    • rxResource for Observables, httpResource via HttpClient
    • value() throws in error state: check hasValue()
    • Meant for reads, not mutations
  60. 60.What causes the ExpressionChangedAfterItHasBeenCheckedError (NG0100), and how do you fix it properly?hard

    It's a development-mode check. After each change detection pass, Angular runs a second verification pass over the same bindings. If one now produces a different value than the one it just rendered, it throws NG0100, because the screen would disagree with the state. Production builds skip the check, so there the same bug silently shows stale values.

    Common causes:

    • Changing a bound value in ngAfterViewInit or ngAfterViewChecked, after the view was already checked.
    • A child updating its parent's state (through a shared service or a synchronous output) while the parent is being checked.
    • Template methods or getters that return a different primitive on every call, like Date.now() or Math.random().

    Proper fixes:

    • Set initial values earlier, in the constructor or ngOnInit.
    • Derive values with computed() instead of assigning them in lifecycle hooks, or make the value a signal.
    • Keep template expressions pure and stable.
    • Restructure data flow so the parent owns the state.

    Wrapping the assignment in setTimeout or calling detectChanges() hides the symptom without fixing the flow.

    @Component({
      selector: 'app-banner',
      changeDetection: ChangeDetectionStrategy.Eager, // named Default in older versions
      template: `<p>{{ message }}</p>`,
    })
    export class Banner implements AfterViewInit {
      message = 'Loading';
    
      ngAfterViewInit() {
        this.message = 'Loaded'; // NG0100: the view was already checked with 'Loading'
      }
    }
    What interviewers listen for
    • Dev-mode verification pass found a binding that changed
    • Classic cause: changing state in ngAfterViewInit
    • Also: child mutating parent state, impure template expressions
    • Fix the data flow: earlier init, computed(), signals
    • setTimeout or detectChanges() only hide it
  61. 61.How do you render a component dynamically when you only know which one at runtime?hard

    Common cases are dialogs, dashboards built from configuration and plugin widgets. There are three main tools:

    • In a template: NgComponentOutlet, e.g. <ng-container *ngComponentOutlet="widget.component; inputs: widget.inputs" />. It's declarative, and the simplest option when a config object decides the component.
    • In code: ViewContainerRef.createComponent(Comp) inserts the component at a location, taken from inject(ViewContainerRef) or a query with { read: ViewContainerRef }. It returns a ComponentRef. Set inputs with ref.setInput('title', value), which also marks it for check, subscribe to outputs through ref.instance, and call ref.destroy() when you're done. Newer versions also accept bindings such as inputBinding() and outputBinding() at creation time.
    • Outside any view, for example a toast attached to document.body: the standalone createComponent(Comp, { environmentInjector, hostElement }), then appRef.attachView(ref.hostView) so change detection reaches it.

    Combine these with a dynamic import() so the component's code loads only when needed. Older code went through ComponentFactoryResolver; since Ivy you pass the component class directly, and recent versions have removed that API.

    @Component({
      selector: 'app-dashboard',
      template: `<ng-container #slot />`,
    })
    export class Dashboard {
      private readonly slot = viewChild.required('slot', { read: ViewContainerRef });
    
      async addChart(title: string) {
        const { SalesChart } = await import('./sales-chart');
        const ref = this.slot().createComponent(SalesChart);
        ref.setInput('title', title);
        ref.instance.closed.subscribe(() => ref.destroy());
      }
    }
    What interviewers listen for
    • NgComponentOutlet for declarative dynamic rendering
    • ViewContainerRef.createComponent() returns a ComponentRef
    • setInput() for inputs; destroy() to clean up
    • Pair with dynamic import() for lazy loading
    • No ComponentFactoryResolver needed any more
  62. 62.How does internationalization (i18n) work in Angular?mid

    Angular's built-in i18n uses @angular/localize (added with ng add @angular/localize) and works at build time:

    • Mark template text with the i18n attribute, optionally with a meaning, a description and a custom id: i18n="site header|Welcome message@@homeWelcome". Attributes use i18n-<attr>, e.g. i18n-title.
    • Mark strings in TypeScript with the $localize tagged template.
    • Handle plurals and alternatives with ICU expressions such as {count, plural, =0 {...} other {...}} and select.
    • Run ng extract-i18n to generate a source translation file (XLIFF by default); translators return one file per locale.
    • Configure locales in angular.json and build with localize. The CLI produces a separate, fully translated build per locale, so there's no runtime lookup cost.

    Locale-aware pipes (date, currency, number) follow LOCALE_ID.

    The trade-off: switching language means loading a different build, usually served under a URL prefix like /fr/. For switching at runtime without a reload, teams use libraries such as Transloco or ngx-translate.

    <h1 i18n="site header|Welcome message@@homeWelcome">Welcome to Stacktrick</h1>
    
    <img [src]="logoUrl" i18n-alt alt="Stacktrick logo" />
    
    <span i18n>
      {count, plural, =0 {No new messages} =1 {One new message} other {{{count}} new messages}}
    </span>
    What interviewers listen for
    • Mark text with i18n, attributes with i18n-attr
    • $localize for strings in TypeScript
    • ICU expressions for plural and select
    • ng extract-i18n produces translation files
    • One compiled build per locale; runtime switching needs a library
esc