Control Flow Syntax in Angular

Published on Nov 18, 2024

Introduction

In Angular 17, a new way to control the layout and flow of your web app was introduced. @if was introduced to conditionally show or hide content, @for was introduced to loop over arrays of data, and @switch was introduced to determine the content like you do with any switch statement. Prior to this, we used the *ngIf, *ngFor, and *ngSwitch structural directives for this purpose. While there is no functional difference in the different methods, I have found that the developer experience of the new control flow syntax has been better, and provides some nice improvements in the process.

@if

First let's look at the new @if control flow. This is used to conditionally show or hide content on the page, similar to what we've done as Angular developers for years. Here's a sample of what it looks like in your template:

@if (condition) {
    <h1>This heading will show</h1>
}

This is simple, but if the condition evaluates to true, the h1 will be included in the document. If the condition changes during the lifetime of the component, the content is updated accordingly. You can also easily add if else or else conditions:

@if (condition) {
    <h1>This heading will show</h1>
} @if else (condition2) {
    <h1>Or this heading will show</h1>
} @else {
    <h1>Here's the default heading</h1>
}

When using the *ngIf directive, there wasn't an easy way to do if/else options. You could do it, but the else clause needed to be an ng-template. It's not that this was hard, exactly, but I seemed to have to look it up every time. This new syntax is so much more intuitive than it was before.

Another benefit of the new syntax is that we no longer need to add in extra HTML to the document for the *ngIf directive. Take the following example of HTML code with the directive vs the new syntax:

<div *ngIf="condition">
    <p>This should be shown</p>
    <p>As should this sibling</p>
</div>

@if (condition) {
    <p>This should be shown</p>
    <p>As should this sibling</p>
}

In the above example we have two paragraph tags that we want to display. With the directive, we have to wrap them in an extra div, or ng-container, or duplicate the directive on both paragraphs. With the new syntax, we just put both tags inside the curly brackets. This simplifies the resulting HTML and reduces the complexity of this particular situation.

Another bonus is that with the new control flow syntax, there is no need to import anything to conditionally show and hide anything. With the old directives, you needed to import the NgIf directive or the CommonModule. The new syntax is automatically understood with no imports needed.

Lastly, @if does offer slight improvements in terms of performance, although it might be minimal in most cases. @if is built in to Angular itself, so there is no need to import or instantiate a directive, as was needed previously.

Final Thoughts on @if

When this new option came up for control flow, I didn't really see the benefit. I'd been using *ngIf for so long that I didn't really care to change. But after using the new syntax I've found it so much more convenient and intuitive. I can't remember the last time I didn't use it, and am constantly refactoring old code to use the new syntax.

@for

THe next control flow syntax improvement is the for loop. This is the ability to loop over a list of items and display them on the screen. The *ngFor directive was previously used, and worked well, but similar to the @if syntax, @for provides a more intuitive, developer-friendly way to loop over the items. Here's an example of the new syntax:

<ul>
    @for(item of items; track item.id) {
        <li>{{ item.displayName }}</li>
    }
</ul>

The new syntax is straight forward; you loop over a list of items very much like you did previously. It does provide a couple benefits over the directive, though. One is that just like @if, you have more freedom about what you are duplicating on the page, and don't need to add extra HTML to the document flow. Maybe each item should output two li elements on the page. This new syntax makes that really easy. Another big difference is that the track expression is required. It was optional before, and beneficial for performance, but it is required with @for. It's simple though, as you just need to give it a field that is unique for each item.

Another great benefit comes when your list is empty. Look at the following example:

<ul>
    @for (item of items; track item.id) {
        <li>{{ item.displayName }}</li>
    } @empty {
        <li>There are no items.</li>
    }
</ul>

In this situation, we'll continue to output the items on the screen, but if the array is empty we automatically get that output on the screen. No more manually handling that situation or using an extra piece of control flow syntax.

There are also still contextual variables available for use, such as $index, $first, $last, $even, $odd, and $count. You can use them directly, or alias them:

<ul>
    @for (item of items; track item.id; let idx = $index) {
        <li>Item {{ idx }}: {{ item.displayName }}</li>
    } @empty {
        <li>There are no items.</li>
    }
</ul>

This for loop syntax offers many of the same benefits that were mentioned above for @if. Because it's a built-in part of Angular, it can be more performant than ngFor, especially because track is now required. If you were using trackBy with ngFor, the performance benefits might not be as noticeable, but you'll still benefit from the developer experience and simplicity.

Final Thoughts on @for

Just like with @if, I didn't really see the benefit at first of @for. But the simplicity and how intuitive it is have won me over.

@switch

The last one we'll talk about here is the new @switch control flow syntax. I'll be honest, I don't use this often, but I avoided the *ngSwitch directive like the plague. I would find any other way to show the data, but sometimes switch statements just work the best. The new syntax is much easier to remember and use. Here's an example of the new and old methods:

<container-element [ngSwitch]="switch_expression">
  <p *ngSwitchCase="match_expression_1">...</p>
  <p *ngSwitchCase="match_expression_2">...</p>
  <p *ngSwitchCase="match_expression_3">...</p>
  <p *ngSwitchDefault>...</p>
</container-element>

@switch (condition) {
  @case (caseA) {
    Case A.
  }
  @case (caseB) {
    Case B.
  }
  @default {
    Default case.
  }
}

The result is effectively the same, but you can put any content you want in each case without adding extra HTML to the page, as mentioned on other control flow options, and it's easier to remember. In the above example, caseA is a variable in your component.

Final Thoughts on @switch

While this is the control flow syntax option I use least frequently, it is nice to have when needed.

Conclusion

These new additions to the framework have little impact, really, on the resulting applications that we develop, but it shows that the Angular team is trying to make the developer experience as nice as possible and want the framework to evolve in meaningful ways. Things aren't stagnating, they're getting better still with every release. I definitely recommend to everyone to start using the new syntax. Update your old code, either manually or with the provided migration script. Look for ways your code can benefit from advancements in the framework as we continue to move forward.