skip to content

Template Language

Angular's template syntax: expression rules, property, attribute and event bindings, two-way binding, the @if, @for and @switch blocks and template variables. Interviewers probe each construct.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

In current Angular templates, how do you write an if / else-if / else, and how does it replace *ngIf with an else template?

level: juniorimportance: must knowfreq 78%

answer

  1. built into the template syntax
  2. blocks, not a directive
  3. no ng-template reference needed
  4. deprecated since v20

basics

~20 s

Use the built-in @if (cond) { } @else if (other) { } @else { } blocks. They replace *ngIf="cond; else tpl" plus an <ng-template #tpl>, need no import, and *ngIf has been deprecated since v20.

solid answer

~40 s

Since v17 Angular templates have built-in control flow. `@if (condition) { ... }` renders its content when the condition is truthy, any number of `@else if (other) { ... }` branches can follow, and a single `@else { ... }` must come last. The old form, `*ngIf="loaded; else loading"` with a separate `<ng-template #loading>`, needed the `NgIf` directive imported and split one decision across two places in the template; there was also no `else if` at all. The blocks are part of the template language, so a standalone component imports nothing to use them, and the compiler checks the branches as one construct. `NgIf` is deprecated since v20 with intent to remove, so new code uses `@if`, and existing templates are migrated.

code

ts · 19 lines
ts
import { Component, input } from '@angular/core';

type OrderStatus = 'pending' | 'paid' | 'shipped' | 'cancelled';

@Component({
  selector: 'app-order-banner',
  template: `
    @if (status() === 'cancelled') {
      <p class="banner banner--error">This order was cancelled.</p>
    } @else if (status() === 'shipped') {
      <p class="banner">On its way.</p>
    } @else {
      <p class="banner">We are preparing your order.</p>
    }
  `,
})
export class OrderBanner {
  status = input.required<OrderStatus>();
}

go deeper

for a junior

Recall the @if / @else if / @else syntax and translate a *ngIf with an else template into it.

for a middle

Explain what the parser enforces and that the chosen branch's view is created and destroyed as the condition changes.

for a senior

Discuss migrating large templates off *ngIf, removing leftover imports, and where @if destroying state is a bug.

for a principal

Plan when to schedule control-flow migration against other upgrade work, given NgIf is deprecated but still supported.

