Preview let syntax in HTML template in Angular 18

Connie Leung - Jul 9 - - Dev Community

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.

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'
  }
];
Enter fullscreen mode Exit fullscreen mode
// 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()
  ],
}
Enter fullscreen mode Exit fullscreen mode
// main.ts

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

bootstrapApplication(App, appConfig);
Enter fullscreen mode Exit fullscreen mode

Bootstrap the component and the application configuration to start the Angular application. The application configuration provides a HttpClient feature to make requests to the 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); 
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 simple component 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$;  
}
Enter fullscreen mode Exit fullscreen mode

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) ?? [];
Enter fullscreen mode Exit fullscreen mode

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

@let rate = product.rating.rate;
@let isPopular = rate >= 4;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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, which 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$; 
}
Enter fullscreen mode Exit fullscreen mode

Let me list the differences

@if (products$ | async; as products) {
   @if (products) {
        ....
   }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

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

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

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:

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .