Angular Lists
Lists display collections of items in the template.
List Rendering Essentials
- Loop: Use
@for
withtrack
for stable identity and@empty
for empty states. - Signals: Store list state in a signal (e.g.,
items = signal([...])
) and update immutably withset()
/update()
. - Identity: Track by a stable key (e.g.,
it.id
) to avoid unnecessary DOM work. - Derived views: Filter/sort copies of your data for the UI; keep the source list intact (use
computed()
for derived state).
Note: See Control Flow for @for
, Conditional Rendering, and Templates for interpolation and basics.
Basic Lists
- Use
@for
to loop; expose the index withlet i = $index
. - Update immutably with signals (e.g.,
items.update(arr => [...arr, newItem])
).
<ul>
@for (item of items(); let i = $index; track item) {
<li>{{ i + 1 }}. {{ item }}</li>
} @empty {
<li>No items</li>
}
</ul>
Example
Render a basic list with @for
and expose the index:
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<h3>Lists</h3>
<ul>
@for (item of items(); let i = $index; track item) {
<li>{{ i + 1 }}. {{ item }}</li>
} @empty {
<li>No items</li>
}
</ul>
<button (click)="add()">Add Item</button>
<button (click)="clear()">Clear</button>
<button (click)="reset()">Reset</button>
`
})
export class App {
items = signal(['Angular', 'React', 'Vue']);
add() { this.items.update(arr => [...arr, 'Svelte']); }
clear() { this.items.set([]); }
reset() { this.items.set(['Angular', 'React', 'Vue']); }
}
bootstrapApplication(App);
<app-root></app-root>
Example explained
@for (item of items(); let i = $index; track item)
: Loops over theitems
signal; exposes the zero-based index asi
; uses the primitiveitem
itself as identity.- Buttons:
add()
appends immutably,clear()
sets an empty list,reset()
restores defaults. @empty
: Renders the fallback list item when there are no items.
Notes:
- No import needed:
@for
is built into Angular's template syntax; no module import required. - Don't mutate in place: With signals, prefer
set()
/update()
to assign a new array and trigger updates.
Lists with track (@for)
- On list changes, Angular reconciles DOM rows with data items.
track
provides a stable identity (e.g., anid
) to minimize DOM churn and preserve focus/inputs.- Legacy equivalence: With
*ngFor
, usetrackBy
to achieve the same effect.
@for (it of items(); track it.id) { <li>{{ it.name }}</li> } @empty { <li>No items</li> }
Example
Render lists with a stable identity using track
:
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, signal } from '@angular/core';
type Item = { id: number; name: string };
@Component({
selector: 'app-root',
standalone: true,
template: `
<h3>Lists with track</h3>
<ul>
@for (it of items(); let i = $index; track it.id) {
<li>{{ i + 1 }}. {{ it.name }} (id: {{ it.id }})</li>
}
</ul>
<button (click)="renameFirst()">Rename first</button>
<button (click)="shuffle()">Shuffle</button>
<button (click)="add()">Add item</button>
`
})
export class App {
items = signal- ([
{ id: 1, name: 'Angular' },
{ id: 2, name: 'React' },
{ id: 3, name: 'Vue' }
]);
nextId = 4;
renameFirst() {
this.items.update(arr => arr.map((it, i) => i === 0 ? { ...it, name: it.name + ' *' } : it));
}
shuffle() {
this.items.update(arr => {
const copy = [...arr];
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
});
}
add() {
this.items.update(arr => [...arr, { id: this.nextId++, name: 'New ' + Date.now() }]);
}
}
bootstrapApplication(App);
<app-root></app-root>
Example explained
track it.id
: Provides a stable identity so Angular reuses DOM rows when items shuffle or update, preserving focus and local state.renameFirst()
: Updates the first item's name immutably (new object reference) to trigger change detection.shuffle()
: Randomizes order to demonstrate DOM reuse withtrack
.add()
: Appends a new item with a uniqueid
.
Notes:
- Avoid index identity: Don't use the array index as identity if order can change; use a stable
id
. - Ensure uniqueness: Duplicate
id
values cause DOM/UI desync. Use unique keys. - *ngFor equivalence: With
*ngFor
, usetrackBy
to achieve the same behavior.
Filter & Sort
- Compute a derived
view
withcomputed()
based on signals. - Filter and sort copies of your data; keep the source list intact for easy resets.
import { signal, computed } from '@angular/core';
items = signal([{ name: 'Angular', price: 0 }, { name: 'React', price: 0 }]);
query = signal('');
sortKey = signal<'name' | 'price'>('name');
sortDir = signal<1 | -1>(1);
view = computed(() => {
const q = query().toLowerCase();
const dir = sortDir();
const key = sortKey();
return items()
.filter(it => it.name.toLowerCase().includes(q))
.sort((a, b) => {
const av: any = (a as any)[key];
const bv: any = (b as any)[key];
return av < bv ? -1 * dir : av > bv ? 1 * dir : 0;
});
});
@for (p of view(); track p.name) {
<tr>
<td>{{ p.name }}</td>
<td>{{ p.price | currency:'USD' }}</td>
</tr>
}
Example
Filter and sort lists using computed()
:
Example
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, signal, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
type Product = { name: string; price: number };
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
template: `
<h3>Filter & Sort</h3>
<div style="display:flex;gap:8px;margin-bottom:8px;">
<label>Search: <input #q (input)="query.set(q.value)" placeholder="Type to filter..." /></label>
<button (click)="setSort('name')">Sort by Name</button>
<button (click)="setSort('price')">Sort by Price</button>
<button (click)="toggleDir()">{{ sortDir() === 1 ? 'Asc' : 'Desc' }}</button>
</div>
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr><th style="border:1px solid #ddd;padding:8px;background:#f7f7f7;">Name</th><th style="border:1px solid #ddd;padding:8px;background:#f7f7f7;width:140px;">Price</th></tr>
</thead>
<tbody>
@for (p of view(); track p.name) {
<tr>
<td style="border:1px solid #ddd;padding:8px;">{{ p.name }}</td>
<td style="border:1px solid #ddd;padding:8px;">{{ p.price | currency:'USD' }}</td>
</tr>
}
</tbody>
</table>
`
})
export class App {
items = signal<Product[]>([
{ name: 'Angular', price: 0 },
{ name: 'React', price: 0 },
{ name: 'Vue', price: 0 },
{ name: 'Svelte', price: 0 },
{ name: 'Solid', price: 0 },
{ name: 'Lit', price: 0 }
]);
query = signal('');
sortKey = signal<'name' | 'price'>('name');
sortDir = signal<1 | -1>(1); // 1 asc, -1 desc
view = computed(() => {
const q = this.query().toLowerCase();
const dir = this.sortDir();
const key = this.sortKey();
return this.items()
.filter(it => it.name.toLowerCase().includes(q))
.sort((a, b) => {
const av: any = (a as any)[key];
const bv: any = (b as any)[key];
return av < bv ? -1 * dir : av > bv ? 1 * dir : 0;
});
});
setSort(key: 'name' | 'price') {
if (this.sortKey() === key) {
this.toggleDir();
} else {
this.sortKey.set(key);
}
}
toggleDir() {
this.sortDir.set(this.sortDir() === 1 ? -1 : 1);
}
}
bootstrapApplication(App);
<app-root></app-root>
Example explained
computed()
view: Derives a filtered/sorted array from signals without mutating the source list.query/sortKey/sortDir
: Control the derived view by updating these signals from the UI.@for (p of view(); track p.name)
: Renders the derived rows; uses a stable key (p.name
) for identity.
Note: Avoid heavy work in templates; pre-compute with computed()
and loop with @for
to keep templates fast.