Angular Showcase: Control Flow

Dan Bennett
10 min readMar 14, 2024

--

What is Angular?

Angular is a development platform aimed at providing the tools needed to build high quality web applications. It is typically used within a single page application (SPA) where everything is rendered dynamically within the browser using JavaScript and any interaction with back-end system(s) is performed via web service (REST) calls. Rendering an application in this way is seen as a positive because it eliminates page “flashing” and page reloads: everything is loaded behind-the-scenes and only displayed when ready.

Angular was born out of AngularJS in 2016 when it was rebuilt to support Typescript and rebranded as Angular version 2.0. This change was huge in many ways but it has come even further since then with each major release bringing new features and conveniences. One of the key features from the latest (at the time of writing) version 17 is Control Flow.

See the official Angular documentation for more information on Angular and its history or the new interactive Angular dev guide to have a play with more of its features.

What is Control Flow?

Control Flow, or Control Flow blocks, are one of the newest features of Angular, released in version 17. This version, which includes deferrable views, makes up one of the most significant updates to come to the platform since 2016 (we’ll cover deferrable views in a later article).

The new control flow syntax brings the functionality of the classic NgIf, NgFor and NgSwitch components into the core Angular framework in a new, more readable way. It was proposed as part of this RFC as a way of improving the “developer experience” and to reduce the pain of development caused by using structural directives; NgIf, NgFor etc. within regular code/markup.

In simple terms it means a change to way we write basic display logic within the components’ HTML.

  • The *ngIf expression is replaced with @if for conditionality in the UI.
  • The ngSwitch directive is replaced with the @switch block.
  • The *ngFor expression is replaced with @for and has added support for @empty to allow placeholder code when the given iterable collection is empty.

The change from a directive style approach to explicitly defined code blocks presents several benefits, including:

  • No need for a container element or template to hold the expression, meaning less markup.
  • Support for type-safe checks within the template.
  • The tracking expression in a for loop is now mandatory but also simpler to use compared to the previous version.
  • Performance improvements due to a new optimized algorithm for calculating the minimum number of DOM operations performed in response to changes.

What does this mean?

To try and give you a better idea of what this means for a developer, below is a very simple use case of a shopping list that we need to display in a given order. To achieve this prior to Angular 17 we would use the following code to display a bullet pointed list of each item on the shopping list within our application or to simply display an appropriate message if the list is empty: note the named ng-template block “#listEmpty”:

<div *ngIf="shoppingList?.length; else listEmpty">
There {{ shoppingList.length > 1 ? 'are' : 'is' }} {{ shoppingList.length }}
item(s) on your shopping list:
<ul>
<li *ngFor="let item of shoppingList">{{ item }}</li>
</ul>
</div>

<ng-template #listEmpty>Your shopping list is empty!</ng-template>

We can update this using the new control flow syntax to clean this up and minimise unnecessary clutter, reduce the need for extra HTML markup and greatly increase code readability by separating out logic from markup, all while producing the same result as in the previous example.

@if (shoppingList?.length) {
There {{ shoppingList.length === 1 ? 'is' : 'are' }} {{ shoppingList.length }} item(s)
on your shopping list:
<ul>
@for (item of shoppingList; track item) {
<li>{{ item }}</li>
}
</ul>
} @else {
Your shopping list is empty!
}

In the above example we have replaced the directive style NgIf and NgFor commands with @if and @for which more closely represents the code inside a component class. This difference creates a clear distinction between HTML markup and display logic. We could further expand this logic and create an @else if clause as well which would have been impossible in previous versions.

Both examples above will dynamically display the following in the given situations:

Empty shopping list
Shopping list containing 1 item
Shopping list containing 4 items

Continue reading to see some more examples of the control flow syntax and follow along to give this a try for yourself.

Try it!

In this section we’ll create a simple grocery picker application to help us complete our shopping list.

Prerequisites

You must have Node JS and NPM installed and have a basic knowledge of using them. It would also be beneficial to have a working knowledge of using Angular.

If you’re new to Angular please see this guide to get you started.

Note: If you’re short on time or you’d rather just see a finished version to play with, you can checkout this example from GitHub. However, we do recommend following along with the next section, particularly if you’re relatively new to Angular 17.

Setup

The first step is to install the Angular CLI on your environment as we’ll be using this to generate our initial components.

npm i -g @angular/cli@17

Setup a new project

Now we’re ready to begin creating our project, start with `ng new` to create the initial project and name it angular-control-flow choosing these options:

ng new angular-control-flow
> Which stylesheet format would you like to use? Sass
> Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? No

Once the Angular CLI has finished setting up your new project, continue to the next section to begin using control flow syntax.

