New Angular 17 feature: new control flow syntax
Angular's new declarative control flow demonstrated by Signal-based examples
Angular 17 is going to be released in the beginning of November, and it is going to offer a new template control block syntax with a declarative control flow. Two groups of features use these new blocks:
the deferred loading blocks: Angular 17 is going to have a
@defer
control block enabling lazy-loading of the block's content. Lazy-loading also applies to the dependencies of the content of the block: all the components, directives and pipes are going to be lazy-loaded, too. I demonstrated by examples howdefer
blocks work in my previous articleblocks for providing conditional rendering and rendering the items of a collection (RFC): these are alternatives for the
NgIf
,NgFor
, andNgSwitch
directives
One of the most important benefits of these new control blocks is that they will support zoneless applications with Signals.
In this article, I demonstrate how to:
use the new control block syntax and create conditionally rendered blocks with
@if
and@else
create switch and case blocks with
@switch
,@case
and@default
create loops with
@for,
and handle empty collections with the@empty
blockmigrate
ngIf
,ngFor
, andngSwitch
to the new control block syntax
This article is also available on dev.to with better source code syntax highlighting.
The full source code is available here:
https://github.com/gergelyszerovay/angular-17-control-flow
I used Angular v17.0.0-next.8 with standalone components and Signals. You can start the frontend by yarn run start
or npm run start
.
Conditionally rendered control blocks: @if
and @else
In the first example, I create a checkbox and bind it to the isChecked
signal. The signal's default value is true
, so initially the checkbox is checked, and the content of the @if
block is rendered. The examples below are from the src\app\app.component.html template 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 @if (logical_expression) {
statement creates a @if
block with a logical expression. I use the isChecked()
signal as a logical expression, as it evaluates to a boolean value.
I added an @else
block under the @if
block, it's rendered when the logical expression in the @if
block evaluates to false
, so in our case if the value of the isChecked()
signal is false
. So if I uncheck the checkbox, Angular renders the contents of the @else
block.
There is one more important thing regarding the new control block syntax: since the @
, {
and }
characters have a special meaning, we have to replace them in the text of the templates using their HTML entities. We need to use:
@
instead of@
, see the<h3>
heading in my code above{
instead of{
}
instead of}
Otherwise we get one of the following compile-time errors:
[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.)
Using @for
blocks to render items of a collection
Let's define the items
array in the component's class:
collection = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
];
We can use an @for (item of items; track item.id) {
block to render a collection's elements:
<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>
Each item in the collection needs to have a unique property (like the id
), and we need to refer to this value with the track
argument. If the collection contains strings or numbers, not objects, we can use the item
itself as a track
value: @for (item of items; track item) {
.
Next to the current item
, the @for
expression enables us to access the following values inside the block:
$index
: the index of the item in the collection$even
: true, if theindex
is even$odd
: true, if theindex
is odd$count
: the number of the items in the collection$first
: true, if the current item is the first in the collection$last
: true, if the current item is the last in the collection
Using an @empty
block to handle the empty collections passed to @for
We can add an @empty
block under the @for
block. The @empty
block's content is rendered if the collection we passed to the @for
block is empty:
<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 with @switch,
@case
and @default
In my next example, I create four radio buttons and a radioValue
signal. The signal's initial value is 1, and when the user clicks on the radio buttons, we change the signaal's value to 1
, 2
, 3
or 4
:
<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>
I nested three blocks into the @switch
block:
the contents of the
@case (1) {
block is rendered, when theradioValue()
signal equals to1
the contents of the
@case (2) {
block is rendered, when theradioValue()
signal equals to2
and the contents of the
@default {
block is rendered, when theradioValue()
signal doesn't equal to the values we specified in the@case
blocks
How to migrate ngIf
, ngFor
, and ngSwitch
to the new control block syntax
To convert the old structural directives in an app's templates to the new control blocks, run the following schematic:
ng g @angular/core:control-flow-migration
Summary
In this article, I demonstrated how the new control flow in Angular 17 is going to work: I showed you how to create conditional blocks and loops using the new control block syntax. I hope you have found my tutorial useful!
In the first part of this article series, I explain how the new deferred blocks work and how to specify conditions to trigger the loading and rendering of these blocks' content: New Angular 17 feature: deferred loading.
And as always, please let me know if you have some feedback!
👨💻About the author
My name is Gergely Szerovay, I work as a frontend development chapter lead. Teaching (and learning) Angular is one of my passions. I consume content related to Angular on a daily basis — articles, podcasts, conference talks, you name it.
I created the Angular Addict Newsletter so that I can send you the best resources I come across each month. Whether you are a seasoned Angular Addict or a beginner, I got you covered.
Next to the newsletter, I also have a publication called — you guessed it — Angular Addicts. It is a collection of the resources I find most informative and interesting. Let me know if you would like to be included as a writer.
Let’s learn Angular together! Subscribe here 🔥
Follow me on Substack, Medium, Dev.to, Twitter or LinkedIn to learn more about Angular!
how can i use @ instead of @
i am getting error for @ .. any setting need to do ?
Angular is getting more and more jsf. Great, so the switch was totally useful. #Ironie.