skip to content

In Angular's structural-directive microsyntax, how do `let`, `as` and keys like `of` or `trackBy` map onto the directive's inputs and template variables?

level: middleimportance: should knowfreq 38%

answer

  1. keys become inputs, lets become variables
  2. the selector is the prefix
  3. capitalise the key's first letter
  4. bare let reads $implicit

basics

~20 s

A naked first expression binds to the input named after the selector; other keys get the selector prefix and a capital letter, so of becomes ngForOf; let x reads $implicit, and as or let x = y read named context properties.

solid answer

~40 s

The microsyntax is a small grammar the compiler turns into ordinary bindings on the generated `<ng-template>`. A naked expression right after `=` binds to `[prefix]`, where the prefix is the directive's selector. A key-expression pair such as `of items` or `trackBy: fn` becomes `[prefixKey]`, with the key's first letter upper-cased: `[ngForOf]`, `[ngForTrackBy]`. `let item` declares a template variable bound to the context's `$implicit`; `let i = index` binds `i` to `context.index`. `as` also declares a variable but reads the other way round: `index as i` gives `let-i="index"`, and `items as list` or `cond as c` binds the local to the prefixed input's name. Colons and `;`/`,` separators are optional. So a directive's inputs must be named selector-plus-key for the sugar to reach them.

go deeper

for a junior

Recall that of items binds to ngForOf and that let item reads the default $implicit value of the template context.

for a middle

Explain the four expansion rules, including the reversed reading of as and the capitalised selector prefix on every key.

for a senior

Use the rules to design a custom directive's inputs and context, and to diagnose binding errors that name the expanded property.

for a principal

Weigh the shorthand's readability against its limits, such as one directive per element, when choosing between a structural directive and a component API.

## What microsyntax is When you apply a structural directive with an asterisk, the attribute value is not a normal template expression. It is **microsyntax**: a compact grammar the Angular compiler expands into property bindings and template variables on the `<ng-template>` it generates. Knowing the expansion rules lets you read any structural directive's usage and design your own directive's inputs so the shorthand reaches them. The **prefix** is the attribute name after the asterisk, which is the directive's selector: `ngFor` in `*ngFor`, `appHasRole` in `*appHasRole`. ## The grammar in four rules The Angular documentation gives the grammar as a prefix followed by `let` declarations, `as` declarations and key-expression pairs, optionally separated by `;` or `,`. In practice: 1. **Naked expression first.** An expression directly after the `=` binds to the prefix itself: `*ngIf="cond"` becomes `[ngIf]="cond"`. 2. **Key-expression pairs are prefixed.** `key expression` (the colon is optional, so `key: expression` is the same) becomes `[prefixKey]="expression"`, with the key's first letter capitalised. `of items` becomes `[ngForOf]="items"`; `trackBy: track` becomes `[ngForTrackBy]="track"`; `else loading` on `*ngIf` becomes `[ngIfElse]="loading"`. 3. **`let` declares a template variable read from the context.** `let item` with no assignment becomes `let-item`, which reads the context property `$implicit`. `let i = index` becomes `let-i="index"`, which reads `context.index`. 4. **`as` declares a variable, reading right to left.** `index as i` also becomes `let-i="index"`. After a keyed or naked expression, `as` names the value of that binding: `cond as c` becomes `let-c="ngIf"`, and `of items as list` becomes `let-list="ngForOf"`. The directive must put those properties on its context object for the variables to have values. ## Worked expansion | Shorthand fragment | Expansion on the `<ng-template>` | | --- | --- | | `*ngFor="let item of items"` | `ngFor let-item [ngForOf]="items"` | | `; trackBy: track` | `[ngForTrackBy]="track"` | | `; index as i` | `let-i="index"` | | `; let last = last` | `let-last="last"` | | `*ngIf="user$ \| async as user"` | `[ngIf]="user$ \| async" let-user="ngIf"` | | `*appHasRole="'admin'; else denied"` | `[appHasRole]="'admin'" [appHasRoleElse]="denied"` | Two details stand out. First, when the value starts with `let`, there is no naked expression, so the prefix attribute (`ngFor`) is present but not bound; that plain attribute is what matches the directive's `[ngFor]` part of its selector while the bound `[ngForOf]` input carries the data. Second, pipes are allowed inside an expression, so `async as user` works because the `as` applies to the whole piped expression. ## Consequences for writing a directive - **Name inputs selector-plus-key.** A directive with selector `[appRepeat]` that wants `*appRepeat="let x of list"` needs an input called `appRepeatOf`. An input called `of` would never be bound. - **Name context properties for `let` and `as`.** Put the default value on `$implicit` so `let x` works, and add named properties (`index`, or a property with the directive's own name, as `NgIf` does with `ngIf`) for `let y = index` and `as` aliases. - **The shorthand has limits.** Only one directive per element can use the asterisk, and every key must map to a prefixed input, so pick short, readable keys. - **The long form is always available.** Anything the shorthand expresses, you can write explicitly on an `<ng-template>`, which helps when debugging an unexpected binding. ## Reading an unfamiliar directive When you meet a third-party structural directive, the expansion rules give you a quick way to find out what it accepts: 1. Look at its selector; that is the prefix every key is added to. 2. List its inputs that start with the prefix. Each suffix is a key you can write in the microsyntax: an input `appPagerSize` means `size: 20` is accepted. 3. Look at the context type it declares, often through a static context guard. Every property there can be read with `let x = property` or `property as x`, and `$implicit` is what a bare `let x` receives. 4. If the shorthand gets confusing, write the long form on an `<ng-template>` and compare. It is also how you spot a typo: a key with no matching prefixed input never reaches the directive, so compare every key you wrote against the expanded input names. ## Why interviewers ask this The question separates people who have only used `*ngFor` from those who understand that it is ordinary input binding under the hood. `NgFor` and `NgIf` are deprecated since v20 in favour of `@for` and `@if`, but the same grammar governs every custom structural directive, and it explains why a directive's inputs carry its selector as a prefix, a naming pattern that otherwise looks arbitrary.

  • `NgFor`'s selector is `[ngFor][ngForOf]`; how does `*ngFor="let x of items"` satisfy both parts?
    The value starts with `let`, so there is no naked expression: the expansion adds a plain `ngFor` attribute with no binding. The `of items` key adds a bound `[ngForOf]` property. The generated `<ng-template>` therefore carries both attribute names, and the selector matches.
  • What must a custom directive do so that `*appRepeat="let row of rows; let n = index"` gives `n` a value?
    It needs an `appRepeatOf` input to receive `rows`, and each embedded view it creates must be given a context object with an `index` property (and `$implicit` for `row`). The microsyntax only declares that `n` reads `index`; the directive has to supply it.

saying these in an interview costs you the question

  • The of in ngFor is a JavaScript for...of loop evaluated by the template.
  • A key like trackBy binds to an input named trackBy on the directive.
  • let i = index creates a new variable that Angular computes itself, independent of the directive.
  • The as keyword only works with the async pipe.
  • Microsyntax requires the colon after every key.