Post

Modern Angular 10: Rendering Lists with Signals and @for

Modern Angular 10: Rendering Lists with Signals and @for

This is lesson 10 of the Modern Angular Course. In the previous lesson, we built the ProductsGrid and ProductCard components with static placeholder content. Now it is time to make the UI reactive by connecting it to signals and rendering the product list dynamically.

This post is part of the Modern Angular Course series. Check the course page for the full episode list.

In this post, we cover:

  • Defining a Product interface for type safety
  • Storing product data in a signal
  • Rendering lists using Angular’s @for syntax with track
  • Handling empty lists with @empty
  • Using loop context variables like $index, $first, $last, $even, $odd, and $count

Creating the Product Interface

Before we create our signal, let’s define a TypeScript interface for our products. This gives us type safety and better autocomplete in the IDE.

You can generate the file with the Angular CLI:

1
ng generate interface products/product

Or the shorthand:

1
ng g i products/product

Then define the interface in product.ts:

1
2
3
4
5
6
7
export interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  originalPrice?: number;
}

Notice the optional property originalPrice with the ? syntax. This field only applies to products that are on sale.

This interface will be used throughout the application whenever we work with product data.

Creating the Products Signal

Now let’s create a signal to hold our products data. Open products-grid.ts and update it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { Component, signal } from '@angular/core';
import { ProductCard } from '../product-card/product-card';
import { Product } from '../product';
import { MatIcon } from "@angular/material/icon";

@Component({
  selector: 'app-products-grid',
  imports: [ProductCard, MatIcon],
  templateUrl: './products-grid.html',
  styleUrl: './products-grid.scss',
})
export class ProductsGrid {
  protected readonly products = signal<Product[]>([
    {
      id: 1,
      name: 'Premium Wireless Headphones',
      description:
        'High-quality wireless headphones with noise cancellation and premium sound quality.',
      price: 199.99,
      originalPrice: 249.99,
    },
    {
      id: 2,
      name: 'Smart Fitness Watch',
      description:
        'Track your fitness goals with this advanced smartwatch featuring heart rate monitoring.',
      price: 299.99,
    },
    {
      id: 3,
      name: 'Portable Bluetooth Speaker',
      description:
        'Compact speaker with powerful bass and 12-hour battery life.',
      price: 79.99,
    },
  ]);
}

Notice the syntax signal<Product[]>. We are passing a type inside angle brackets (sometimes called generic syntax or diamond operator). In this case, Product[] means “an array of Product objects.”

We are explicitly typing this signal as a signal that holds a list of products, not just a single product. This gives us stronger type safety and better autocomplete while we build.

The signal function creates a reactive container for our data. Whenever the value changes, Angular knows to update the UI.

Reading the Signal in the Template

Remember how we read a signal in the template from previous lessons? We call it as a function using parentheses.

1
2
3
<div class="products-container">
  <p>Total products: {{ products().length }}</p>
</div>

Those parentheses after products are what read the signal’s current value. Every time the signal updates, this expression re-evaluates automatically.

Rendering Lists with @for

This is where Angular’s modern control flow really shines. We use the @for block to loop through the array from our signal and render one block per product.

Here is a simplified example to illustrate the syntax:

1
2
3
4
5
6
7
8
9
10
11
<div class="products-container">
  <div class="products-grid">
    @for (product of products(); track product.id) {
      <div class="product-item">
        <h3>{{ product.name }}</h3>
        <p>{{ product.description }}</p>
        <p class="price">{{ product.price | currency }}</p>
      </div>
    }
  </div>
</div>

Let’s break this down:

  1. @for (...) starts the loop block. Inside this block, we place the HTML that Angular repeats for each iteration.
  2. product of products() means: read the signal value, then iterate over each item. The product variable is a local loop variable — you can name it anything meaningful, like item or currentProduct.
  3. track product.id tells Angular to use each product’s unique id as the key, so it can correctly match existing DOM elements to data items when the list changes.
  4. { ... } contains the template that gets repeated for every product.

In Angular’s @for syntax, the track expression is required. Angular needs a stable way to identify each item, and this also improves rendering performance.

Why track Matters

Without track, Angular has less information to match old items with new ones, so it may recreate more DOM than necessary when the list changes.

With track product.id, Angular can keep the existing DOM nodes for items that did not change and update only the ones that actually changed. That means fewer unnecessary re-renders and a smoother UI, especially as your lists grow.

Compared to the Old *ngFor

For reference, this is how we used to write list rendering with *ngFor:

