Understanding control flow syntax in Angular 17

leemeganj

Megan Lee

Posted on March 20, 2024

Understanding control flow syntax in Angular 17

Written by Lewis Cianci
✏️

As software developers, we’re subjected to an ever-changing landscape of tools, best practices, and ways of doing things. Sometimes, when a change is introduced, it’s tempting to stay with old habits. It could be that the old way is more intuitive or easier, but often, it’s just that we don’t want to learn and remember yet another new thing.

A good example of this is the new control flow syntax that has been added in Angular 17. The reality is that it’s a huge new feature that offers significant benefits to pretty much any Angular developer. But to learn how to use it, we have to dive into the value of what control flow syntax can do for us.

The problem with Angular before control flow syntax

One of Angular’s hallmarks — even in the AngularJS days — was that, within its HTML templates, everything could be controlled through HTML tags. This made writing Angular applications intuitive, since if you understood the HTML, you could naturally leverage that skill to understand how Angular worked.

This was a good approach for most of the features within Angular. One area where this approach started to break down, however, was in comparatively simple operations like conditional rendering, or even for...of loops that could render multiple items in an array.

Even in TypeScript, the language that backs Angular, these operations were far simpler and more intuitive than they ever were in Angular’s HTML templates. For example, in TypeScript, a conditional operation would look like this:

if (booleanCondition){
  // if true
}
else
{
  // if false
}
Enter fullscreen mode Exit fullscreen mode

Whereas in Angular, a similar operation would look like this:

<div *ngIf="booleanCondition">
  true
</div>
<div *ngIf="!booleanCondition">
  false
</div>
Enter fullscreen mode Exit fullscreen mode

The natural flow of "if this, do this; otherwise, do that" is lost. Instead, we have to negate the booleanCondition manually to show something else.

Angular does have an option to use something like an else statement, but in my opinion, it’s never been easy to use and is so overly verbose and inflexible that I never actually used it:

<div *ngIf="booleanCondition; else elseBlock">Condition is true</div>
<ng-template #elseBlock>Condition is false</ng-template>
Enter fullscreen mode Exit fullscreen mode

Straightaway, the relationship between the if and the else block is not clear. We have the condition written in our ngIf attribute, but it calls out to a different HTML tag.

As soon as the page approaches any level of complexity, trying to dig through the HTML to find what is going to be rendered becomes difficult. This is only exacerbated by else...if rendering:

<div *ngIf="condition; then thenBlock else elseBlock"></div>
<ng-template #thenBlock>It's true.</ng-template>
<ng-template #elseBlock>It's false.</ng-template>
Enter fullscreen mode Exit fullscreen mode

In cases where you want to render something different based on something like an enum, having to write a different ng-template becomes tiring quickly. And once other components are added to the page, it only becomes harder to track what’s going where, and what’s rendering.

Writing switch cases also becomes unwieldy for similar reasons:

<div [ngSwitch]="switchVariable">
  <div *ngSwitchCase="'one'">one</div>
  <div *ngSwitchCase="'two'">two</div>
  <div *ngSwitchDefault>It's not one or two...</div>
</div>
Enter fullscreen mode Exit fullscreen mode

To an extent, a similar problem affects for...of loops within Angular. To iterate over a collection and then render that collection, the ngFor attribute is used. It can only be placed on a HTML node:

<li *ngFor="let item of array">List item to repeat</li>
Enter fullscreen mode Exit fullscreen mode

Granted, this isn’t as bad as the if...else example at the outset. But it’s always felt strange using words like let in our HTML markup.

Within TypeScript, it makes more sense, but in the HTML, less so. And, again, it can make it harder to read in larger projects. The code in the ngFor attribute is called microsyntax, but because it’s not in our TypeScript code, it can make it harder to refactor and inspect in the future.

The future: Control flow syntax

In June 2023, the Angular team raised a new RFC to implement control flow syntaxes within Angular. They gave the following rationale for introducing control flow syntax:

"Our review of developer experience pain points in Angular has highlighted microsyntax-based control flow as having significant weaknesses compared to syntaxes in other frameworks. The proposed built-in control flow syntax addresses these issues and significantly improves the developer experience, in addition to being a foundation for new features."

Control flow syntax is essentially just bringing the ergonomics of the standard if...else within the Angular template. Instead of using microsyntax — the little bits of code in the ngFor and ngIf attributes — we can use if...else statements that would make more sense to most developers.

It also saves us from digging through multiple ng-template elements, as the control flow occurs outside of these HTML nodes. With it, our simple example at the outset of conditionally rendering a HTML node becomes as simple as this:

@if (booleanCondition){
  true
}
@else {
  false
}
Enter fullscreen mode Exit fullscreen mode

Immediately, our code becomes easier to read and interpret. So, let’s take the control flow for a spin and understand the benefits of control flow syntax for us.

if...else statements

Before control flow syntax was added in Angular 17, the only way to achieve conditional rendering was to use ngIf on the HTML node that you wanted to render. As we have just seen in the example above, this is now a lot simpler.

What about if...else statements though? Let’s see:

@if (booleanCondition){
  true
} @else if (otherBoolean){
  other boolean is true
}
@else{
  neither were true
}
Enter fullscreen mode Exit fullscreen mode

