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.
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 }
Whereas in Angular, a similar operation would look like this:
<div *ngIf="booleanCondition"> true </div> <div *ngIf="!booleanCondition"> false </div>
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>
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>
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>
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>
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.
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 }
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
statementsBefore 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 }
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.
Let’s look at how switch cases work. First, let’s define those enums in our component:
export enum LoginState{ LoggedOut, LoggedIn, Expired, ProfileNeedsUpdate, }
Next, let’s reference this from our component so we can address it in our template:
loginState = LoginState;
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! } }
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
loopsBefore 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, }
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}} }
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}} }
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 }
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
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.
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!
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 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]