## The built-in conditional block Angular v17 added **control flow blocks** to the template language. The conditional family is: ```html @if (order.status === 'cancelled') { <p class="banner banner--error">This order was cancelled.</p> } @else if (order.status === 'shipped') { <p class="banner">On its way.</p> } @else { <p class="banner">We are preparing your order.</p> } ``` The rules the parser enforces: - `@if` takes **one expression** in parentheses, optionally followed by an `as` alias. - **Any number** of `@else if` blocks may follow. - **At most one** `@else`, and it must be **last**; it takes no parameters. - A branch renders when its expression is **truthy** by JavaScript rules; the first truthy branch wins and the rest are skipped. Because the blocks are syntax, not directives, there is nothing to import. A standalone component can use `@if` with an empty `imports` array. ## What it replaces Before v17 the same template needed the `NgIf` structural directive: ```html <p *ngIf="order.status === 'cancelled'; else notCancelled">Cancelled.</p> <ng-template #notCancelled> <p *ngIf="order.status === 'shipped'; else preparing">On its way.</p> </ng-template> <ng-template #preparing> <p>We are preparing your order.</p> </ng-template> ``` | Concern | `*ngIf` | `@if` | |---|---|---| | Else branch | a `#ref` to an `<ng-template>` placed elsewhere | an `@else { }` block right after | | Else-if | nest another `*ngIf` inside the else template | `@else if (...) { }` | | Imports | `NgIf` or `CommonModule` in the component | none | | Multiple root nodes | wrap in `<ng-container *ngIf>` | put them straight in the block | | Status | deprecated since v20, intended for removal | current recommendation | The `*` form is shorthand for wrapping the element in an `<ng-template>`, and the else branch has to be a named template because a directive can only receive other templates through inputs. The block syntax removes that indirection: the branches sit next to each other in reading order. ## What happens at runtime The compiler turns the whole `@if` chain into one instruction that evaluates the conditions in order during change detection and returns the index of the branch to show (or none). Then: 1. If the **chosen branch changed**, Angular destroys the previous branch's view and creates the new one. 2. If the **same branch** is still chosen, the existing view is kept and simply updated. 3. If **no branch** matches and there is no `@else`, nothing is rendered. Destroying a branch destroys its components and directives and runs their cleanup, the same as removing any view. That is intended: an `@if` is for showing different content, not for hiding content that should keep its state. ## Narrowing inside the branches Under strict template type checking, the compiler reproduces the chain as real TypeScript `if` statements, so a branch body is type-checked with the condition's narrowing applied. `@if (order.shipment) { {{ order.shipment.carrier }} }` compiles without a possibly-undefined error, because `order.shipment` is a property path TypeScript can narrow. Each `@else if` and the `@else` also see the **negation** of the earlier conditions, so an `@else` after `@if (status === 'cancelled')` knows the status is not `'cancelled'`. Narrowing does not apply to calls: `@if (order()) { {{ order().id }} }` still sees `order()` as possibly empty, which is where the `as` alias or an `@let` variable helps. The same guards are applied to event handlers inside the branch, so a `(click)` in the branch body is checked with the narrowed type too. ## Common mistakes - Putting `@else` before an `@else if`: the parser reports that the `@else` block must be last. - Writing two `@else` blocks: only one is allowed. - Expecting `@if` to hide content while preserving component state; for that, bind a class or the `hidden` attribute instead. - Keeping `NgIf` or `CommonModule` in `imports` after migrating; under strict templates the `unusedStandaloneImports` diagnostic (NG8113) can flag an import the template no longer uses. ## When the old syntax still appears Most production codebases still contain `*ngIf`, and it keeps working in v22 as a deprecated API. Interviewers often ask candidates to read both forms, so it is worth being able to translate one into the other in both directions, including `*ngIf="value$ | async as value"`, which becomes `@if (value$ | async; as value)`.

  • In Angular, what happens to a component inside an @if branch when the condition becomes false?
    The branch's view is destroyed, so the component is destroyed and its cleanup runs. When the condition becomes true again, a fresh instance is created with fresh state. If the state must survive, hide the content with a class or the `hidden` attribute, or keep the state in a parent or a service.
  • How do you write *ngIf="user$ | async as user; else loading" with Angular's built-in control flow?
    `@if (user$ | async; as user) { ... } @else { ... }`, moving the loading markup from the `<ng-template #loading>` into the `@else` block. Note the semicolon before `as` in the block form.

saying these in an interview costs you the question

  • @if requires importing NgIf or CommonModule
  • @else can appear anywhere among the @else if branches
  • @if only toggles CSS visibility and keeps the DOM
  • *ngIf supported else-if chains directly
  • *ngIf was removed in v17 when @if arrived
open as a page

In an Angular template, how does an (event)="statement" binding work, and what does the $event variable hold?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Parentheses around an event name make Angular listen for that event and run the quoted template statement each time it fires. $event is the native DOM event object, or the value a component output emitted.

open as a page

In an Angular template, what does {{ }} interpolation evaluate its expression against, and what text does it render for null, undefined or an object?

level: juniorimportance: must knowfreq 58%

basics

~20 s

Interpolation evaluates its expression against the component instance, with this implied, plus any template variables in scope, converts the result to a string and writes it as text. null and undefined render as an empty string; an object renders through String().

open as a page

In Angular's @for block, why is the track expression mandatory, and what should you usually pass to it?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Angular's @for requires track so the runtime can match each item to its existing row view by key. Track a stable unique field like item.id, use $index only for static lists, and the item itself only as a last resort.

open as a page

In an Angular template, what is the difference between a property binding such as [value] and an attribute binding such as [attr.colspan]?

level: juniorimportance: must knowfreq 68%

basics

~20 s

[value] writes the element's DOM property, or a matching component or directive input, and is checked at compile time. [attr.colspan] calls setAttribute with the stringified value and removes the attribute for null or undefined; use it when no matching property exists.

open as a page

In an Angular template, what does [(value)]="volume" expand to, and why must the parentheses sit inside the square brackets?

level: juniorimportance: must knowfreq 70%

basics

~10 s

[(value)]="volume" is shorthand for [value]="volume" plus (valueChange)="volume = $event". The parentheses go inside the brackets because ([value]) parses as an event binding to an event literally named [value].

open as a page

Why does <input [(ngModel)]="name"> in an Angular standalone component fail with "Can't bind to 'ngModel'", and what does the binding expand to?

level: juniorimportance: must knowfreq 68%

basics

~20 s

