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>@if and @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>
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:
@for@, as in the<h3>heading of the example{for{}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' }
];
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>
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:truewhen the index is even$odd:truewhen the index is odd$count: total number of items in the collection$first:trueif the item is at the first position$last:trueif 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>
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>
Inside the @switch block, three nested blocks control the output:
@case (1) {renders whenradioValue()is1@case (2) {renders whenradioValue()is2@default {renders whenradioValue()doesn't match any specified@casevalue
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
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.
