Ng directive Thought Experiment

ngLet directive to resolve an async observable binding without having to use ngIf to hide content

Not long after publishing my recent post on minimising Async bindings in Angular by using the *ngIf=”obs$ | async as obs”; construct, I received the following comment on twitter:

This got me thinking… Is there an existing way to do this? Is it possible? and even is it useful? After looking around into the ngIf documentation on Angular.io Structural Drirectives and then into the ngIf directive source/packages/common/src/directives/ng_if.ts, I figured it couldn’t be too hard to create a “Let” directive… As it turns out a Let directive is exactly the same as the ngIf directive without the “else“…

Acknowledgement: None of this is my code, I’ve just borrowed it from the existing ngIf directive

So let’s try

Step 1: Generate the directive

ng g directive ngLet

Step 2: Borrowing from ngIf, create an NgLetContext

export class ngLetContext {
  public $implicit: any = null;
  public ngLet: any = null;
}

Step 3: Declare and initialise local variables

a. The current context b. Reference to the template c. Reference to the currently embedded view

private _thenTemplateRef: TemplateRef<ngLetContext> | null = null;
private _thenViewRef: EmbeddedViewRef<ngLetContext> | null = null;
private _context: ngLetContext = new DhsLetContext();

Step 4: Inject into the constructor a “ViewContainerRef” and a “TemplateRef” and initialise our templateRef

a. The ViewContainerRef, is a reference to where our rendered view will be hosted

b. the TemplateRef, is a reference to the template within of our content

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

Step 5: Define an Input property to accept the binding and update the view ref

@Input() set ngLet(condition: any) {
    this._context.$implicit = this._context.ngLet = condition;
    this._updateView();
  }

Step 6: Update the current view

If there is a condition to evaluate, and we don’t already have a view, then ask our host container to create one based on our template and set the context.

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

Step 7: Ensure the directive is declared in your module (this should be done for you if you used the cli to generate the directive for you)

@NgModule({
  declarations: [
    AppComponent,
    NgLet
    ],

Final step: Put it to use

<div *ngLet="person$ | async as person">
    {{person?.name}}
</div>

Final directive class

import { Directive, ViewContainerRef, TemplateRef, Input, EmbeddedViewRef } from '@angular/core';

@Directive({
  selector: '[ngLet]'
})
export class LetDirective {

  private _thenTemplateRef: TemplateRef<NgLetContext> | null = null;
  private _thenViewRef: EmbeddedViewRef<NgLetContext> | null = null;
  private _context: NgLetContext = new NgLetContext();

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

  @Input()
  set ngLet(condition: any) {
    this._context.$implicit = this._context.ngLet = condition;
    this._updateView();
  }
  private _updateView() {
    if (this._context.$implicit) {
      if (!this._thenViewRef) {
        this._viewContainer.clear();
        if (this._thenTemplateRef) {
          this._thenViewRef =
              this._viewContainer.createEmbeddedView(this._thenTemplateRef, this._context);
        }
      }
    }
  }
}

export class NgLetContext {
  public $implicit: any = null;
  public ngLet: any = null;
}

Although it is possible to create a “Let” directive, I’m not quite convinced on it’s usefulness at this stage, as each inner binding will require the ‘?.’ null check to avoid a ‘name not found on undefined” binding error and the issues associated with such binding errors. Either way, thanks for the challenge @mayeast