Preview let syntax in HTML template in Angular 18

Reading Time: 5 minutes

Loading

Introduction

In this blog post, I want to describe the let syntax variable that Angular 18.1.0 will release. This feature has debates in the Angular community because some people like it, others have concerns, and most people don’t know when to use it in an Angular application.

I don’t know either, but I use the syntax when it makes the template clean and readable.

let's go

Bootstrap Application

// app.routes.ts

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'let-syntax',
    loadComponent: () => import('./app-let-syntax.component'),
    title: 'Let Syntax',
  },
  {
    path: 'before-let-syntax',
    loadComponent: () => import('./app-no-let-syntax.component'),
    title: 'Before Let Syntax',
  },
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'let-syntax'
  },
  {
    path: '**',
    redirectTo: 'let-syntax'
  }
];
// app.config.ts

import { provideHttpClient } from "@angular/common/http";
import { provideRouter } from "@angular/router";
import { routes } from "./app.routes";
import { provideExperimentalZonelessChangeDetection } from "@angular/core";

export const appConfig = {
  providers: [
    provideHttpClient(),
    provideRouter(routes),
    provideExperimentalZonelessChangeDetection()
  ],
}
// main.ts

import { appConfig } from './app.config';

bootstrapApplication(App, appConfig);

Bootstrap the component and the application configuration to start the Angular application. The application configuration provides HttpClient feature to make requests to server to retrieve a collection of products, a Router feature to lazy load standalone components, and experimental zoneless.

The first route, let-syntax, lazy loads AppLetSyntaxComponent that uses the let syntax. The second route, before-let-syntax, lazy loads AppNoLetSyntaxComponent that does not use the let syntax. Then, I can compare the differences in the templates and explain why the let syntax is good in some cases.

Create a Product Service

I created a service that requests the backend to retrieve a collection of products. Then the standalone components can reuse this service.

// app.service.ts

import { HttpClient } from "@angular/common/http";
import { Injectable, inject } from "@angular/core";

const URL = 'https://fakestoreapi.com/products';

type Product = {
  id: number;
  title: string;
  description: string;
  category: string;
  image: string;
  rating: {
    rate: number;
  }
}

@Injectable({
  providedIn: 'root',
})
export class AppService {
  http = inject(HttpClient);
  products$ = this.http.get<Product[]>(URL); 
}

Create the main component

The main component is a simple component that routes to either AppLetSyntaxComponent or AppNoLetSyntaxComponent to display a list of products.

// main.ts

import { ChangeDetectionStrategy, Component, VERSION } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { appConfig } from './app.config';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <header>Angular {{version}}</header>
    <h1>Hello &#64;let demo</h1>

    <ul>
      <li>
        <a routerLink="let-syntax" routerLinkActive="active-link">Let syntax component</a>
      </li>
      <li>
        <a routerLink="before-let-syntax" routerLinkActive="active-link">No let syntax component</a>
      </li>
    </ul>

    <router-outlet />
   `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  version = VERSION.full;
}

The unordered list displays two hyperlinks for users to click. When a user clicks the first link, Angular loads and renders AppLetSyntaxComponent. When a user clicks the second link, AppNoLetSyntaxComponent is rendered instead.

Create a component that uses the syntax

This is a simple component that uses the let syntax in the template to make it clean and readable.

// app-let-syntax.component.ts

import { AsyncPipe } from "@angular/common";
import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
import { AppService } from "./app.service";

@Component({
  selector: 'app-let-syntax',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <div>
      @let products = (products$ | async) ?? [];
      @for (product of products; track product.id; let odd = $odd) {
        @let rate = product.rating.rate;
        @let isPopular = rate >= 4;
        @let borderStyle = product.category === "jewelery" ? "2px solid red": "";
        @let bgColor = odd ? "#f5f5f5" : "goldenrod";
        <div style="padding: 0.75rem;" [style.backgroundColor]="bgColor">
          <div [style.border]="borderStyle" class="image-container">
            <img [src]="product.image" />
          </div>
          @if (isPopular) {
           <p class="popular">*** Popular ***</p> 
          }
          <p>Id: {{ product.id }}</p>
          <p>Title: {{ product.title }}</p>
          <p>Description: {{ product.description }}</p>
          <p>Rate: {{ rate }}</p>
        </div>
        <hr>
      }
    </div>
   `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class AppLetSyntaxComponent {
  service = inject(AppService);
  products$ = this.service.products$;  
}

