skip to content

In an Angular <ng-template>, what do let-item and let-i="index" declare, and how do they differ from #ref and @let?

level: middleimportance: nice to knowfreq 35%

answer

  1. only on ng-template
  2. values come from the context
  3. no value means $implicit
  4. fed by whoever renders it
  5. read-only in handlers

basics

~10 s

On an Angular <ng-template>, let-item binds the context's $implicit value and let-i="index" its index property: read-only inputs supplied by the renderer, unlike #ref (a template target) or @let (a named expression).

solid answer

~40 s

`let-` attributes declare **template input variables**, and only on `<ng-template>`; elsewhere the compiler reports that `let-` is only supported on ng-template elements. `let-item` with no value maps to the context object's `$implicit` property; `let-i="index"` maps to its `index` property. The values come from the **context** that the code rendering the template passes in, for example a structural directive, `NgTemplateOutlet` or `createEmbeddedView`, so the same template can show different data each time it is rendered. That differs from `#ref`, which points at something declared in the template (an element, component, directive or `TemplateRef`), and from `@let`, which names an expression the template itself evaluates. `let-` variables, like `@let`, are read-only in handlers.

code

ts · 29 lines
ts
import {Component, input, TemplateRef} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';

interface Product { id: string; name: string; }

@Component({
  selector: 'app-product-list',
  imports: [NgTemplateOutlet],
  template: `
    <ul>
      @for (p of products(); track p.id; let i = $index) {
        <ng-container
          [ngTemplateOutlet]="rowTemplate()"
          [ngTemplateOutletContext]="{$implicit: p, position: i + 1}"
        />
      }
    </ul>
  `,
})
export class ProductList {
  readonly products = input.required<Product[]>();
  readonly rowTemplate = input.required<TemplateRef<unknown>>();
}

// Consumer template:
// <ng-template #rowTpl let-product let-pos="position">
//   <li>{{ pos }}. {{ product.name }}</li>
// </ng-template>
// <app-product-list [products]="items()" [rowTemplate]="rowTpl" />

go deeper

for a junior

Recall that let- goes on ng-template, that a bare let-x means $implicit, and that the renderer supplies the values.

for a middle

Contrast the three variable kinds by source of value and place of declaration, and map let-x="key" to a context property.

for a senior

Use let- inputs to build reusable row or cell templates handed to components, and keep context keys stable and typed.

for a principal

Decide when a template-injection API is worth its complexity compared with content projection or a dedicated child component.

## Three kinds of template variables Angular templates have three ways to introduce a name, and interviewers like to see them told apart: | Syntax | Name for | Where the value comes from | Where it can be declared | |---|---|---|---| | `#row` | an element, component, directive (via `exportAs`) or `TemplateRef` | the thing it is declared on | any element | | `@let total = expr;` | the result of an expression | the template evaluates it | anywhere in the template | | `let-item`, `let-i="index"` | a property of the rendering **context** | the code that renders the template | only on `<ng-template>` | ## How let- variables work An `<ng-template>` is a fragment that is not rendered where it is written. Something else renders it, once or many times, and each time it may pass a **context object**. `let-` attributes declare which context properties the fragment wants, and under which local names: - `let-item` (no value) binds the context's **`$implicit`** property; - `let-i="index"` binds the context's **`index`** property to the local name `i`; - any number can be declared: `let-row let-i="index" let-last="last"`. ```html <ng-template #rowTpl let-product let-pos="position"> <li>{{ pos }}. {{ product.name }}</li> </ng-template> ``` If this template is rendered with a context `{ $implicit: someProduct, position: 3 }`, then `product` is `someProduct` and `pos` is `3`. Rendered with another context, the same fragment shows other data. The names are local to the fragment's view and to views nested inside it. Compiler rules: 1. `let-` is only allowed on `<ng-template>`; on any other element it is a template error. 2. The local name may not contain `-`. 3. The variable is **read-only**: assigning to it in an event handler is reported with `Template variables are read-only`. ## Where the context comes from This leaf only needs the shape; the rendering APIs belong elsewhere. In short: - a **structural directive** creates the context and the asterisk syntax desugars into an `<ng-template>` with `let-` attributes; this is how `*ngFor="let item of items; let i = index"` works; - **`NgTemplateOutlet`** takes the context through its context input; - **`ViewContainerRef.createEmbeddedView(templateRef, context)`** passes it programmatically. A fragment still sees the component's members and the variables of the view where it was **declared**, in addition to its `let-` inputs. ## Contrasting with #ref and @let - A **`#ref` on the `<ng-template>` itself**, as in `#rowTpl`, is how other code gets the `TemplateRef` to render; it says nothing about the data inside. - A **`@let`** inside the fragment can derive values from the `let-` inputs: `@let label = pos + '. ' + product.name;`. - `let-` variables are the only ones whose values are **supplied from outside** the template text, which is what makes a fragment reusable. ## Typing and modern alternatives With strict template checks, `let-` variables are typed from the context type the renderer declares, or `any` when it cannot be inferred; typing a custom directive's context is a structural-directive topic. In current Angular, the built-in `@for` and `@if` blocks declare their variables in the block header instead, so `let-` attributes appear mainly with reusable fragments passed to components, such as a row template given to a table component. ## Common mistakes - Putting `let-x` on a `<div>`, expecting it to work like a local variable. - Expecting `let-x="item"` to read a component field named `item`; it reads the **context** property `item`. - Forgetting that `let-x` with no value is `$implicit`, then passing the data under another key.

  • In an Angular <ng-template>, what does let-x="item" read when the component also has an item property?
    It reads the `item` property of the context object passed by whoever renders the template, not the component's field. If the context has no `item` key, `x` is `undefined`. To read the component field inside the fragment, use `item` directly, since the fragment still sees the declaring component's members.
  • Can an Angular let- template variable be changed from an event handler inside the fragment?
    No. Template input variables are read-only, and the template checker reports an assignment to one. To change the underlying data, call a component method or update the source that supplies the context, and the next render of the fragment receives the new value.

saying these in an interview costs you the question

  • let- attributes work on any element as local variables
  • let-item with no value reads a context key named item
  • let-x="name" reads the component's name property
  • A #ref on ng-template gives the fragment's context data
  • let- variables can be reassigned inside the fragment