1. @if / @else if / @else
Conditional rendering, block-scoped and readable top to bottom — no more prefixing an attribute with an asterisk.
@if (order().status === 'delivered') {
<p class="status status--success">Delivered {{ order().deliveredAt | date }}</p>
} @else if (order().status === 'shipped') {
<p class="status status--info">On its way — tracking #{{ order().trackingNumber }}</p>
} @else if (order().status === 'cancelled') {
<p class="status status--error">Cancelled</p>
} @else {
<p class="status status--pending">Processing</p>
}
You can also capture the condition's value with as — handy when the
condition is an expensive call or a signal you want to read once per block:
@if (currentUser(); as user) {
<p>Welcome back, {{ user.name }}</p>
}
2. @for, Track Expressions & Performance
@for requires a track expression — not optional the way
trackBy was with *ngFor. It tells Angular how to recognize
"this is the same item" across re-renders, even if its position in the array changed.
@for (product of products(); track product.id) {
<app-product-card [product]="product" />
} @empty {
<p>No products match your filters.</p>
}
track product.id and not track $index
With a stable ID, reordering, filtering, or sorting the array lets Angular match existing DOM nodes and components to their (possibly moved) items — no destroy/recreate, form state and focus survive. With $index, position is treated as identity: sort the list and Angular thinks every item "changed," destroying and recreating components it didn't need to. Reach for $index only when items genuinely have no stable identity.
@for also exposes contextual variables you'll use constantly:
@for (item of items(); track item.id; let i = $index, isLast = $last, count = $count) {
<li [class.is-last]="isLast">
{{ i + 1 }} of {{ count }}: {{ item.name }}
</li>
}
3. @switch / @case / @default
For more than two or three branches on the same value, @switch reads
better than a chain of @else if:
@switch (user().role) {
@case ('admin') {
<app-badge color="red">Admin</app-badge>
}
@case ('editor') {
<app-badge color="blue">Editor</app-badge>
}
@case ('viewer') {
<app-badge color="gray">Viewer</app-badge>
}
@default {
<app-badge color="gray">Unknown</app-badge>
}
}
Unlike JavaScript's switch, there's no fallthrough to worry about — each
@case block is independent, so you never need a stray break.
4. @let for Local Template Variables
@let declares a template-scoped variable — useful for naming an expression
you'd otherwise repeat, without introducing a whole @if ... as block just to
get a local name.
@let itemCount = cartItems().length;
@let isEmpty = itemCount === 0;
<p>{{ itemCount }} item{{ itemCount === 1 ? '' : 's' }} in cart</p>
@if (isEmpty) {
<p>Your cart is empty.</p>
}
@let variables are read-only and scoped to the template block they're
declared in — you can't reassign one, and a value declared inside an @if
block isn't visible outside it.
5. Conditional Rendering & Empty-State Patterns
Combining what you've learned so far, a realistic loading/empty/error/success template
looks like this — a pattern you'll see again, built on signals instead of raw booleans,
once you meet resource() in Week 16:
@if (isLoading()) {
<app-spinner />
} @else if (error()) {
<p class="error">{{ error() }}</p>
} @else {
@for (result of results(); track result.id) {
<app-result-row [result]="result" />
} @empty {
<p>No results for "{{ query() }}".</p>
}
}
Notice @empty only fires when the list is genuinely empty — it's distinct
from the loading and error states above it, which is exactly the distinction a real
search UI needs to make.
6. Migrating from *ngIf/*ngFor
Side by side, the mapping is direct:
<div *ngIf="user; else guestTpl">
Welcome, {{ user.name }}
</div>
<ng-template #guestTpl>
<div>Please log in</div>
</ng-template>
<li *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</li>
@if (user) {
<div>Welcome, {{ user.name }}</div>
} @else {
<div>Please log in</div>
}
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}
The Angular CLI ships a schematic that does this conversion automatically across an entire project:
ng generate @angular/core:control-flow
It's worth running once on a real project and reading the diff — you'll see every pattern from this lesson applied at scale, which cements the mapping far better than flashcards would.
7. Hands-on Exercise
Rebuild a data table using only the new control-flow syntax
Build a sortable, filterable table over a list of employees — no *ngIf or *ngFor anywhere in it.
Requirements:
- A signal holding an array of
{ id, name, department, salary }records (at least 10 fake entries). - A text input bound to a
filterTextsignal; use acomputed()to derive the filtered list (case-insensitive match onnameordepartment). - Three sort buttons (by name, department, salary) that toggle a
sortKeysignal; anothercomputed()derives the sorted-and-filtered list from that. - Render rows with
@for, tracked byid, showing an@emptyblock ("No employees match.") when the filtered list is empty. - Use
@switchto render a colored badge based on a computedsalaryBand('junior' / 'mid' / 'senior') per row. - Use at least one
@letto avoid repeating an expression in the template.
Chain your computed signals: filtered reads the raw list and filterText; sorted reads filtered and sortKey. Keeping each computed focused on one transformation makes the whole pipeline easier to reason about than one giant computed doing everything at once.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does @for require a track expression, when *ngFor's trackBy was optional?
Why does @for require a track expression, when *ngFor's trackBy was optional?
Making it mandatory forces you to think about item identity up front instead of accidentally shipping the slower default (identity-based tracking, which behaves like tracking by index for object literals). It's a deliberate API design choice to prevent a whole class of easy-to-miss performance bugs.
Q2
What actually goes wrong if you use track $index on a list the user can reorder?
What actually goes wrong if you use track $index on a list the user can reorder?
Angular treats "position 3" as the identity, not the item itself. After a reorder, it thinks every position holds a different item than before (even ones that just moved), so it destroys and recreates the components at each position instead of moving them — losing any local component state, focus, or in-progress animations along the way, and doing unnecessary work.
Q3
What's the scope of a variable declared with @let inside an @if block?
What's the scope of a variable declared with @let inside an @if block?
Just that @if block — it isn't visible outside it, the same way a let declared inside a JavaScript if { } block wouldn't be visible after the closing brace. @let variables are also read-only; you can't reassign one after declaring it.
Q4
In the loading/error/empty pattern from Section 5, why is @empty not enough on its own to handle all three states?
In the loading/error/empty pattern from Section 5, why is @empty not enough on its own to handle all three states?
@empty only knows about one thing — whether the array passed to @for has zero items. It can't distinguish "still loading" or "the request failed" from "the request succeeded and returned nothing," because by the time @for runs, the array is either populated or it isn't. Loading and error are separate booleans/signals that have to be checked before you even reach the @for.