skip to content

In Angular's @for block, which implicit variables does every row get, and how do let aliases and @empty work?

level: juniorimportance: should knowfreq 55%

answer

  1. dollar-prefixed row variables
  2. position, size and parity
  3. let renames them
  4. needed in nested loops
  5. fallback block after the loop

basics

~20 s

Every @for row gets $index, $count, $first, $last, $even and $odd without declaring them; a let clause gives them other names, which matters in nested loops; an optional @empty block renders when the collection has no items.

solid answer

~40 s

Inside `@for` each row can read six implicit variables: `$index` (position), `$count` (collection size), and the booleans `$first`, `$last`, `$even` and `$odd`. They are always available under those names; `let i = $index, isLast = $last` adds aliases. Aliases matter in nested loops, because the inner loop's `$index` shadows the outer one, so you alias the outer index to reach it from inside. Only those six names can be aliased, and an alias cannot reuse the item's name. An `@empty` block placed directly after the loop's closing brace renders instead of the rows when the collection is empty, and also when it is `null` or `undefined`. These replace `*ngFor`'s `index`, `count`, `first`, `last`, `even` and `odd` exports.

code

html · 7 lines
html
@for (step of steps(); track step.id; let i = $index) {
  <li [class.done]="step.done" [class.striped]="$odd">
    {{ i + 1 }} of {{ $count }}: {{ step.title }}
  </li>
} @empty {
  <li>No steps defined.</li>
}

go deeper

for a junior

Recall the six names, that they need no declaration, and that @empty sits right after the loop and shows when there are no rows.

for a middle

Explain shadowing in nested loops and why an alias fixes it, and that $index is re-assigned after moves so derived flags stay correct.

for a senior

When migrating *ngFor templates, map index-style exports to the dollar names and replace separate empty-state checks with @empty in the same change.

for a principal

Treat these as template conveniences; if templates grow heavy index arithmetic, move that logic into computed view models the team can test.

## The six contextual variables Angular's `@for` block (built into the template syntax since v17) exposes per-row information as **implicit contextual variables**. You do not declare them; they are in scope inside the block body: | Variable | Type | Meaning | |---|---|---| | `$index` | number | position of the current row, starting at 0 | | `$count` | number | number of items in the collection being iterated | | `$first` | boolean | true for the row at index 0 | | `$last` | boolean | true for the row at index `$count - 1` | | `$even` | boolean | true when `$index` is even | | `$odd` | boolean | true when `$index` is odd | They are kept correct as the list changes. When rows are inserted, removed or moved in the middle of the list, Angular re-assigns each row's `$index` after reconciling, so `$first`, `$last` and the parity flags follow. Typical uses: - numbering rows: `{{ $index + 1 }}.`; - separators: `@if (!$last) { <hr /> }`; - zebra striping: `[class.striped]="$odd"`; - "3 of 10" labels with `$count`. ## Aliasing with let A `let` segment in the loop header creates extra names for the same variables: ```angular-html @for (team of teams(); track team.id; let t = $index) { <h3>{{ t + 1 }}. {{ team.name }}</h3> @for (player of team.players; track player.id; let p = $index, last = $last) { <span>{{ t + 1 }}.{{ p + 1 }} {{ player.name }}</span> @if (!last) { , } } } ``` The rules the template compiler enforces: 1. Only the six names above can be aliased; anything else is an `Unknown "let" parameter variable` error. 2. An alias cannot have the same name as the loop item. 3. The same contextual variable cannot be aliased twice. The unaliased names stay available as well, so aliasing is optional in a flat loop. It becomes necessary in **nested loops**: inside the inner `@for`, `$index` means the inner row's index, so the outer index is reachable only through an alias declared on the outer loop, as `t` is above. ## The @empty block `@empty` is an optional block that must follow the `@for` block immediately: ```angular-html <ul> @for (entry of entries(); track entry.id) { <li>{{ entry.label }}</li> } @empty { <li class="muted">Nothing here yet.</li> } </ul> ``` Angular renders the `@empty` content whenever the loop has no rows: an empty array, an empty iterable, or a collection expression that evaluates to `null` or `undefined`. When items arrive, the `@empty` view is removed and rows are created; when the list empties again, it comes back. `@empty` replaces the old pattern of an `*ngIf="items.length === 0"` element next to an `*ngFor`, and it avoids evaluating the collection twice. ## Mapping from *ngFor `NgForOf` (the `*ngFor` directive, deprecated since v20) exported the same information without the dollar sign and required you to alias it before use: | `*ngFor` | `@for` | |---|---| | `let i = index` or `index as i` | `$index` directly, or `let i = $index` | | `count`, `first`, `last`, `even`, `odd` | `$count`, `$first`, `$last`, `$even`, `$odd` | | separate `*ngIf` for the empty state | `@empty` block | ## What the variables are not - They are **not available in the `track` expression**, except `$index`. `track $first` or a track expression reading `$count` is a template type-check error. - They are **per-loop**: an inner loop cannot read an outer loop's unaliased `$index`, because the inner one shadows it. - `@for` has no `break` or `continue`; to skip rows, filter the collection in a `computed()` rather than hiding rows with `@if` inside the loop.

  • In nested Angular @for loops, how do you read the outer row's index from inside the inner loop?
    Declare an alias on the outer loop, such as `let outer = $index`, and read `outer` inside the inner block. The inner loop's own `$index` shadows the outer one, so the unaliased name always refers to the innermost loop.
  • Does Angular's @empty block render when the collection expression is null?
    Yes. The runtime treats `null` and `undefined` as a collection with no rows, so the `@empty` content renders, exactly as it does for an empty array. That makes a separate null check around the loop unnecessary.

saying these in an interview costs you the question

  • You must alias $index with let before you can use it
  • $first and $last are not updated when rows move
  • @empty only renders for an empty array, not for null
  • An inner @for can read the outer loop's $index unaliased
  • Any template variable can be aliased in the @for let clause