Conditional rendering with @if and @else blocks

The first example pairs a checkbox with the isChecked signal. Since the signal defaults to true, the checkbox starts selected, and the template renders what's inside the @if block. The snippet comes from the src\app\app.component.html file:

<h3>&#64;if and &#64;else</h3>
<div>
  <input #checkbox type="checkbox" [checked]="isChecked()" (change)="isChecked.set(checkbox.checked)" id="checkbox"/>
</div>
<div>
@if (isChecked()) {
  <span>Checked</span>
} 
@else {
  <span>Not checked</span>
}
</div>
Enter fullscreen mode Exit fullscreen mode

The syntax @if (logical_expression) { opens a block governed by a logical expression. Here, the isChecked() signal serves as that expression because it resolves to a boolean.

A matching @else block sits directly below. It appears whenever the @if condition is false — so when the checkbox is cleared, the template switches to the @else content.

One key detail of the new syntax: the characters @, {, and } are reserved. Any literal occurrence of these in a template must be written as an HTML entity:

  • &#64; for @, as in the &lt;h3> heading of the example
  • &#123; for {
  • &#125; for }

Failing to do so triggers compile-time errors such as:

  • [ERROR] NG5002: Incomplete block "...". If you meant to write the @ character, you should use the "@" HTML entity instead. [plugin angular-compiler]
  • [ERROR] NG5002: Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)

Rendering collections with @for blocks

Start by declaring the items array in the component class:

  collection = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];
Enter fullscreen mode Exit fullscreen mode

To iterate over the collection, use @for (item of items; track item.id) {:

<ul>
@for (item of collection; track item.id; let index = $index, first = $first; let last = $last, even = $even, odd = $odd; let count = $count) {
<li><strong>{{item.name}}</strong> index={{index}} first={{first}} last={{last}} even={{even}} odd={{odd}} count={{count}}</li>
}
</ul>
Enter fullscreen mode Exit fullscreen mode

Every object in the list needs a unique field — like id — which is passed to the track parameter. When the collection holds primitives such as strings or numbers, you can track the item itself: @for (item of items; track item) {.

Inside the block, you have access to several contextual values besides the current item:

  • $index: the item's position in the collection
  • $even: true when the index is even
  • $odd: true when the index is odd
  • $count: total number of items in the collection
  • $first: true if the item is at the first position
  • $last: true if the item is at the last position

Handling empty collections with @empty blocks

An @empty block can accompany an @for block. Its content shows up when the supplied collection has no items:

<ul>
@for (item of emptyCollection; track item.id;) {
<li><strong>{{item.name}}</strong></li>
}
@empty {
  <span>The collection is empty</span>
}
</ul>
Enter fullscreen mode Exit fullscreen mode

Switch control flow with @switch, @case, and @default

This example uses four radio buttons linked to a radioValue signal. The signal starts at 1 and updates to 1, 2, 3, or 4 based on user selection:

<div>
  <div>
    <input type="radio" [checked]="radioValue() === 1" (change)="radioValue.set(1)" id="radio1"/>
    <label for="radio1">1</label>
  </div>
  <div>
    <input type="radio" [checked]="radioValue() === 2" (change)="radioValue.set(2)" id="radio2"/>
    <label for="radio2">2</label>
  </div>
  <div>
    <input type="radio" [checked]="radioValue() === 3" (change)="radioValue.set(3)" id="radio3"/>
    <label for="radio3">3</label>
  </div>
  <div>
    <input type="radio" [checked]="radioValue() === 4" (change)="radioValue.set(4)" id="radio4"/>
    <label for="radio4">4</label>
  </div>
</div>
<div>
@switch (radioValue()) {
  @case (1) {
    <span>Case 1</span>
  }
  @case (2) {
    <span>Case 2</span>
  }
  @default {
    <span>Default case (Not 1 or 2)</span>
  }
}
</div>
Enter fullscreen mode Exit fullscreen mode

Inside the @switch block, three nested blocks control the output:

  • @case (1) { renders when radioValue() is 1
  • @case (2) { renders when radioValue() is 2
  • @default { renders when radioValue() doesn't match any specified @case value

Migrating ngIf, ngFor, and ngSwitch to the new syntax

To automate the conversion of structural directives across your templates, run this schematic:

ng g @angular/core:control-flow-migration
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

This walkthrough covered how the updated control flow in Angular 17 operates, focusing on building conditional content and loops with the new control block syntax. I trust the examples proved useful for your own projects.

In the earlier segment of this series, I detailed the mechanics of deferred blocks and outlined the criteria that can be set to manage when their content loads and renders: New Angular 17 feature: deferred loading.

Your thoughts and comments are always appreciated.

👨‍💻About the author

I'm Gergely Szerovay, serving as a frontend development chapter lead. A deep interest of mine is both teaching and mastering Angular. Each day, I dive into Angular-related materials—articles, podcasts, conference sessions, and more.

To share the top finds from my daily exploration, I established the Angular Addict Newsletter. Whether you're just starting or already deep into Angular, there's something in it for you.

Additionally, I run a publication called—you guessed it—Angular Addicts. This space gathers what I deem the most insightful and engaging resources. If you're interested in contributing, just reach out.

Come join the learning journey! Subscribe here 🔥

For more Angular insights, connect on Substack, Medium, Dev.to, Twitter, or LinkedIn.