Federico Gambarino

Under the hood: Angular ngIf

July 11, 2020
Under the hood, Angular, ngIf

I think that one of the most famous Angular directives is ngIf. So, today, I am going to write about it, explaining how it’s made.

What is the ngIf directive?

ngIf is a structural directive: it means that if applied to an element of our HTML view, it will change the DOM structure. In this specific case it will make the element appear/disappear based on the condition that is provided to the directive input.

There are different ways to use the ngIf directive in a view. I will show here, very briefly, three main ways:

  1. to show/hide an element of the view based on a condition:
<div *ngIf="condition">
  If condition is met, you can see me
</div>
  1. to show alternatively one element or another respectively when the condition is met or not met:
<div *ngIf="condition; else template">
  If condition is met, you can see me
</div>
<ng-template #template>
  <div>
    If condition is not met, you can see me
  </div>
</ng-template>
  1. similarly to the previous point, but using two different ng-template (to replace the element where ngIf is used with the content of the template linked to the verified/unverified condition)
<div style="color: red">
  <div *ngIf="condition; then templateThen; else templateElse">
    this text won't never be rendered
  </div>
</div>
<ng-template #templateThen>
  <div>
    If condition is met, you can see me in red
  </div>
</ng-template>
<ng-template #templateElse>
  <div>
    If condition is not met, you can see me in red
  </div>
</ng-template>

But the asterisk syntax for the structural directives is just a syntactic sugar, in fact we can write (even if it’s not recommended by the Angular documentation) the former examples in this way as well:

<ng-template [ngIf]="condition">
  <div>
    If condition is met, you can see me
  </div>
</ng-template>
<ng-template [ngIf]="condition" [ngIfElse]="template">
  <div>
    If condition is met, you can see me
  </div>
</ng-template>
<ng-template #template>
  <div>
    If condition is not met, you can see me
  </div>
</ng-template>
<div style="color: red">
  <ng-template
    [ngIf]="condition"
    [ngIfThen]="templateThen"
    [ngIfElse]="templateElse"
  >
    <div>
      this text won't never be rendered
    </div>
  </ng-template>
</div>
<ng-template #templateThen>
  <div>
    If condition is met, you can see me in red
  </div>
</ng-template>
<ng-template #templateElse>
  <div>
    If condition is not met, you can see me in red
  </div>
</ng-template>

Basically the asterisk syntax lets you avoid writing explicitly a ng-template, and it lets you use the awesome microsyntax that Angular offers - which is a topic that maybe I will write about in a future post.

The ngIf source code

Even if the asterisk syntax is the cleaner one, I chose to show also the unsugared one since with that it is far easier to understand the ngIf source code (Angular 10).

Let’s start looking at the top of the directive class:

@Directive({selector: '[ngIf]'})
export class NgIf<T = unknown> {
  private _context: NgIfContext<T> = new NgIfContext<T>();
  private _thenTemplateRef: TemplateRef<NgIfContext<T>>|null = null;
  private _elseTemplateRef: TemplateRef<NgIfContext<T>>|null = null;
  private _thenViewRef: EmbeddedViewRef<NgIfContext<T>>|null = null;
  private _elseViewRef: EmbeddedViewRef<NgIfContext<T>>|null = null;

as we can see we have the usual Directive decorator which defines the class as a directive with the well known selector [ngIf].
The ngIf directive class uses generics, with a unknown type if unspecified. What is the generic type variable T in this case? As you will see later, it’s the type of the expression to evaluate.

the private properties

There are 5 private properties:

  1. _context is the ngIf context
  2. _thenTemplateRef is the ng-template reference used when the rendering condition, input of the ngIf directive, is met
  3. _elseTemplateRef is the ng-template reference used when the rendering condition is not met
  4. _thenViewRef is the view reference of the element rendered when the condition is met
  5. and _elseViewRef is the view reference in case of an unverified condition

The _context property is of type NgIfContext wich is:

export class NgIfContext<T = unknown> {
  public $implicit: T = null!
  public ngIf: T = null!
}

What’s the purpose of this NgIfContext class?
It represents the context structure that will be used when rendering the elements. Its properties are initialised with null using the ! operator to avoid compilation errors due to null checks.

the constructor

Let’s move on and see the constructor of the class:

 constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext<T>>) {
    this._thenTemplateRef = templateRef;
  }

first of all there is a ViewContainerRef injected, that is needed to change the DOM when rendering the elements. Secondly there is the TemplateRef that belongs to the ng-template that contains the [ngIf] directive. This TemplateRef is assigned to the _thenTemplateRef private property, for the cases 1 or 2 - where we don’t have a explicit ngIfThen reference.

the input properties

We already know that ngIf has three different input properties:

  1. the ngIf input is the one with the expression to evaluate:
  /**
   * 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();
  }

it is a setter, in this particular case there is no need to work with the Angular lyfecycle hooks, any time the condition changes it is assigned to both \$implicit and ngIf properties of the _context private property. Then the private method _updateView is called.

  1. the ngIfThen is an optional input, and it takes the template reference where [ngIfThen] is added.
  /**
   * A template to show if the condition expression evaluates to true.
   */
  @Input()
  set ngIfThen(templateRef: TemplateRef<NgIfContext<T>>|null) {
    assertTemplate('ngIfThen', templateRef);
    this._thenTemplateRef = templateRef;
    this._thenViewRef = null;  // clear previous view if any.
    this._updateView();
  }

the template reference input is asserted to check it - we will see what assertTemplate does in detail later -, if all is OK it is assigned to the _thenTemplateRef property. Right after that the _thenViewRef value is set to null (it will be checked to clear the previous view). Then the private method _updateView is called.

  1. also the ngIfElse is an optional input, and it takes the template reference where [ngIfElse] is added.
  /**
   * 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();
  }

It performs the equivalent of the previous input, but for the else case.

So what’s exactly the function assertTemplate?

function assertTemplate(
  property: string,
  templateRef: TemplateRef<any> | null
): void {
  const isTemplateRefOrNull = !!(!templateRef || templateRef.createEmbeddedView)
  if (!isTemplateRefOrNull) {
    throw new Error(
      `${property} must be a TemplateRef, but received '${stringify(
        templateRef
      )}'.`
    )
  }
}

it is a function that simply checks if the templateRef is really a templateRef (it has the createEmbeddedView method) or it is null or undefined. If this condition is not verified it throws an error, with a very clear message.

the _updateView method

Finally we reach the core of the directive, where all the magic happens.

private _updateView() {
    if (this._context.$implicit) {
      if (!this._thenViewRef) {
        this._viewContainer.clear();
        this._elseViewRef = null;
        if (this._thenTemplateRef) {
          this._thenViewRef =
              this._viewContainer.createEmbeddedView(this._thenTemplateRef, this._context);
        }
      }
    } else {
      if (!this._elseViewRef) {
        this._viewContainer.clear();
        this._thenViewRef = null;
        if (this._elseTemplateRef) {
          this._elseViewRef =
              this._viewContainer.createEmbeddedView(this._elseTemplateRef, this._context);
        }
      }
    }
  }

This method is called in every single setter of the input properties. So any times the expression evaluation changes, or the then/else template references change there is an update of the view.
How does it work?

  1. It checks if the condition evaluation gives true (its value is assigned in the ngIf input to the \$implicit property of the context)
  2. If so checks that the _thenViewRef is falsy: that is true for the first rendering since it has been initialised with a null value and in any case the ngIfThen setter is executed
  3. If _thenViewRef is null, it means that _thenTemplateRef needs to be rendered.
    Therefore the clear method of the ViewContainerRef is called, to clear the view from the last template rendered.
    Then the _elseViewRef value is set to null (since the then case is rendered, the else reference is cleared).
    Finally the _thenTemplateRef (if not null) is used to render the element in the view, using the ViewContainerRef createEmbeddedView method.
  4. If the condition evaluation gives false an equivalent flow of 1 to 3 steps, but related to the else case references, is performed.

And basically that’s it.

If you look into the source code, in the Angular repository you will find a couple of static properties, used by Ivy compilation and rendering pipeline. But that’s, at least for now, an argument I won’t discuss due to my limited knowledge of the topic 😉.

I hope you enjoyed the post!

...and that's it for today! Thank you for your time 😃

Federico


© 2022, Federico Gambarino - All Rights Reserved

Made with ❤️ with Gatsby