Declarative Loop Control Flow in Angular 17
Tags: angular, javascript, typescript, frontend
Published: 2023-10-20 18:01:43

In the last few releases, a relatively short time, we witnessed a lot of additions and improvements to the Angular framework, and the future seems to be no worse. Angular Renaissance or Momentum, the next version (v17) will come with some more high qualitative improvements, one amongst them being the new built-in **Control Flow Template Syntax**. 

As the name suggests, this improvement, introduces the **built-in** control flow for templates, a new **declarative** syntax of writing control flow in the template, thus providing the functionality of `*ngIf` `*ngFor`, and `*ngSwitch` (directive-based control flow) into the framework itself.

The template syntax has been debated for some time, with the Angular team and community providing their solutions, and after careful reasoning and their trust in the community, the `@-syntax` (community proposal) was chosen to back the template control flow.

> You can read more about it (reason, benefits, implications … etc) in the [Angular RFC: Built-in Control Flow](https://github.com/angular/angular/discussions/50719) and template syntax choice in the Angular [blog post](https://blog.angular.io/meet-angulars-new-control-flow-a02c6eee7843).

Loop control flow `@-for`, is what grabbed my attention the most, because of the new supporting `@-empty` block that shows a template when no items are in the list:
```
@for (product of products; track product.title) {
  <tr>
    <td>{{ product.title }}</td>
    <td>{{ product.description }}</td>
    <td>{{ product.price }}</td>
    <td>{{ product.brand }}</td>
    <td>{{ product.category }}</td>
  </tr>
} @empty {
  <p>No products added yet!</p>
}
```
This new template syntax allows removing all the `ng-container` and `ng-template` elements on the template that supported `*ngIf` and `*ngFor` making templates more compact and offering a better user experience.

In this article, I want to show how a _for_ loop code looks now in the template after using built-in control flow syntax, and a misconception I created for the `@-empty` block when iterating over data loaded asynchronously (**observable** results, or **read-only** signal values from the **toSignal** function). Let's dive into an example!

## Hands-on! 🐱‍🏍
This demo will have a `Products` component that renders a list of products into a table. Below you see the iteration being done using the current `*ngFor` structural directive:
```
import { Component, inject } from '@angular/core';
import { AsyncPipe, CommonModule } from '@angular/common';
import { ProductService } from '../product.service';

@Component({
  selector: 'app-products',
  standalone: true,
  imports: [CommonModule, AsyncPipe],
  template: `
    <table>
      <thead>
        <tr>
          <th>Title</th>
          <th>Description</th>
          <th>Price</th>
          <th>Brand</th>
          <th>Category</th>
        </tr>
      </thead>

      <tbody>
        <ng-container *ngIf="products$ | async as products">
          <ng-container *ngIf="products.length; else noResults">
            <tr *ngFor="let product of products">
              <td>{{ product.title }}</td>
              <td>{{ product.description }}</td>
              <td>{{ product.price }}</td>
              <td>{{ product.brand }}</td>
              <td>{{ product.category }}</td>
            </tr>
          </ng-container>

          <ng-template #noResults>
            <p>No results yet!</p>
          </ng-template>
        </ng-container>
      </tbody>
    </table>
  `,
  styleUrls: ['./products.component.scss'],
})
export class ProductsComponent {
  products$ = inject(ProductService).getProducts(); 
}
```
You can see the `ng-template | ng-container` elements used, we first check for a **null** case, guarding against the initial **null** value emitted from the async pipe, making sure we get the result from the products observable when emitted, and then check if the list is empty to render the default message or render the products in the table.

Now you correctly ask, what would it look like with the new `@-for` control flow 🤔?

Look at this code 😍:
```
...

@Component({
  ...
  template: `
    <table>
      ...

      <tbody>
        @if (products$ | async; as products) {
          @for (product of products; track product.title) {
           <tr>
             <td>{{ product.title }}</td>
             <td>{{ product.description }}</td>
             <td>{{ product.price }}</td>
             <td>{{ product.brand }}</td>
             <td>{{ product.category }}</td>
           </tr>
          } @empty {
           <p>No results yet!</p>
          }
        }
      </tbody>
    </table>
  `,
  ...,
})
export class ProductsComponent {
  products$ = inject(ProductService).getProducts(); 
}
```
No `ng-container | ng-template` elements are present. It is the new `@-for` and `@-empty` in combination that removes the need to check if the products list is empty and what template to render based on that, default message or products table hence no more "**imperative**" check.

And it's the `@-if` block that does the check for the **null** value emitted from the async pipe. Also, the new syntax enforces the use of `track`, a function that improves performance. Thus we have less, cleaner code, more performant, and easy to understand, read, and write.

### My misconception for @-empty block 😁
When I first started experimenting with the new Control Flow, I was expecting the `@-empty` block in combination with `@-for` block to be working under the hood like **for await of** statement in JavaScript -  in the sense that if the data to be iterated loads asynchronously as the result of an observable or a read-only signal created by toSignal function, it would wait until the data is loaded and then decide if the `@-empty` block renders or not.

In short, I thought we did not need to check for a **null** value when waiting for an observable to emit:
```
...

@Component({
  ...
  template: `
    <table>
      ...
      <tbody>
        // no @if check here...
        @for (product of products$ | async; track product.title) {
          <tr>
            <td>{{ product.title }}</td>
            <td>{{ product.description }}</td>
            <td>{{ product.price }}</td>
            <td>{{ product.brand }}</td>
            <td>{{ product.category }}</td>
          </tr>
        } @empty {
          <p>No results yet!</p>
        }
      </tbody>
    </table>
  `,
  ...,
})
export class ProductsComponent {
  products$ = inject(ProductService).getProducts(); 
}
```
or when reading from a read-only signal:
```
...

@Component({
  ...
  template: `
    <table>
      ...
      <tbody>
        // no @if check here...
        @for (product of products(); track product.title) {
          <tr>
            <td>{{ product.title }}</td>
            <td>{{ product.description }}</td>
            <td>{{ product.price }}</td>
            <td>{{ product.brand }}</td>
            <td>{{ product.category }}</td>
          </tr>
        } @empty {
          <p>No results yet!</p>
        }
      </tbody>
    </table>
  `,
  ...,
})
export class ProductsComponent {
  products = toSignal(this.productService.getProducts(), { initialValue: null });
}
```
But just like the if/else statement of any programming language that works on a sync sequence of values, the same stands for the `@-for` control flow in templates.

What happens in this case, is that first the `@-empty` block with the "No results yet!" message is rendered, and then after the data comes from the server, the products table is rendered:

![Demo how it works with no @if check first](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3azmp9gfk8kj5mxz3vtq.gif)

During the time RFC was open for review, I left a **[comment](https://github.com/angular/angular/discussions/50719)** asking if an optional condition could be added to the `@-empty` block ({: empty} at the time of comment was left) to make it able to wait until data is loaded. The Angular team cares about its community and takes into consideration their input, and it may do something for this behavior in the near future.

> **FYI**: Angular v17 is officially in a [release candidate](https://github.com/angular/angular/releases/tag/17.0.0-rc.0) phase now. Feel free to grab and experience its new features.

Special thanks to @kreuzerk , @danielglejzner and [@eugenioz](https://twitter.com/eugeniyoz)

### Thanks for reading!

I hope you enjoyed it 🙌. If you liked the article please feel free to share it with your friends and colleagues.

For any questions or suggestions, feel free to comment below 👇.

If this article is interesting and useful to you, and you don't want to miss future articles, follow me at [@lilbeqiri](https://twitter.com/lilbeqiri), [dev.to](https://dev.to/ilirbeqirii), or [Medium](https://lilbeqiri.medium.com/). 📖