In the case of multiple conditions that you would like to check, simply add more else if statements, like you would in traditional programming languages.

However, if you find yourself doing this a lot, it’s likely better to use a switch case. Using switch cases really become powerful when you combine them with using enums, as you benefit from having the conditions strongly typed themselves.

Switch cases

Let’s look at how switch cases work. First, let’s define those enums in our component:

export enum LoginState{
  LoggedOut,
  LoggedIn,
  Expired,
  ProfileNeedsUpdate,
}
Enter fullscreen mode Exit fullscreen mode

Next, let’s reference this from our component so we can address it in our template:

loginState = LoginState;
Enter fullscreen mode Exit fullscreen mode

Doing this might seem strange. Why are we referencing the enum from our component in this way? In our template, it means we can do this:

@switch (LoginState)
{
  @case (LoginState.LoggedIn)
  {
    Logged in
  }
  @case (LoginState.LoggedOut)
  {
    Logged out
  }
  @case (LoginState.Expired)
  {
    Expired
  }
  @case (LoginState.ProfileNeedsUpdate)
  {
    Your profile needs updating!
  }
}
Enter fullscreen mode Exit fullscreen mode

By combining switch cases, the control flow syntax, and a simple enum, we have a strongly typed code path that is easy to understand. We don’t need to rifle through the template trying to find what nodes we’re referencing.

All around, it’s just a lot easier to understand what’s happening here. This ease of use extends to for loops within Angular as well.

for loops

Before using control flow in Angular, for loops would be based on ngFor placed on a HTML node. They were quite simple to use, as you could iterate over any array or iterable to render a list of items.

However, within this simplicity lay a simple problem that could cause performance headaches for Angular developers. There was no requirement by default to identify how Angular should track each object. Because of this, Angular would have to use the whole object to check for equality.

On short lists with small objects, this wasn’t such a big problem. However, as projects grew in size and complexity, developers could easily iterate over bigger and bigger arrays with bigger and bigger objects.

With no specific property to track objects with, Angular would continue to compare based on object equality. When arrays were updated, or views were updated, ngFor would have to manually piece through every item in the array.

Using control flow syntax for for loops within Angular now requires that you specify the track parameter so Angular knows how to track each individual object in the for loop. This means that Angular doesn’t have to compare the entire object — just the property that you specify. This leads to improved performance.

To briefly demonstrate this, let’s imagine that we have an interface that defines some simple information for a restaurant. We’re just going to define the name of the place, the rating, and a database-assigned id:

export interface FoodPlace{
  name: string,
  rating: number,
  id: number,
}
Enter fullscreen mode Exit fullscreen mode

If we have an array of these items that we want to iterate through, we can use the new @for loop to achieve that:

@for (place of foodPlaces; track place.id){
  {{place.name}}
}
Enter fullscreen mode Exit fullscreen mode

Here, you can see that we’re using the id property on the restaurant object to track the object. Imagining that this id is a database-assigned integer, or GUID, it’s not hard to understand how these objects would be less taxing for Angular to track.

It would be reasonable for the for functionality to stop there. However, there’s quite a bit of syntactic sugar for developers who want to know more information about the list they are iterating over, without writing custom functions:

Variable Description
$index The current iterator index (number)
$first Whether the current iterator is the first item in the array (boolean)
$last Whether the current iterator is the last item in the array (boolean)
$even Whether the current iterators index is even (boolean)
$odd Whether the current iterators index is odd (boolean)

If we wanted to use all of them, our for loop would look like this:

@for (place of foodPlaces; track place.id; let i = $index; let first = $first; let last = $last; let even = $even; let odd = $odd){
  {{place.name}}
  Index: {{i}}
  Is first?: {{first}}
  Is last?: {{last}}
  Is iterator index even?: {{even}}
  Is iterator index odd?: {{odd}}
}
Enter fullscreen mode Exit fullscreen mode

It’s certainly quite exhaustive, and reads like plain English to the developer. Some things could have been left to the developer, like not including the odd variable, and instead just using something like !even. But it feels thorough, which is nice.

But that’s not all! We also have access to the @empty parameter, which can render if our array is empty:

@for (place of foodPlaces; track place.id){
  {{place.name}}
}
@empty{
  No restaurants in the list
}
Enter fullscreen mode Exit fullscreen mode

Migrating to control flow in Angular

All this new functionality in Angular 17 is exciting, but manually migrating your code across can be a real pain. Fortunately, there’s a migration tool that can do a lot of the heavy lifting for you.

Before using it, check your code and make a new branch so you don’t accidentally trash your codebase. Once you’ve done all that, simply run the below from your terminal:

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

Follow the prompts, and your project should be updated with the control flow goodness. Since some projects can be pretty complex, some may need some manual tweaking after the migration is run.

Wrapping up

Control flow syntax provides a new and more intuitive way of doing things in Angular. It’s sparking a lot of newfound interest in creating projects in Angular.

If you’re like me, Angular is already pretty exciting — but I’m also excited to see what new features Angular 18 and 19 bring in. Here’s to the future!


Experience your Angular apps exactly how a user does

Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Angular apps — start monitoring for free.

💖 💪 🙅 🚩
leemeganj
Megan Lee

Posted on March 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related