skip to content

In Angular, how do you pass values into a template rendered by NgTemplateOutlet, and what does the $implicit context key do?

level: middleimportance: must knowfreq 58%

answer

  1. a plain object travels with the stamp
  2. let- attributes map keys
  3. the key used when no name given
  4. ngTemplateOutletContext or context:

basics

~10 s

Bind an object to ngTemplateOutletContext (or write context: in the * form); the template reads its keys with let-name="key". $implicit is the default key: a let- declaration with no value receives it.

solid answer

~30 s

`NgTemplateOutlet` takes a context object through `[ngTemplateOutletContext]`, or `context:` in the `*ngTemplateOutlet` shorthand. Inside the `<ng-template>`, `let-` attributes map local names to keys: `let-size="size"` reads `size`. A `let-` attribute with **no value**, like `let-user`, reads the special `$implicit` key, so `{ $implicit: currentUser }` binds `user`. It is simply the default key, the same convention `createEmbeddedView()` and structural directives use. Matching is by key name, never by position, and a missing key just gives `undefined`. When the `TemplateRef` has a typed context, the object you pass is type-checked against it.

code

ts · 19 lines
ts
import { Component, signal } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

@Component({
  selector: 'app-greeting',
  imports: [NgTemplateOutlet],
  template: `
    <ng-template #hello let-name let-tone="tone">
      <p [class]="tone">Hello, {{ name }}</p>
    </ng-template>

    <ng-container
      [ngTemplateOutlet]="hello"
      [ngTemplateOutletContext]="{ $implicit: user(), tone: 'warm' }" />
  `,
})
export class Greeting {
  user = signal('Ada');
}

go deeper

for a junior

Recall the two inputs, the template and its context object, and that a bare let- variable receives $implicit.

for a middle

Explain key-based matching, the * microsyntax form with context:, and that createEmbeddedView uses the same context convention.

for a senior

Discuss typing the TemplateRef context so a wrong key fails at compile time, and keeping context objects minimal so templates stay decoupled.

for a principal

Treat the context shape as a public API of any component that accepts templates, and version it with the same care as inputs.

## The problem a context solves An `<ng-template>` is a fragment rendered on demand. When you render it with **`NgTemplateOutlet`**, you usually want to feed it values that differ per render: the current row, a label, an index. The fragment can already read properties of the component that declares it, but it cannot see values that exist only at the place where it is stamped. The **context object** is how those values travel. ## Two halves: declare parameters, pass an object A fragment declares its parameters with **`let-` attributes**, and the outlet supplies a plain object through **`ngTemplateOutletContext`**: ```html <ng-template #badge let-user let-size="size"> <span [class]="size">{{ user.name }}</span> </ng-template> <ng-container [ngTemplateOutlet]="badge" [ngTemplateOutletContext]="{ $implicit: currentUser, size: 'large' }" /> ``` Each `let-` attribute maps a local name to a key of the context object: - **`let-size="size"`** reads the `size` key, so the local variable `size` becomes `'large'`; - **`let-user`** has **no value**, so it reads the special **`$implicit`** key and becomes `currentUser`. That is all `$implicit` is: the **default key**. A `let-` declaration without an assigned name binds to it. It saves the consumer from knowing the key name for the one "main" value, which is why built-in directives such as `NgForOf` expose their primary value under `$implicit`. ## The microsyntax form `NgTemplateOutlet` can also be written with the `*` shorthand, where the outlet's extra inputs become keys: ```html <ng-container *ngTemplateOutlet="badge; context: { $implicit: currentUser, size: 'large' }" /> ``` This is the same directive with the same two inputs; the `context:` key maps to `ngTemplateOutletContext`. Both forms are current. `NgTemplateOutlet` is a standalone directive exported from `@angular/common`, so a standalone component must list it in `imports`. ## Rules worth stating in an interview 1. **Keys, not positions.** Parameters are matched by key name. The order of `let-` attributes means nothing. 2. **One implicit value.** A context object has only one `$implicit` key, but any number of `let-` declarations *without* a value will all read it, which is legal but usually a mistake. 3. **Missing keys are simply undefined.** If the fragment asks for `let-size="size"` and the context has no `size`, the variable is `undefined`; there is no runtime error. 4. **Local names win.** Inside the fragment, a name resolves first to template-local variables such as `let-` declarations, and only then to members of the declaring component. 5. **Context is type-checked against the `TemplateRef`.** Since v16, when the outlet's `TemplateRef<C>` has a concrete context type, the object passed as context must be assignable to `C`; with strict template type checking, extra or wrongly typed keys are compile errors. ## Same idea from code The same context object is the second argument of `ViewContainerRef.createEmbeddedView()`: ```ts this.viewContainer.createEmbeddedView(this.badge, { $implicit: user, size: 'large' }); ``` `NgTemplateOutlet` is essentially a declarative wrapper around this call, which is why the two behave identically with respect to `$implicit`. ## Comparing the ways to feed a fragment | Source of a value | How the fragment reads it | Scope | |---|---|---| | Declaring component's property | By name, directly | Same for every render | | Context key with a name | `let-x="key"` | Per render | | Context `$implicit` | `let-x` with no value | Per render | | Another template variable | `#ref` in the declaring template | Declaring template | ## A worked mental model Think of a stamp as a function call. The `<ng-template>` is the function body, its `let-` attributes are the parameter list, and the context object is the argument object. `$implicit` is the one argument you may pass without a name. Every render is a new call with its own arguments, so two outlets stamping the same template with different contexts produce two views that never share local variables, even though both can still read the same fields of the declaring component. ## Common mistakes - Writing `let-user="$implicit"` everywhere: legal, but longer than `let-user`. - Passing context as an array and expecting positional binding. - Assuming the fragment can see properties of the component that *renders* it; it sees the declaring component and the context, nothing else. - Treating `$implicit` as something `NgTemplateOutlet` invented. It is a convention of Angular embedded-view contexts, used by `createEmbeddedView()` and by every structural directive.

  • What does the fragment see if it declares let-size="size" but the context object has no size key?
    The local variable `size` is simply `undefined`. Angular does not throw at runtime; with a typed `TemplateRef` context, strict template checking can catch the mismatch at compile time instead.
  • How do you pass the same context when rendering from code instead of a template?
    Pass it as the second argument of `ViewContainerRef.createEmbeddedView(templateRef, context)`. The `$implicit` convention is identical, because `NgTemplateOutlet` is a declarative wrapper around that call.
  • Can the template also read properties of the component that renders it through the outlet?
    No. A fragment evaluates against the component that declares it, plus the context. If the rendering component wants to share a value, it must put it in the context object.

saying these in an interview costs you the question

  • Thinks context values bind to let- variables by position
  • Believes $implicit is a special NgTemplateOutlet-only keyword
  • Assumes a missing context key throws a runtime error
  • Thinks the template can read the rendering component's fields directly
  • Writes let-x="$implicit" believing the value is mandatory