Experience the magic of the new control flow in Angular 17
Tags: angular, tutorial, webdev
Published: 2023-12-08 11:24:33

##Introduction

In this blog post, I demonstrated the migration of Angular 16 structure directives (NgIf and NgSwitch) to Angular 17 new control flow  (`@if`/`@else` and `@for`/`track`) in a online shopping cart demo.

During the migration exercise, I revised the inline templates to replace structure directives with the new control flow, deleted trackby function in Component classes and removed redundant import of NgFor and NgIf.  

In the following sections, I am going to show the before code blocks that are written in structure directives and the after code blocks that apply the new control flow 

###Use case of the demo

In the fake store demo, I have a main page that displays a list of product cards.  When customer clicks the name of the card, the demo navigates the customer to the product details page.  In the product details page, customer can add product to shopping cart and click "View Cart" link to check the cart contents at any time.

In the shopping cart, it has a table that renders cart item rows. The cart item row has buttons to delete the entire row, increment or decrement quantity.  Moreover, there is an input field to input promotional code to obtain a discount for the entire cart. In the summary section of the cart, it displays subtotal, discount (optional) and the total amount of the purchase.

This demo has various product and cart components, and some of them display data in the template conditionally or repeatedly.  These components are good choices to apply the new control flow instead of the structure directives. 

###Product use cases

####Before and after of ProductDetailsComponent

Angular V16 (Structural Directives version)

```typescript
// product-details.component.ts

import { NgIf, TitleCasePipe } from '@angular/common';

@Component({
  selector: 'app-product-details',
  standalone: true,
  imports: [TitleCasePipe, FormsModule, NgIf],
  template: `
    <div>
      <ng-container *ngIf="product else noProduct">
        ... product html codes...
      </ng-container>
    </div>
    <ng-template #noProduct>
      <p>Product is invalid</p>
    </ng-template>
  `, 
})
export class ProductDetailsComponent {  
  @Input()
  product: Product | undefined = undefined; 
}
```

In V16, the imports array includes `NgIf` and the inline template has a `ng-container` with `ngIf` and `ngElse` directives.  The `ngElse` is followed with a `noProduct` `NgTemplate`:

```html
<ng-template #noProduct>
      <p>Product is invalid</p>
</ng-template>
```

Angular V17 (New control flow version)

```typescript
// product-details.component.ts

import { TitleCasePipe } from '@angular/common';

@Component({
  selector: 'app-product-details',
  standalone: true,
  imports: [TitleCasePipe, FormsModule],
  template: `
    <div>
      @if (product) {
            ... ... product html codes...
      } @else {
        <p>Product is invalid</p>
      }
    </div>
  `,
})
export class ProductDetailsComponent {
  @Input()
  product: Product | undefined = undefined; 
}
```

In V17, there are three changes in this component. First, the imports array does not include `NgIf`. Second, I got rid of `ng-container` and added `@if` block to display the product HTML code. Third, there is no `NgTemplate` and the `@else` block displays the paragraph element that was in the `NgTemplate`.

####Before and after of ProductListComponent

Angular V16 (Structural Directives version)

```typescript
// product-list.component.ts

import { NgFor } from '@angular/common';

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [NgFor, ProductComponent],
  template: `
    <div>
      <app-product *ngFor="let product of products(); trackBy: trackByFunc" [product]="product" />
    </div>
  `,
})
export class ProductListComponent {
  products = toSignal(inject(ProductService).products$, {
    initialValue: [] as Product[]
  });

  trackByFunc(index: number, product: Product) {
    return product.id;
  }
}
```

In V16, the imports array includes `NgFor` and the inline template has a `ngFor` directive that iterates products signal to pass product to the input of `ProductComponent`. Moreover, `trackBy` invokes `trackByFunc` function to track the list items by product id.

```typescript
trackByFunc(index: number, product: Product) {
    return product.id;
}
```

`trackByFunc` is optional in `ngFor` but it can make the list with good performance when it has many items.  

Angular V17 (New control flow version)

