Almost every app shows lists: tasks, products, messages, search results. The @for block repeats a piece of template for each item in a collection.
What you will learn
- How to loop over an array with
@for - Why
trackis required and what to put in it - How to show a message for an empty list
- How to use
$index,$first,$lastand$count
The basic loop
<ul>
@for (task of tasks(); track task.id) {
<li>{{ task.title }}</li>
}
</ul>
Read it as "for each task in tasks(), draw this <li>". The part after track is explained next.
Why track?
When the list changes, Angular wants to reuse the existing elements instead of destroying and rebuilding all of them. track tells it how to recognise each item. The best choice is a value that is unique and stays the same, such as an id. If your items are plain strings or numbers, you can track the value itself with track $index or track item.
Choosing a stable key keeps scroll positions, typed text in inputs and animations in the right place when items are added, removed or reordered.
Empty lists
Add an @empty block right after the loop. It is shown when there are no items:
@for (task of tasks(); track task.id) {
<li>{{ task.title }}</li>
} @empty {
<li>Nothing to do. Add your first task!</li>
}
Built-in variables
Inside the loop you can use these automatically provided values:
$index: the position of the item, starting at 0$count: the total number of items$firstand$last: true for the first and last item$evenand$odd: true for alternating items
@for (task of tasks(); track task.id; let i = $index) {
<li>{{ i + 1 }}. {{ task.title }}</li>
}
The let i = $index part gives the built-in value a shorter name.
A task list you can use
This component lets you add and remove tasks. It also introduces a template reference variable, #titleInput, which gives the template direct access to an element:
// src/app/task-list/task-list.ts
import { Component, signal } from '@angular/core';
import { Task } from '../task.model';
@Component({
selector: 'app-task-list',
template: `
<h2>My tasks ({{ tasks().length }})</h2>
<input #titleInput placeholder="New task" />
<button type="button" (click)="add(titleInput.value); titleInput.value = ''">Add</button>
<ul>
@for (task of tasks(); track task.id; let i = $index) {
<li>
{{ i + 1 }}. {{ task.title }}
<button type="button" (click)="remove(task.id)" [attr.aria-label]="'Remove ' + task.title">Remove</button>
</li>
} @empty {
<li>Nothing to do. Add your first task!</li>
}
</ul>
`,
})
export class TaskList {
private nextId = 1;
protected readonly tasks = signal<Task[]>([]);
protected add(title: string): void {
const clean = title.trim();
if (!clean) {
return;
}
this.tasks.update((list) => [...list, { id: this.nextId++, title: clean, done: false }]);
}
protected remove(id: number): void {
this.tasks.update((list) => list.filter((task) => task.id !== id));
}
}
// src/app/app.ts
import { Component } from '@angular/core';
import { TaskList } from './task-list/task-list';
@Component({
imports: [TaskList],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
<h1>Lists</h1>
<app-task-list />
</main>
Add a few tasks and remove one from the middle. Each removal keeps the other rows stable because every task has its own id to track.
The add method ignores empty text. Checking input before you use it is a good habit, and you will do it more thoroughly with forms later.
Looping over other collections
@for works with arrays and other iterable values. To loop over the properties of an object, convert it first, for example with Object.entries(obj) in your class.
Common mistakes
- Leaving out `track`: the compiler reports an error. Always provide it.
- Tracking `$index` for lists that change order: Angular then cannot tell that an item moved. Prefer a unique id.
- Changing the array in place: use
updatewith a new array so the signal notices the change.
Try it yourself
- Add a "Clear all" button that sets the list to an empty array.
- Show "(last)" next to the final task using
$last. - Give every task a
doneflag, and add a button that toggles it by creating a new array withmap.
Quick check
- What does
trackdo, and what is a good value for it? - Which block displays a message for an empty list?
- How do you get the position of an item inside the loop?
Next, continue with Angular ngIf, ngFor, ngSwitch and trackBy: Legacy Syntax and Migration.