Week 6: New Control Flow & Template Syntax

Angular replaced its structural directives — *ngIf, *ngFor, *ngSwitch — with built-in template syntax that reads closer to JavaScript itself. This week you'll write every conditional and loop in your templates the modern way, and know exactly why it's an improvement, not just a style change.

Phase 2 of 7 Week 6 of 26 ~3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Write templates using @if/@for/@switch fluently
  • Understand @for's tracking behavior and why it matters for performance
  • Migrate an existing *ngIf/*ngFor template confidently

1. @if / @else if / @else

Conditional rendering, block-scoped and readable top to bottom — no more prefixing an attribute with an asterisk.

order-status.html
@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:

as-binding.html
@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.

product-list.html
@for (product of products(); track product.id) {
  <app-product-card [product]="product" />
} @empty {
  <p>No products match your filters.</p>
}
Why 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:

context-variables.html
@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:

role-badge.html
@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.

cart-summary.html
@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:

search-results.html
@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:

before — structural directives
<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>
after — new control flow
@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:

terminal
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

Hands-on

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:

  1. A signal holding an array of { id, name, department, salary } records (at least 10 fake entries).
  2. A text input bound to a filterText signal; use a computed() to derive the filtered list (case-insensitive match on name or department).
  3. Three sort buttons (by name, department, salary) that toggle a sortKey signal; another computed() derives the sorted-and-filtered list from that.
  4. Render rows with @for, tracked by id, showing an @empty block ("No employees match.") when the filtered list is empty.
  5. Use @switch to render a colored badge based on a computed salaryBand ('junior' / 'mid' / 'senior') per row.
  6. Use at least one @let to avoid repeating an expression in the template.
Hint

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?

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?

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?

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?

@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.