```typescript
// product-list.component.ts

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [ProductComponent],
  template: `
    <div>
      @for (product of products(); track product.id) {
        <app-product [product]="product" />
      }
    </div>
  `,
})
export class ProductListComponent {
  products = toSignal(inject(ProductService).products$, {
    initialValue: [] as Product[]
  });
}
```

In V17, there are three changes in this component. First, the imports array does not include `NgFor`. Second, `trackBy` function is replaced by track that accepts an expression.   The expression can be `$index` system, an Object or an unique id. Third, I got rid of `trackByFunc` from the component class because `@for` block uses `@track` to track the list items by product id. 

###Cart Use Cases

####Before and after of CartComponent

Angular V16 (Structural Directives version)

```typescript
// cart.component.ts

import { NgFor, NgIf } from '@angular/common';

@Component({
  selector: 'app-cart',
  standalone: true,
  imports: [NgFor, CartItemComponent, CartTotalComponent, NgIf, FormsModule],
  template: `
    <div class="cart">
      <ng-container *ngIf="cart().length > 0 else emptyCart">
         ... omit HTML codes brevity ...

        <app-cart-item *ngFor="let item of cart(); trackBy: trackByFunc" [item]="item" />

        ... omit HTML codes for brevity ...
      </ng-container>
    </div>
    <ng-template #emptyCart>
      <p>Your cart is empty, please buy something.</p>
    </ng-template>
  `,
})
export class CartComponent {
  cartService = inject(CartService);
  cart = this.cartService.cart;
  promoCode = this.cartService.promoCode;

  trackByFunc(index: number, item: CartItem) {
    return item.id;
  }
}
```

In V16, the imports array includes `NgIf` and `NgFor`, and the inline template has a `ng-container` with `ngIf` and `ngElse` directives.  The `ngElse` is followed with an  `emptyCart` `NgTemplate`:

```html
<ng-template #emptyCart>
      <p>Your cart is empty, please buy something.</p>
</ng-template>
```

```html
<app-cart-item *ngFor="let item of cart(); trackBy: trackByFunc" [item]="item" />
```

`ngFor` directive iterates the cart signal to pass cart item to the input of `CartItemComponent`. Moreover, `trackBy` invokes `trackByFunc` function to track the list items by product id.

Angular V17 (New control flow version)

```typescript
// cart.component.ts

@Component({
  selector: 'app-cart',
  standalone: true,
  imports: [CartItemComponent, CartTotalComponent, FormsModule],
  template: `
    @if (cart().length > 0) {
      <div class="cart">
        ... omit HTML codes for brevity ...

        @for (item of cart(); track item.id) {
          <app-cart-item [item]="item" />
        }

         ... omit HTML codes for brevity ...
      </div>
    } @else {
      <p>Your cart is empty, please buy something.</p>
    }
  `,
})
export class CartComponent {
  cartService = inject(CartService);
  cart = this.cartService.cart;
  promoCode = this.cartService.promoCode;
}
```

In V17, there are four changes in this component. First, the imports array does not include `NgIf` and `NgFor`. Second, I got rid of `ng-container` and added `@if` block to display the cart item HTML code. Third, there is no `NgTemplate` and the `@else` block displays the paragraph element with the text "Your cart is empty, please buy something.".  Last, `@for` block iterates the cart signal to render `CartItemComponent` and the mandatory `@track` block tracks list items by product id.

####Before and after of CartTotalComponent

Angular V16 (Structural Directives version)

```typescript
// cart-total.component.ts

import { NgIf, PercentPipe } from '@angular/common';

@Component({
  selector: 'app-cart-total',
  standalone: true,
  imports: [PercentPipe, NgIf],
  template: `
    <div class="summary">
      ... omit HTML codes for brevity...

      <div class="row" *ngIf="discountPercent() > 0">
        <div class="col">Minus {{ discountPercent() | percent:'2.2-2' }}</div> 
        <div class="col">Discount: {{ summary().discount }}</div>
      </div>

       ... omit HTML codes for brevity ....
    </div>
  `,
})
export class CartTotalComponent {
  cartService = inject(CartService);
  summary = this.cartService.summary;
  discountPercent = this.cartService.discountPercent;
}
```

