*ngIf: From Micro-syntax to ng-template

A familiar scenario will serve as our starting point. Most projects eventually need to display data arriving from an API while showing a loading state in the interim.

A common pattern is to let Angular's AsyncPipe handle the subscription logic directly in the template rather than managing it manually in the component class.

The template snippet typically looks like this:
*ngIf
Let's dissect this statement piece by piece and observe how Angular interprets each segment.

Using the same color-coded breakdown as before:
*ngIf color coded
The left side features the asterisk * followed by the ngIf selector.

On the right side, we can identify three distinct components:

  1. an :expression
  2. an :as declaration
  3. a :keyExp declaration

Each of these deserves a closer look:
items
When the right side begins with an expression, Angular will bind that expression to an @Input() bearing the same name as the selector, which in this case is ngIf. The micro-syntax translation performed by Angular results in:

<ng-template [ngIf]="(items$ | async)">...
Enter fullscreen mode Exit fullscreen mode

Let's move forward.
as items
The next segment exports our expression into a local variable for use throughout the template. Although not explicitly documented, this is perfectly valid Angular. The underlying mechanism mirrors how a :keyExp declaration operates. To ensure items holds the correct value, we need to add an ngIf key to the context object, assigning it the value from our @Input() ngIf. This requirement becomes obvious when the micro-syntax is converted to the ng-template equivalent:

<ng-template
[ngIf]="(items$ | async)"
let-items="ngIf"
>...
Enter fullscreen mode Exit fullscreen mode

For an :as declaration, Angular adopts the name of the @Input() as the key in the context that maps to our local variable. Since our @Input() is ngIf, that exact name must exist as a key in the context. Furthermore, before the template renders, the value of @Input ngIf needs to be assigned to the corresponding ngIf key in the context.

else
The final component is a straightforward :keyExp declaration, which assigns the loading TemplateRef to an @Input() on the NgIf directive. Using the camelCase-fusing rule, we can deduce that this input must be named ngIfElse. When we translate this section of micro-syntax to its ng-template form, the output is:

<ng-template
...
[ngIfElse]="loading"
>...
Enter fullscreen mode Exit fullscreen mode

Assembling all the pieces gives us the complete picture:
micro-syntax to ng-template

From this analysis, we can deduce several details about the NgIf directive's implementation. Let's formulate hypotheses about its source code and then verify them.

Based on our observations, the NgIf directive should include:

  1. an @Input() named ngIf
  2. a context object that exposes an ngIf key
  3. an additional @Input() called ngIfElse

Let's examine the official NgIf implementation to confirm our predictions:

  /**
   * The Boolean expression to evaluate as the condition for showing a template.
   */
  @Input()
  set ngIf(condition: T) {
    this._context.$implicit = this._context.ngIf = condition;
    this._updateView();
  }

  private _updateView() {
    if (this._context.$implicit) {
          ...
          this._thenViewRef = this._viewContainer.createEmbeddedView(this._thenTemplateRef , this._context);
    } else {
          ...
          this._elseViewRef = this._viewContainer.createEmbeddedView(this._elseTemplateRef, this._context);
    }
  }
Enter fullscreen mode Exit fullscreen mode

The presence of @Input() ngIf is confirmed. When this condition is set, the NgIf context assigns it to both the implicit and ngIf keys. The view is then refreshed. If the provided condition evaluates to truthy, our embed view is shown; otherwise, the _elseTemplateRef takes its place.

  /**
   * A template to show if the condition expression evaluates to false.
   */
  @Input()
  set ngIfElse(templateRef: TemplateRef<NgIfContext<T>>|null) {
    assertTemplate('ngIfElse', templateRef);
    this._elseTemplateRef = templateRef;
    this._elseViewRef = null;  // clear previous view if any.
    this._updateView();
  }
Enter fullscreen mode Exit fullscreen mode

This specific template originates from our @Input ngIfElse. The setter validates that the provided template reference is legitimate and then stores it in _elseTemplateRef.

/**
 * @publicApi
 */
export class NgIfContext<T = unknown> {
  public $implicit: T = null!;
  public ngIf: T = null!;
}
Enter fullscreen mode Exit fullscreen mode

Finally, the output confirms our assumption: the context indeed contains an ngIf key alongside $implicit. Both are populated with the truthy expression passed to @Input() ngIf. This is precisely why we can destructure our async items$ using a :let declaration with the micro-syntax:

*ngIf="(items$ | async); let items; else: template"
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can achieve the same result by using a let-declaration on the ng-template in conjunction with the $implicit key:

