In current Angular templates, how do you write an if / else-if / else, and how does it replace *ngIf with an else template?
answer
- built into the template syntax
- blocks, not a directive
- no ng-template reference needed
- deprecated since v20
basics
~20 sUse 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 sSince 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 linesimport { 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
Recall the @if / @else if / @else syntax and translate a *ngIf with an else template into it.
Explain what the parser enforces and that the chosen branch's view is created and destroyed as the condition changes.
Discuss migrating large templates off *ngIf, removing leftover imports, and where @if destroying state is a bug.
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