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?
answer
- keys become inputs, lets become variables
- the selector is the prefix
- capitalise the key's first letter
- bare let reads $implicit
basics
~20 sA 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 sThe 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
Recall that of items binds to ngForOf and that let item reads the default $implicit value of the template context.
Explain the four expansion rules, including the reversed reading of as and the capitalised selector prefix on every key.
Use the rules to design a custom directive's inputs and context, and to diagnose binding errors that name the expanded property.
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.