1
2
3
4
5
6
7
<div class="products-grid">
  <div class="product-item" *ngFor="let product of products(); trackBy: trackByProductId">
    <h3>{{ product.name }}</h3>
    <p>{{ product.description }}</p>
    <p class="price">{{ product.price | currency }}</p>
  </div>
</div>

*ngFor is deprecated and the Angular team indicates intent to remove it in v22, so use @for for new code.

Using @for with Product Cards

In our actual project, instead of rendering product details inline, we render the ProductCard component inside the loop:

1
2
3
4
5
6
7
<div class="products-container">
  <div class="products-grid">
    @for (product of products(); track product.id) {
      <app-product-card />
    }
  </div>
</div>

Right now, the card still shows placeholder content. In the next lesson, we will pass each product to ProductCard using the input() function.

Handling Empty Lists with @empty

What if we have no products? The @for block supports an @empty section for exactly this case:

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="products-container">
  <div class="products-grid">
    @for (product of products(); track product.id) {
      <app-product-card />
    } @empty {
      <div class="empty-state">
        <mat-icon>inventory_2</mat-icon>
        <h3>No products available</h3>
        <p>Check back later for new arrivals!</p>
      </div>
    }
  </div>
</div>

The @empty block only renders when the array is empty. This is cleaner than wrapping everything in a separate @if check.

Empty State Styles

The styles center the empty state message across the full grid width:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.empty-state {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 4rem 2rem;
  text-align: center;

  mat-icon {
    font-size: 4rem;
    width: 4rem;
    height: 4rem;
    color: var(--mat-sys-outline);
    margin-bottom: 1rem;
  }

  h3 {
    margin: 0 0 0.5rem;
    color: var(--mat-sys-on-surface);
  }

  p {
    margin: 0;
    color: var(--mat-sys-on-surface-variant);
  }
}

grid-column: 1 / -1 makes the empty state span the full grid width. The --mat-sys-* values are Angular Material theme tokens (CSS variables), so the colors come from the active Material theme instead of hardcoded hex values.

Loop Context Variables

The @for block provides useful context variables you can use inside the loop:

  • $index — the current index (0-based)
  • $first — true if this is the first item
  • $last — true if this is the last item
  • $even / $odd — true for even/odd indices
  • $count — total number of items

Here is an example that demonstrates all of them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<ul>
  @for (label of ['Product 1', 'Product 2', 'Product 3', 'Product 4'];
    track $index;
    let i = $index;
    let total = $count;
    let isFirst = $first;
    let isLast = $last;
    let isEven = $even;
    let isOdd = $odd) {
    <li
      class="demo-item"
      [class.first-item]="isFirst"
      [class.last-item]="isLast"
      [class.even-row]="isEven"
      [class.odd-row]="isOdd">
      {{ i + 1 }} of {{ total }} - {{ label }}
    </li>
  }
</ul>

In this example:

  • isFirst and isLast let you highlight list boundaries
  • isEven and isOdd let you style alternating rows
  • total lets you show progress like “3 of 12”

And the matching styles:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.demo-item.even-row {
  background: color-mix(in srgb, var(--mat-sys-primary) 6%, transparent);
}

.demo-item.odd-row {
  background: color-mix(in srgb, var(--mat-sys-outline) 6%, transparent);
}

.demo-item.first-item {
  outline: 2px solid var(--mat-sys-primary);
}

.demo-item.last-item {
  outline: 2px dashed var(--mat-sys-outline);
}

Updated Grid Styles

The full products-grid.scss now includes styles for the container, grid, product items, empty state, and the context variables demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.products-container {
  display: grid;
  gap: 1rem;
}

.products-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 16px;
}

.product-item {
  padding: 1rem;

  h3 {
    margin: 0 0 0.5rem;
    color: var(--mat-sys-on-surface);
  }

  p {
    margin: 0 0 0.5rem;
    color: var(--mat-sys-on-surface-variant);
  }

  .price {
    font-weight: 600;
    color: var(--mat-sys-primary);
  }
}

The --mat-sys-on-surface, --mat-sys-on-surface-variant, and --mat-sys-primary are Angular Material theme tokens. Using var(...) means the colors come from the active Material theme instead of hardcoded hex values.

Products grid rendering product cards with signals and @for

Source Code

The full source code for the course is available on GitHub: loiane/modern-angular.

Next Step

Now that we have a typed product interface, a signal holding our data, and the @for loop rendering cards, the next step is to make the product cards dynamic. In the next lesson, we pass product data to the ProductCard component using Angular’s input() function.

Watch the Video

This post is part of the Modern Angular Course series. Check the course page for the full episode list.

This post is licensed under CC BY 4.0 by the author.
This site uses cookies. Please choose whether to accept analytics cookies. Privacy Policy