The component injects AppService, makes a GET request to retrieve products, and assigns to products$ Observable.

The demo applies the let syntax to the HTML template.

@let products = (products$ | async) ?? [];

The AsyncPipe resolves the products$ Observable or default to an empty array

@let rate = product.rating.rate;
@let isPopular = rate >= 4;

I assigned the product rate to the rate variable and derived the isPopular value. It is true when the rate is at least 4, otherwise, it is false.

@if (isPopular) {
      <p class="popular">*** Popular ***</p> 
}

<p>Rate: {{ rate }}</p>

isPopular is checked to display ***Popular*** paragraph element and rate is displayed along with other product information.

@let borderStyle = product.category === "jewelery" ? "2px solid red": "";
@let bgColor = odd ? "#f5f5f5" : "goldenrod";
 <div style="padding: 0.75rem;" [style.backgroundColor]="bgColor">
      <div [style.border]="borderStyle" class="image-container">
            <img [src]="product.image" />
      </div>
     ....
</div>

When the product’s category is jewelry, the border style is red and 2 pixels wide, and it is assigned to the borderStyle variable. When array elements have odd indexes, the background color is “#f5f5f5”. When the index is even, the background color is goldenrod, and it is assigned to the bgColor variable.

The variables are assigned to style attributes, backgroundColor and border, respectively.

Let’s repeat the same exercise without the let syntax.

Create the same component before the let syntax exists

// ./app-no-let-syntax.componen.ts

import { AsyncPipe } from "@angular/common";
import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
import { AppService } from "./app.service";

@Component({
  selector: 'app-before-let-syntax',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <div>
      @if (products$ | async; as products) {
        @if (products) {
          @for (product of products; track product.id; let odd = $odd) {
            <div style="padding: 0.75rem;" [style.backgroundColor]="odd ? '#f5f5f5' : 'yellow'">
              <div [style.border]="product.category === 'jewelery' ? '2px solid green': ''" class="image-container">
                <img [src]="product.image" />
              </div>
              @if (product.rating.rate > 4) {
                <p class="popular">*** Popular ***</p> 
              }
              <p>Id: {{ product.id }}</p>
              <p>Title: {{ product.title }}</p>
              <p>Description: {{ product.description }}</p>
              <p>Rate: {{ product.rating.rate }}</p>
            </div>
            <hr>
          }
        }
      }
    </div>
   `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class AppNoLetSyntaxComponent {
  service = inject(AppService);
  products$ = this.service.products$; 
}

Let me list the differences

@if (products$ | async; as products) {
        @if (products) {
                 ....
        }
}

I used nested ifs to resolve the Observable and test the products array before iterating it to display the data.

@if (product.rating.rate > 4) {
        <p class="popular">*** Popular ***</p> 
 }
<p>Rate: {{ product.rating.rate }}</p>

product.rating.rate occurs twice in the HTML template.

[style.backgroundColor]="odd ? '#f5f5f5' : 'yellow'"

[style.border]="product.category === 'jewelery' ? '2px solid green': ''" 

The style attributes are inline and not easy to read inside the tags.

I advise keeping the let syntax to a minimum in an HTML template. Some good use cases are:

  • style attributes
  • class enablement. Enable or disable a class by a boolean value
  • resolve Observable by AsyncPipe
  • extract duplicated

The following Stackblitz repo displays the final results:

This is the end of the blog post that introduce the preview feature, let syntax in Angular 18. I hope you like the content and continue to follow my learning experience in Angular, NestJS, GenerativeAI, and other technologies.

Resources:

  1. Stackblitz Demo: https://stackblitz.com/edit/angular-let-demo-uxsvu6?file=src%2Fmain.ts