Angular Lists
Angular gives you two ways to render collections in a template: the classic *ngFor structural directive and the newer built-in @for control flow block, and knowing when and how to track items keeps your lists fast.
Rendering Collections in Angular
Almost every real application displays a list of something — products, comments, search results. Angular supports two syntaxes for repeating a template over an array: the structural directive *ngFor, and the newer @for block that ships as part of Angular's built-in control flow. Both walk an iterable and stamp out one copy of a template per item, but they differ in syntax, performance defaults, and how they handle empty state.
The Legacy *ngFor Directive
*ngFor is applied as an attribute on an element and desugars to an <ng-template> behind the scenes. You bind it to an expression of the form 'let item of items', and Angular exposes contextual variables like index, first, last, even, and odd that you can alias with the 'as' keyword. Performance-sensitive lists should also supply a trackBy function so Angular can identify which DOM nodes correspond to which data items across re-renders.
Using *ngFor with an index and trackBy
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { bootstrapApplication } from '@angular/platform-browser';
interface Task {
id: number;
title: string;
done: boolean;
}
@Component({
selector: 'app-task-list',
standalone: true,
imports: [CommonModule],
template: `
<ul>
<li *ngFor="let task of tasks; index as i; trackBy: trackByTaskId">
{{ i + 1 }}. {{ task.title }} - {{ task.done ? 'Done' : 'Pending' }}
</li>
</ul>
`
})
export class TaskListComponent {
tasks: Task[] = [
{ id: 1, title: 'Learn Angular', done: false },
{ id: 2, title: 'Build a component', done: false },
{ id: 3, title: 'Write tests', done: true }
];
trackByTaskId(index: number, task: Task): number {
return task.id;
}
}
bootstrapApplication(TaskListComponent).catch((err) => console.error(err));
The Modern @for Control Flow Block
Since Angular 17, templates support a built-in control flow syntax that doesn't require importing CommonModule at all. The @for block reads more like plain TypeScript, requires an explicit track expression on every loop (there is no untracked fallback), and pairs naturally with an @empty block that renders when the collection has zero items — something *ngFor cannot do on its own.
Using @for with track and @empty
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
interface Task {
id: number;
title: string;
done: boolean;
}
@Component({
selector: 'app-task-list',
standalone: true,
template: `
<ul>
@for (task of tasks; track task.id; let i = $index, count = $count) {
<li>{{ i + 1 }} of {{ count }}: {{ task.title }}</li>
} @empty {
<li>No tasks yet. Add one to get started.</li>
}
</ul>
`
})
export class TaskListComponent {
tasks: Task[] = [];
}
bootstrapApplication(TaskListComponent).catch((err) => console.error(err));
- $index — zero-based position of the current item
- $count — total number of items in the collection
- $first / $last — true for the first or last item
- $even / $odd — true when $index is even or odd
- track — the required expression Angular uses to identify each item across re-renders
A filterable product list using signals and @for
import { Component, signal } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-product-list',
standalone: true,
template: `
<input
type="text"
placeholder="Filter products..."
(input)="filterTerm.set($any($event.target).value)"
/>
@for (product of filteredProducts(); track product.id) {
<div class="product-row">
{{ product.name }} - Price: {{ product.price }}
</div>
} @empty {
<p>No products match "{{ filterTerm() }}".</p>
}
`
})
export class ProductListComponent {
private allProducts: Product[] = [
{ id: 1, name: 'Keyboard', price: 49 },
{ id: 2, name: 'Mouse', price: 25 },
{ id: 3, name: 'Monitor', price: 199 }
];
filterTerm = signal('');
filteredProducts() {
const term = this.filterTerm().toLowerCase();
return this.allProducts.filter(p => p.name.toLowerCase().includes(term));
}
}
bootstrapApplication(ProductListComponent).catch((err) => console.error(err));
Exercise: Angular Lists
Which syntax repeats a template element for each array item?