In V16, the imports array includes `NgIf`, and the inline template has a `ngIf` directive that displays the percentage of discount and the amount of discount conditionally.

Angular V17 (New control flow version)

```typescript
// cart-total.component.ts

import { PercentPipe } from '@angular/common';

@Component({
  selector: 'app-cart-total',
  standalone: true,
  imports: [PercentPipe],
  template: `
    <div class="summary">
      ... omit HTML codes for brevity ...

      @if (discountPercent() > 0) {
        <div class="row">
          <div class="col">Minus {{ discountPercent() | percent:'2.2-2' }}</div> 
          <div class="col">Discount: {{ summary().discount }}</div>
        </div>
      }

      ... omit HTML codes for brevity....
    </div>
  `,
})
export class CartTotalComponent {
  cartService = inject(CartService);
  summary = this.cartService.summary;
  discountPercent = this.cartService.discountPercent;
}
```

In V17, there are four changes in this component. First, the imports array does not include `NgIf`. Second, I added `@if` block to display the HTML code of discount percent and total discount conditionally. 

###Navigation Bar Use Case

####Before and after of NavBarComponent

Angular V16 (Structural Directives version)

```typescript
// nav-bar.component.ts

import { AsyncPipe, NgIf } from '@angular/common';
import { isCurrentUrlIncludedFn } from './utilities/is-current-url-included';

@Component({
  selector: 'app-nav-bar',
  standalone: true,
  imports: [RouterLink, NgIf, AsyncPipe],
  template: `
    <div>
      <a *ngIf="(isShowHomeButton$ | async) else spacer" routerLink="/">Home</a>
      <a [routerLink]="['my-cart']">View Cart</a>
    </div>
    <ng-template #spacer>
      <span>&nbsp;</span>
    </ng-template>
  `,
})
export class NavBarComponent {
  cdr = inject(ChangeDetectorRef);
  isShowHomeButton$ = isCurrentUrlIncludedFn('/', '/products')
    .pipe(tap(() => this.cdr.markForCheck()));
}
```

In V16, the imports array includes `NgIf`, and the inline template has a anchor element with `ngIf` and `ngElse` directives.  The `ngElse` is followed with a `spacer` `NgTemplate`:

```html
<ng-template #spacer>
      <span>&nbsp;</span>
</ng-template>>
```

Angular V17 (New control flow version)

```typescript
// nav-bar.component.ts

import { AsyncPipe } from '@angular/common';
import { isCurrentUrlIncludedFn } from './utilities/is-current-url-included';

@Component({
  selector: 'app-nav-bar',
  standalone: true,
  imports: [RouterLink, AsyncPipe],
  template: `
    <div>
      @if (isShowHomeButton$ | async) {
        <a routerLink="/">Home</a>
      } @else {
        <span>&nbsp;</span>
      }
      <a [routerLink]="['my-cart']">View Cart</a>
    </div>
  `,
})
export class NavBarComponent {
  cdr = inject(ChangeDetectorRef);
  isShowHomeButton$ = isCurrentUrlIncludedFn('/', '/products')
    .pipe(tap(() => this.cdr.markForCheck()));
}
```

In V17, there are three changes in this component. First, the imports array does not include `NgIf`. Second, I added `@if` block to display an anchor element that routes user to home. Third, there is no `NgTemplate` and the `@else` block displays the &amp;nbsp character.

At this point, the new control flow replaces all occurrences of structure directives in the demo.

The following Stackblitz repo shows the final results:

{%embed https://stackblitz.com/edit/stackblitz-starters-tzashk?file=src%2Fmain.ts %}

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

## Resources:

- Github Repo: https://github.com/railsstudent/ng-online-store-demo-v17
- Stackblitz Angular 16 structure directives: https://stackblitz.com/edit/stackblitz-starters-mjxlec?file=src%2Fmain.ts
- Stackblitz Angular 17 New control flow: https://stackblitz.com/edit/stackblitz-starters-tzashk?file=src%2Fmain.ts
- Angular documentation:  https://angular.dev/guide/templates/control-flow#if-block-conditionals