Angular 17 introduced block syntax for conditionals and loops. It is built into the template
compiler, needs no import, and has replaced the structural directives
*ngIf, *ngFor and *ngSwitch in new code. The pizza app uses the
blocks exclusively — there is not one *ngIf in it.
@if
@if (isAdmin()) {
<li class="nav-item">
<a class="nav-link" routerLink="/admin" routerLinkActive="active" (click)="closeMenus()"
>Admin</a
>
</li>
}It takes @else if and @else too:
@if (loading()) {
<app-spinner label="Loading the menu…" />
} @else {
<div class="row row-cols-1 row-cols-md-3 g-4">The block removes the element from the DOM rather than hiding it. That is
worth being deliberate about: a hidden element still exists, still holds state, and is still
reachable by a screen reader; a removed one is gone, and its component is destroyed. When you want
the cheaper thing — toggling visibility without tearing anything down — bind a class or
[hidden] instead.
@if with as
When the condition is a value you also want to use, name it:
@if (error(); as message) {
<div class="alert alert-danger d-flex justify-content-between align-items-center">
<span>{{ message }}</span>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="reload()">
Try again
</button>
</div>
}error() is checked once, and message holds the result for the block.
Without as the signal would be read again for the interpolation, which is both wasteful
and — if the value could change between the two reads — wrong.
@for, and why track is mandatory
@for (product of visibleProducts(); track product.id) {
<div class="col">
<app-product-card [product]="product" (selected)="selectedProduct.set($event)" />
</div>
}track is required. This is the biggest practical difference from
*ngFor, where trackBy was optional, awkward enough that almost nobody used
it, and the cause of a whole family of performance complaints. Leave it out and the build fails.
It tells Angular how to recognise the same item across renders. Given track
product.id, reordering a list moves DOM nodes; without stable identity Angular would destroy
and rebuild them, losing focus, scroll position and any state inside those components. It is React's
key, promoted from a convention people forget to a rule the compiler enforces.
Track whatever is genuinely stable and unique. An id is ideal. track $index is
available and is the wrong answer for anything that can be reordered, inserted into or filtered —
the index of an item is not a property of the item.
@empty
The loop carries its own empty state:
} @empty {
<div class="text-center text-muted py-5">
<div class="display-6 mb-2">🍕</div>
<p class="mb-0">Your cart is empty.</p>
</div>
}One block describes both states, so they cannot drift apart — which is exactly what the
items.length === 0 ? … : … ternary alongside a separate loop invites. The orders table
uses the same shape to render a full-width "you have not placed any orders yet" row.
The contextual variables
Inside @for you also get $index, $first,
$last, $even, $odd and $count. Alias them if a
nested loop makes it ambiguous:
@for (item of items(); track item.id; let i = $index, isLast = $last) {
<div [class.border-bottom]="!isLast">{{ i + 1 }}. {{ item.name }}</div>
}@switch
@switch (order.status) {
@case ('PENDING_PAYMENT') { <span class="badge bg-warning">Awaiting payment</span> }
@case ('PAID') { <span class="badge bg-success">Paid</span> }
@default { <span class="badge bg-secondary">{{ order.status }}</span> }
}No break, and no fall-through — each case is a block. The comparison is strict
equality. There is no @switch in the pizza app, because a status badge there is one
lookup rather than a branch, which is usually the better answer when the cases only differ by a
value.
@let
Covered in the templates lesson and worth repeating here, because it is most useful inside a loop. It binds a value for the rest of the block:
@let selected = isSelected(topping.id);Without it, a value needed by three bindings is computed three times per iteration.
The old syntax
You will still meet this:
<!-- The old way. Still supported; not what you should write. -->
<li *ngIf="isAdmin()">…</li>
<div *ngFor="let product of products; trackBy: trackById">…</div>Two practical differences beyond the syntax. The old directives had to be imported —
NgIf and NgFor from CommonModule — and forgetting that produced
a template that silently rendered nothing, because *ngIf was just an unknown attribute.
And the asterisk is shorthand for wrapping the element in an <ng-template>, which
is why two structural directives could never sit on one element. The blocks have neither
problem.
Angular ships a migration: ng generate @angular/core:control-flow.
What is next
Components have been taking data through input() and sending it back through
output() without much explanation. Next lesson, properly.