AGS Logo AGS Logo

Angular: Upgrading to the Control Flow Syntax

Three stop lights on a yellow textured background. One red with an arrow point left, and 1 green with arrows pointing forward.

Photo by Etienne Girardet on Unsplash

Control flow block templates were introduced in Angular 17. Like their structural directives counterparts (deprecated in Angular 20), they allow us to control how to show, hide, or repeat elements. Let's look at the differences between the two types and how to migrate them.

*ngIf versus @if and @else

When we want to show or hide elements based on a particular condition, we can use if statements. Figure 1 shows how we can use *ngIf to conditionally show text based on the user's salary.

Conditionally showing content using *ngIf
<div class="salary">
  <span class="title">Salary: </span>
  <ng-container *ngIf="user.salary < 75_000">less than 75,000 </ng-container>
  <ng-container *ngIf="user.salary >= 75_000 && user.salary < 100_000">77,000 - 100,000</ng-container>
  <ng-container *ngIf="user.salary >= 100_000">greater than 100,000</ng-container>
</div>

Converted to the block template syntax, the same code looks as follow (Figure 2)

Conditionally showing content using @if(), @else if(), and @else()
<div class="salary">
  <span class="title">Salary: </span>
  @if (user.salary < 75_000) { less than 75,000 }
  @else if (user.salary < 100_000) { 77,000 - 100,000 }
  @else { greater than 100,000 } 
</div>

Notice the introduction of @else if @else which was not available with the directives. Although we could do an if / else with the use of <ng-template> (listing 3), the ability to do else if was much harder to achieve.

If / Else using *ngIf and <ngTemplate>
<div class="salary">
  <span class="title">Salary: </span>
  <ng-container *ngIf="user.salary < 75_000; then low else high" />
  <ng-template #low>less than 75,000</ng-template>
  <ng-template #high>greater than or equal to 75,000</ng-template>
</div>

Another use of if statements was to get the value of a property such as an observable and to assign said value to a variable. Listing 4 for shows us getting the value from an observable users$ and assigning it to a variable users.

Using *ngIf in conjunction with as
<div *ngIf="users$ | async as users">
  {{ users.length }} Users
</div>

With the block syntax, the same code looks as follows as seen in Listing 5. The syntax is very similar the addition of a semicolon between the parameter being evaluated and the assignment to the variable.

Using @if() in conjunction with as
@if (users$ | async; as users) {
  <div>
    {{ users.length }} Users
  </div>
}

[ngSwitch] versus @switch

When we have many different conditions results we want conditionally display content against, another option is to use a switch statement. Listing 6 shows conditionally shows a user's status based on their employment status using the directive driven approach. If the status is not found in the list of cases, the switch will display the default value of "Unknown".

Switch statement using [ngSwitch], *ngSwitchCase, and *ngSwitchDefault
<div [ngSwitch]="employmentStatus[user.status]" class="status">
  <ng-container *ngSwitchCase="'unemployed'">Inactive</ng-container>
  <ng-container *ngSwitchCase="'consultant'">Active</ng-container>
  <ng-container *ngSwitchCase="'employee'">Active</ng-container>
  <ng-container *ngSwitchCase="'retired'">Retired</ng-container>
  <ng-container *ngSwitchDefault>Unknown</ng-container>
</div>

Notice that for both the consultant and employee statuses we repeat the contents of <ng-container>. If we want to do a case statement that applies to a OR b, we can set the [ngSwitch] property to trigger on true and then put a condition in the ngSwitchCase as seen lin listing 7.

Switch case with multiple conditions
<div [ngSwitch]="true" class="status">
  <ng-container *ngSwitchCase="employmentStatus[user.status] === 'unemployed'">Inactive</ng-container>
  <ng-container *ngSwitchCase="
    employmentStatus[user.status] === 'consultant'
    || employmentStatus[user.status] === 'employee'
  ">
    Active
  </ng-container>
  <ng-container *ngSwitchCase="employmentStatus[user.status] === 'retired'">Retired</ng-container>
  <ng-container *ngSwitchDefault>Unknown</ng-container>
</div>