ngModel is an input of the NgModel directive from FormsModule, not a property of <input>. Without FormsModule in the component's imports nothing claims it, so the compiler reports NG8002. With it, the binding is [ngModel] plus (ngModelChange).

open as a page

In an Angular template, what does a #ref variable hold on a plain element, a component, an ng-template and with an exportAs name?

level: juniorimportance: must knowfreq 68%

basics

~10 s

An Angular #ref holds the DOM element on a plain element, the component instance on a component's host element, a TemplateRef on an ng-template, and the directive instance when written #x="exportAsName".

open as a page

Which JavaScript constructs are not allowed in Angular template expressions, and why does {{ Math.round(price) }} fail?

level: middleimportance: must knowfreq 55%

basics

~20 s

Template expressions are a JavaScript subset: no globals except undefined and $any, no new, no bitwise operators, no destructuring, comma operator or declarations, and no assignments in bindings. Math.round fails because Math is looked up on the component, not on the global scope.

open as a page

In Angular, when an array rendered by @for changes, how does the track key decide whether each row's view is kept, updated, moved or recreated?

level: middleimportance: must knowfreq 60%

basics

~20 s

Angular's @for matches old and new track keys: a matching key keeps the view and swaps in the new item, a key at a new position moves the view's DOM nodes, new keys create views, vanished keys destroy them.

open as a page

In Angular templates, what does an @let declaration do, why can it not be reassigned, and where is it visible?

level: middleimportance: must knowfreq 55%

basics

~20 s

Angular's @let name = expression; gives a template expression a name that stays in sync like a binding. It cannot be assigned to, and it is visible after its declaration in its own view and nested views, not in parents or siblings.

open as a page

In an OnPush Angular component, why does a click handler's field update render but the same update made later in a setTimeout callback does not?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Angular wraps every template listener so it marks the declaring view and its ancestors dirty before running the handler. A setTimeout callback runs outside that wrapper, so a plain-field change there leaves the OnPush view clean and unrendered.

open as a page

In Angular's @for block, which implicit variables does every row get, and how do let aliases and @empty work?

level: juniorimportance: should knowfreq 55%

basics

~20 s

Every @for row gets $index, $count, $first, $last, $even and $odd without declaring them; a let clause gives them other names, which matters in nested loops; an optional @empty block renders when the collection has no items.

open as a page

In Angular's @if block, what does the as alias hold, and why does @if (unreadCount(); as count) hide the badge when there are zero messages?

level: middleimportance: should knowfreq 45%

basics

~20 s

The as alias holds the value of the @if expression, evaluated once per check. The block still renders only when that value is truthy, so a count of 0 hides it; test count !== null instead, or show the number outside the @if.

open as a page

How does Angular's @switch block match its @case values, and how do several order statuses share one @case body?

level: middleimportance: should knowfreq 50%

basics

~20 s

@switch evaluates its expression once and compares it with each @case value using ===, rendering the first match or @default. There is no fallthrough; stack consecutive @case blocks to share one body (supported since v21.1).

open as a page

With Angular's strictTemplates enabled, what type does $event get in (keydown), (keydown.enter) and a component output binding, and why?

level: middleimportance: should knowfreq 40%

basics

~10 s

Under strictTemplates the compiler infers $event from TypeScript's DOM event maps, so (keydown) gives a KeyboardEvent, while a keyed pseudo-event like keydown.enter falls back to plain Event. Outputs use their declared emitted type.

open as a page

In Angular, how are key-filtered event bindings like (keydown.enter) and (keydown.code.alt.keys) matched, and why does (keydown.enter) ignore Shift+Enter?

level: middleimportance: should knowfreq 48%

basics

~20 s

Angular's key-events plugin parses the name into a key plus modifiers and runs the handler only when the pressed key and the full set of held modifiers match exactly, so Shift+Enter does not match keydown.enter.

open as a page

In an Angular template, what is the difference between user?.name and user!.name, and what does each do when user is null?

level: middleimportance: should knowfreq 48%

basics

~20 s

user?.name is a runtime guard: if user is null or undefined it short-circuits and yields undefined (null before v22). user!.name is a TypeScript non-null assertion that only silences the type checker; at runtime a null user still throws a TypeError.

open as a page

In Angular, how do a static class or style attribute, [class]/[style] bindings and [class.x]/[style.x.px] bindings on one element combine?

level: middleimportance: should knowfreq 50%

basics