Create the project structure

Start by using the Angular CLI from the root directory to setup the initial classes. To create our new component class:

ng g component features/grocery-picker

To create our new GroceryShopService:

ng g service core/services/grocery-shop

To create the model classes that we will use for type safety:

ng g class models/grocery-shop
ng g class models/shopping-cart

This should create the following files — we can delete the unit test files (*.spec.ts) that the CLI generates for us because we won’t be using unit tests in this example:

Now that we have created the initial project structure; we can start adding some simple logic to the grocery picker.

Add logic to retrieve groceries

First we’ll configure some model type classes. Navigate to src/app/models/grocery-shop.ts and add the following code:

export declare class GroceryShop {

items: string[];
}

And then navigate to src/app/models/shopping-cart.ts and add the following code:

export declare class ShoppingCart {

items: Map<string, number>;
}

We only need to create a declaration rather than a full class here because we’re just using them to help with type safety so make sure to use “declare class” to provide a simple type instead.

Next we’ll setup our GroceryShopService to return a list of fruits and vegetables. Open the src/app/core/services/grocery-shop.service.ts class and add the following code:

@Injectable({
providedIn: 'root'
})
export class GroceryShopService {

async loadGroceryShop(): Promise<GroceryShop> {

return {
items: [
'apple (green)', 'apple (red)', 'avocado', 'banana',
'cherry', 'grapes', 'kiwi', 'lemon', 'orange',
'pear', 'pineapple', 'strawberry', 'tomato', 'watermelon'
]
};
}
}

Note: in this example we’re just going to provide mock data but in a real world example this service could retrieve the data we need from a back-end service using fetch or the Angular HttpClient and translate it for the application.

Next, open the src/app/features/grocery-picker.component.ts class. In this class we need to add a reference to a GroceryShop object and inject our GroceryShopService via the constructor.

groceryShop: GroceryShop | undefined;
shoppingCart: ShoppingCart | undefined;

constructor(private groceryShopService: GroceryShopService) {
}

async loadItems(): Promise<void> {
this.loading = true;
this.groceryShop = await this.groceryShopService.loadGroceryShop();
this.loading = false;
}

For the purposes of this example we’ll use Angular’s OnInit function call loadItems automatically when the component is initialised.

export class GroceryPickerComponent implements OnInit {
...
async ngOnInit(): Promise<void> {
await this.loadItems();
}
}

Next we need a way to choose an item to add to our shopping cart and remove an item from the cart. Add this simple function to handle this action:

chooseItem(item: string): void {
const quantity = this.shoppingCart.items.get(item);
if (quantity === undefined) {
this.shoppingCart.items.set(item, 1);
} else {
this.shoppingCart?.items.set(item, quantity + 1);
}
}

Make the page accessible, add a route definition

We’ll add a quick route so the grocery picker component is accessible. Open the src/app/app.routes.ts file and add the following into the routes array:

{ path: 'grocery-picker', component: GroceryPickerComponent }

Display the grocery picker

Now that we have completed our setup, we can write the markup to display this and test out some of the features of the control flow syntax. Open the src/app/features/grocery-picker.component.html file (it is possible to include the markup directly inside the .ts file but it’s cleaner to keep this separation where possible).

<h1>My Grocery Shop</h1>

<div id="groceries">
@if (groceryShop) {
<div id="list”>
Shelf 🍅🍇🍒
<ul>
@for (item of groceryShop.items; track item) {
<li>
<button (click)="chooseItem(item)">+ </button>
{{ item }}
</li>
}
</ul>
</div>
<div id="cart">
Cart 🛒
<ul>
@for (item of shoppingCart.items.keys(); track item) {
<li>
<button (click)="removeItem(item)">- </button>
{{ item }} ({{ shoppingCart.items.get(item) }})
</li>
} @empty {
<p>Your cart is empty</p>
}
</ul>
</div>


} @else {

<p>Sorry, we’re closed! Please come back later.</p>
}
</div>

In this example, we have used the @if and @for control flow blocks to create a “shelf” of items that are available from the grocery shop and a shopping cart that we can add items to.

Finally, add the following styles to the stylesheet at src/app/features/grocery-picker.component.sass to make it look nice:

button
border: 0
outline: 0
cursor: pointer
color: white
background-color: rgb(48, 124, 173)
border-radius: 4px
font-size: 14px
font-weight: 500
padding: 4px 8px
display: inline-block
min-height: 28px

#groceries
display: flex
flex-direction: row
&> div
padding: 1rem

&:first-child
border-right: 1px dashed

ul
padding-left: 0