The same remains true with the block syntax shown in listing 8. We set the switch statement to a value of true and our case statements are assigned the code to evaluate.

Switch statement using block syntax
<div class="status">
  @switch (true) {
    @case(employmentStatus[user.status] === 'unemployed') { Inactive }
    @case(
      employmentStatus[user.status] === 'consultant' 
      || employmentStatus[user.status] === 'employee'
    ) { 
      Active
    }
    @case(employmentStatus[user.status] === 'retired') { Retired }
    @default { Unknown }
  }
</div>

*ngFor vs @for

To iterate over lists of content we can use a for loop. Listings 9 and 10 show how to iterate over a list of users as well how to assign a track by function.

Using *ngFor to iterate over a list of users
<ul class="users">
  <li *ngFor="let user of users$ | async; trackBy: trackById; let i = index">
      {{ i + 1 }}: {{ user.firstName }} {{ user.lastName }}
    </div>
  </li>
  <li *ngIf="(users$ | async)?.length === 0" class="no-data">No users found</li>
</ul>
TrackBy function used with *ngFor in Listing 9
public trackById(index: number, user: User): number {
  return user.id
}

Converted to the block syntax, we get the following (Listing 11):

Using @for() to iterate over a list of users
<ul class="users">
  @for (user of users$ | async; track user.id; let i = $index;) {
    <li>
      {{ i + 1 }}: {{ user.firstName }} {{ user.lastName }}
    </li>
  } @empty {
    <li class="no-data">No users found</li>
  }
</ul>

Track

Note that trackBy: becomes track and that we can have it reference a property in the object directly, we no longer have to rely on a function.

Although not mandatory using *ngFor, track does become required with the use of control flow blocks (@for). The new requirement is important because track is used to optimize performance when handling data changes and allows Angular to maintain a relationship between the data and the DOM to minimize DOM operations when the data changes.

For more about track checkout: Why is track in @for blocks important?.

@empty()

Also notice the use of @empty(). This is a new feature that provides a fallback for when there are no elements in the list. This includes when the list is null or undefined.

Contextual variables

In both the directive and block based examples, we also have a context variable i to represent the index of the item in the list. Table 1 lists the available contextual variables with @for().

Table 1: Contextual variable in @for() blocks
VariableTypeMeaning
$countnumberTotal number of items in the list
$indexnumberIndex of the current item
$firstbooleanWhether the current item is the first item of the list
$lastbooleanWhether the current item is the last item of the list
$evenbooleanWhether the current item's index is even
$oddbooleanWhether the current item's index is odd

To define multiple context variables using @for(), we separate them with semi-colons (Listing 12).

Using multiple contextual variables with @for()
<ul class="users">
  @for (user of users$ | async; track user.id; let i = $index; let even = $even; let count: $count) {
    <li [ngClass]="{ 'stripe': even }">
      {{ i + 1 }} of {{ count }}: {{ user.firstName }} {{ user.lastName }}
    </li>
  }
</ul>

Migrating to the Control Flow Syntax using the CLI

When upgrading a project to Angular 20 using ng upgrade, the CLI will ask us wether we want to upgrade the project to use the Control FLow syntax. Another option is to run the following command:

Command to upgrade to the Control Flow Syntax
ng generate @angular/core:control-flow

Regardless of the whether we migrate as part of upgrading to Angular 20 or we run the above command, we will need to then manually go through our code to handle breaking changes with @for() and make sure that the track property automatically assigned as part of the update is sensible and unique.

Tips for choosing track values:

  • Must be unique
  • Commonly used values include properties such as IDs, or UUIDs
  • For static / non-editable lists, we can use the $index
  • Avoid using objects as this can lead to significantly slower rendering of updates

Closing Remarks

In this article we covered the differences in syntax between structural directives versus block control flow and how to migrate from one to the other. A project that includes all of the concepts in this article can be found on Github at: github.com/martine-dowden/control-flow.

Happy Coding!

Angular

Angular is a robust web application framework that is modern and scalable, facilitating rapid development of reliable, performant applications.

References

License: CC BY-NC-ND 4.0 (Creative Commons)