~20 s

Angular merges them all onto the element. On a conflict a single-key binding like [style.width.px] beats the [style] map, which beats the static attribute; undefined defers to the lower-priority value. Map bindings compare by reference, so mutating an object changes nothing.

open as a page

In an Angular table header written as <th [colspan]="span()" [aria-sort]="sortDir()">, which binding fails to compile, and how should each be written?

level: middleimportance: should knowfreq 40%

basics

~10 s

[colspan] fails with NG8002 because a th has no colspan property; write [colSpan] or [attr.colspan]. [aria-sort] compiles since Angular 20.2 and writes the aria-sort attribute, removing it when sortDir() is null.

open as a page

How do you support [(value)] on an Angular component without model(), and what do the NG8007 and NG1054 errors mean for such a pair?

level: middleimportance: should knowfreq 38%

basics

~20 s

Declare an input value and an output valueChange; the parent's [(value)] then binds both. NG8007 means the two halves went to different targets, usually a missing or misnamed output; NG1054 means two members claim the same binding name.

open as a page

In Angular, how does [(value)]="volume" work when volume is a WritableSignal, and why is [(value)]="volume()" rejected?

level: middleimportance: should knowfreq 52%

basics

~20 s

The compiler passes the signal itself: the property half reads its value and the event half calls set($event) on it. volume() is a call, not an assignable target, so the parser reports an unsupported expression.

open as a page

In Angular templates, why can a #ref declared inside an @if or @for block not be read from outside that block?

level: middleimportance: should knowfreq 40%

basics

~20 s

Angular's @if, @for, @defer and ng-template bodies are separate embedded views; a #ref declared inside one is visible there and in views nested deeper, but name lookup only walks outward, so the parent template cannot see it.

open as a page

When migrating Angular templates from *ngIf and NgSwitch to @if and @switch, which behaviour differences can change what renders?

level: seniorimportance: should knowfreq 40%

basics

~20 s

NgSwitch renders every matching case and every default, while @switch renders one branch, and pre-v17 NgSwitch matched with ==. *ngIf's host element moves inside the @if block, and a shared else template cannot simply be inlined.

open as a page

An Angular command palette should open on Ctrl+K anywhere on the page, but (document:keydown.ctrl.k) never fires and Mac users expect Cmd+K; what is wrong and how do you fix it?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Angular's key filter only knows the modifier word control, so ctrl makes the key plugin decline the name and a listener for a nonexistent event is registered. Bind keydown.control.k and keydown.meta.k on document and call preventDefault().

open as a page

After removing the $safeNavigationMigration() wrappers an Angular 22 update added, {{ greet(user()?.firstName) }} shows 'Hello, undefined' instead of 'Hello, guest'. Why?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Since Angular 22 the template ?. operator yields undefined like JavaScript, not null. greet() tests name === null, so undefined falls through to 'Hello, undefined'. The removed wrapper told the compiler to keep the old null result for that argument.

open as a page

In an Angular leaderboard using @for (p of ranked(); track $index), why do expanded panels and typed notes stay at old positions after a re-sort, and how do you fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

With track $index every position key still matches after a sort, so Angular keeps each row view in place and hands it another player; view state stays behind. Track p.id so views move with players.

open as a page

When migrating an Angular *ngFor that uses a trackBy function to @for, what does the track clause become and what behaviour changes?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A trackBy: trackById option becomes track trackById($index, item), or better an inline track item.id; an *ngFor without trackBy maps to identity tracking, which should be replaced by an id. Afterwards @for reuses views in place where *ngFor sometimes remounted them.

open as a page

Why does <button [attr.disabled]="isSaving()"> in an Angular template stay disabled after isSaving() becomes false, and what is the fix?

level: seniorimportance: should knowfreq 38%

basics

~20 s

An attribute binding writes false as the string 'false', and disabled is a boolean attribute whose mere presence disables the button; Angular removes an attribute only for null or undefined. Bind the property with [disabled], or use isSaving() ? '' : null.

open as a page

A parent writes <app-volume-slider (valueChange)="persist()" [(value)]="volume" /> and persist() always saves the previous volume in Angular; why, and how do you fix it?

level: seniorimportance: should knowfreq 36%

basics

~10 s

Both bindings subscribe to the same valueChange output in template order, so persist() runs before the two-way listener writes the new value. Pass $event to persist, or at least move the handler after [(value)].

open as a page

showing 1–30 of 35