<ng-template
  [ngIf]="(items$ | async)"
  let-items>
  {{items | json}}
</ng-template>
Enter fullscreen mode Exit fullscreen mode

Both approaches will yield identical outcomes!

For hands-on examples of all these concepts, refer to this Stackblitz.

*ngFor - From micro-syntax to ng-template, revisited

Let's shift our attention to the NgFor directive. We'll assume a familiar setup similar to the one we used for NgIf.

  • We need to render a list of asynchronous data retrieved from an API in our template.
  • Once the data arrives, each element of the list should be displayed using a custom template.

Our implementation looks like this:
*ngFor
Once again, we color-code, break apart, and analyze:
*ngFor color coded
The asterisk * along with the ngFor selector form the left portion.
The right portion consists of three distinct segments:

  1. a :let declaration
  2. a :keyExp declaration
  3. an :as declaration

Let's examine each segment more closely:
let item

of items
Although the colon is often omitted to keep the NgFor micro-syntax cleaner, this is genuinely a standard :keyExp. We bind our (items$ | async) expression to the directive's camelCase-combined @Input() ngForOf. The same value gets assigned to the context's ngForOf key, which enables us to reference it through an :as expression. From there, we can use the local items variable directly within the template.
When converted to the ng-template form, this segment becomes:

<ng-template
  [ngForOf]="(items$ | async)"
  let-items="ngForOf"
>...
Enter fullscreen mode Exit fullscreen mode

as index
Finally, we have a straightforward :as declaration that extracts the index key from our context into a variable named i:

<ng-template
  let-i="index"
>...
Enter fullscreen mode Exit fullscreen mode

Assembling all the pieces gives us the complete picture:
micro-syntax to ng-template

With what we've gathered, we can form predictions about the underlying source code and verify them.

We know the NgFor directive possesses:

  1. an @Input() named ngForOf that accepts an iterable, rendering one template per item
  2. a context object with an $implicit key exposing the current item being rendered
  3. a context object with an ngForOf key exposing the iterable itself
  4. a context object with an index key exposing the current position being rendered

Let's verify these assumptions against the official NgFor source code:

  @Input()
  set ngForOf(ngForOf: U&NgIterable<T>|undefined|null) {
    this._ngForOf = ngForOf;
    this._ngForOfDirty = true;
  }
Enter fullscreen mode Exit fullscreen mode

In the implementation, we encounter the @Input() ngForOf accepting our iterable. There's also a flag indicating that the variable has been modified. This is intentional: NgFor doesn't simply render and re-render every element each time. Instead, it performs sophisticated diffing to determine whether a template actually needs updating, ensuring solid performance even with substantial lists and complex templates.

For a deeper dive into NgFor mechanics, the source code is the best resource. For now, let's inspect the context object.

/**
 * @publicApi
 */
export class NgForOfContext<T, U extends NgIterable<T> = NgIterable<T>> {
  constructor(public $implicit: T, public ngForOf: U, public index: number, public count: number) {}

  get first(): boolean {
    return this.index === 0;
  }

  get last(): boolean {
    return this.index === this.count - 1;
  }

  get even(): boolean {
    return this.index % 2 === 0;
  }

  get odd(): boolean {
    return !this.even;
  }
}
Enter fullscreen mode Exit fullscreen mode

As anticipated, we find the $implicit, ngForOf, and index keys. You'll notice several other keys available for use in your templates as well!
If you'd like to see NgFor and its context in action, check out this StackBlitz!

Where do we go from here?

I can't express how impressed I am that you've made it this far! Hopefully, you now have a solid grasp of how Angular performs its magic with structural directive micro-syntax. Without question, this is one of the more challenging areas of the framework. By deconstructing things into digestible pieces, my goal was to equip you with the understanding and tools to not only grasp the built-in structural directives better but also to craft your own custom ones.

Let's take a breather (or two, or three) to absorb all this new knowledge and appreciate that the asterisk has made its comeback.

In the next installment (possibly the final one?) of this series, we'll enhance our custom "*exchangeRate" directive so it works seamlessly with the micro-syntax. Moreover, rather than re-rendering the template after each API call, we'll expose the response as an observable to our template.

Do you have lingering questions or topics you'd like covered? Would a video walkthrough of structural directives be helpful? I'd love to hear your thoughts. If there's anything you'd like to see in future posts, drop a comment or reach out directly.

And if you found this article valuable, feel free to like and share it. For more of my content, follow me on Twitter or GitHub.