Angular / Basic / Lesson 4
Control flow and list identity
Render loading, empty, and populated states deliberately, and give repeated rows a stable identity that matches the data rather than their current position.
On this page
Lesson goals
- Use built-in if, for, and empty template blocks.
- Choose stable unique track keys for changing collections.
- Test reordered data and preserved user interaction.
Give every view state an explicit meaning
A collection view needs a policy for loading, failure, empty success, and populated success. An empty array alone does not distinguish all four states. Model those alternatives so the template can explain what the user should do next. Angular built-in control flow uses @if, @else, and @for blocks; these blocks do not require importing the older structural directives merely to use their syntax. They still need valid component expressions and imported child dependencies. Keep conditions readable and avoid making a false value such as a legitimate zero accidentally look like missing data.
Keep the identifier when content changes
The complete model changes one row title while preserving its identifier and order. An identifier represents a logical entity, whereas the row object reference and display position can change during refresh. The model prints the resulting data only; it does not measure DOM reuse. A real list test should additionally inspect focus, input state, and rendered rows after reordering. Do not assign a new random identifier every time a response is mapped, because that tells the renderer every row is a different entity. Validate duplicate identifiers at the data boundary rather than asking a tracking expression to repair them.
export {};
type Row = { id: number; title: string };
const rows: readonly Row[] = [{ id: 1, title: "Basics" }, { id: 2, title: "Forms" }];
const changed = rows.map(row => row.id === 2 ? { ...row, title: "Typed forms" } : row);
console.log(JSON.stringify(changed));
[{"id":1,"title":"Basics"},{"id":2,"title":"Typed forms"}]
The row with id two gets a new title while both logical identifiers remain unchanged. DOM reconciliation is not executed.
Track entities and describe empty success
The contextual template assumes loading and items are signals and every item has a unique stable id and title. track item.id connects rows to their logical entities when order or object references change. @empty provides a fallback when the iteration has no items, while the outer @if separately handles loading. Use $index tracking only when position is genuinely the intended stable identity, such as a static collection that never changes. An editable or reorderable list normally needs entity keys. The framework can then preserve the appropriate DOM association instead of confusing one rowβs state with another.
@if (loading()) {
<p role="status">Loading courses</p>
} @else {
<ul>
@for (item of items(); track item.id) {
<li>{{ item.title }}</li>
} @empty {
<li>No courses match</li>
}
</ul>
}
A loaded empty list would show No courses match; a populated list would show its titles. The actual host state and DOM behavior are not executed by this fragment.
Use block scope without hiding the data contract
Control-flow blocks introduce local values such as the current item and contextual loop variables. Alias a long expression when it improves readability, but keep derived filtering or sorting in a tested computation when it carries business meaning. @for does not support JavaScript break or continue; produce the intended collection before rendering. A track expression is about identity, not permission to display a record. A client-side hidden row can still exist in memory, so the server must restrict returned data according to authorization. Empty and error messages should have distinct wording and an accessible place in the view.
Test reorder, replacement, and interaction state
Use a fixture with two editable rows, focus one input, and reorder the data without changing entity identifiers. A component or browser test should verify the logical row still owns its intended value and focus policy. Add duplicate-key and empty-result cases. A pure array test cannot establish those DOM guarantees, and a fast benchmark does not prove correctness. Keep loading and error transitions deterministic in tests so a missing request does not look like a valid empty response. Stable identity improves both reasoning and potential rendering efficiency, but it must reflect the real domain and be verified at the user-facing boundary.
Common errors and fixes
Row input state follows the wrong item after reordering.
- Why it happens
- Position was used as identity for changing entities.
- How to fix it
- Track a stable unique entity identifier.
- Prevent it next time
- Test editable rows during reorder, not only text output.
A failed request displays an empty-success message.
- Why it happens
- One empty array represented several different states.
- How to fix it
- Model loading, error, and successful data separately.
- Prevent it next time
- Test every state transition explicitly.
Every refresh recreates all logical row identities.
- Why it happens
- Keys were generated anew instead of preserved from the domain.
- How to fix it
- Keep stable keys across refreshed representations.
- Prevent it next time
- Validate uniqueness and identity before rendering.
Check your understanding
1. Reorder the model rows to place id two first. Which values should change and which identity property must remain stable?
Show worked solution
The display order changes to two then one, but each row keeps its original id and associated title. A track expression based on id describes that relationship; an index key describes positions instead. The array model can assert the id sequence, while a component test must check that editable state and focus are still associated with the intended row.
2. The server returns two records with id one. Explain why changing track to the title is not a general repair.
Show worked solution
Titles can also repeat or change, so they do not establish a reliable entity identity. Diagnose the data or mapping contract and reject, reconcile, or assign genuinely stable identifiers according to the domain. Add a duplicate-identifier test and a visible error policy if the collection cannot be represented safely. A tracking trick should not hide malformed data.
Key takeaways
- Model view states explicitly.
- Track logical identity, not accidental position.
- Verify user state across list changes.