skip to content

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