li
list-style-type: none
padding-top: .5rem

To test this out, we can use a built command from the Angular CLI:

ng serve

This command will load the application in a temporary server and automatically refresh the browser whenever any changes are detected.

Open your browser and navigate to http://localhost:4200/grocery-picker and the grocery picker application should look something like this:

Let’s expand this a little. Firstly add an artificial delay when loading the grocery shop. Update the GroceryShopService so that the loadGroceryShop function matches this:

async loadGroceryShop(): Promise<GroceryShop> {

return new Promise((resolve, reject) =>
setTimeout(
() => {
resolve({ items: [
'apple (green)', 'apple (red)', 'avocado', 'banana',
'cherry', 'grapes', 'kiwi', 'lemon', 'orange',
'pear', 'pineapple', 'strawberry', 'tomato', 'watermelon'
] });
},
2000 // Timeout of 2 seconds to simulate loading from a back-end service
)
);
}

Update the component HTML to add a new condition which displays a message while waiting for the grocery shop items to be loaded. We’ll also add an “Empty” button to the cart for convenience, only displayed when there are items inside the cart to remove.

<h1>My Grocery Shop</h1>

<div id="groceries">
@if (!loading && groceryShop) {
<div id="list”>
Shelf 🍅🍇🍒
<ul>
@for (item of groceryShop.items; track item) {
<li><button (click)="chooseItem(item)">+ </button> {{ item }} </li>
}
</ul>
</div>
<div id="cart">
Cart 🛒
@if (shoppingCart.items.size) {
<button (click)="clearCart()">Empty</button>
}

<ul>
@for (item of shoppingCart.items.keys(); track item) {
<li>
<button (click)="removeItem(item)">- </button>
{{ item }} ({{ shoppingCart.items.get(item) }})
</li>
} @empty {
<p>Your cart is empty</p>
}
</ul>
</div>


} @else if (loading) {

<p>Please wait while the grocery shop is loading... </p>

} @else {

<p>Sorry, we're closed! Please come back later.</p>
}
</div>

The grocery picker should look something like this after you’ve added some items to the cart:

We can go a little further and add some colour using a switch statement. Open the file grocery-picker.component.html file again and update the HTML to add a new control flow @switch block that we can use to determine which Unicode character to display for a given item on the shelf.

<h1>My Grocery Shop</h1>

<div id="groceries">
@if (!loading && groceryShop) {
<div id="list”>
Shelf 🍅🍇🍒
<ul>
@for (item of groceryShop.items; track item) {
<li><button (click)="chooseItem(item)">+ </button>
@switch (item) {
@case ('apple (green)') { 🍏 }
@case ('apple (red)') { 🍎 }
@case ('avocado') { 🥑 }
@case ('banana') { 🍌 }
@case ('cherry') { 🍒 }
@case ('grapes') { 🍇 }
@case ('kiwi') { 🥝 }
@case ('lemon') { 🍋 }
@case ('orange') { 🍊 }
@case ('pear') { 🍐 }
@case ('pineapple') { 🍍 }
@case ('strawberry') { 🍓 }
@case ('tomato') { 🍅 }
@case ('watermelon') { 🍉 }
@default { ◼ }
}

{{ item }} </li>
}
</ul>
</div>
<div id="cart">
Cart 🛒
@if (shoppingCart.items.size) {
<button (click)="clearCart()">Empty</button>
}

<ul>
@for (item of shoppingCart.items.keys(); track item) {
<li>
<button (click)="removeItem(item)">- </button>
{{ item }} ({{ shoppingCart.items.get(item) }})
</li>
} @empty {
<p>Your cart is empty</p>
}
</ul>
</div>


} @else if (loading) {

<p>Please wait while the grocery shop is loading... </p>

} @else {

<p>Sorry, we're closed! Please come back later.</p>
}

Now that we have added a relevant icon for each of the available items on the shelf using the switch case; the final application should look like this:

Summary

In this article we have demonstrated how to use the new Control Flow syntax in Angular 17 including:

- @if, @else if and @else

- @for and @empty

- @switch and @case

This new syntax may cause a bit of a headache for developers who are upgrading from a previous version of Angular and want to adopt this approach, but overall it makes the code a lot more readable and thus easier to maintain. It also aligns the logic within the template markup more closely with the logic within the component itself, so may be less of a learning curve for developers brand new to Angular.

In the next post in our Angular series, we’ll be talking about the Angular Signals system and how it can be utilised to improve state tracking at runtime.

--

--

Dan Bennett
Dan Bennett

Written by Dan Bennett

0 Followers

Head of Software Developer at Tier 2 Consulting

No responses yet