Ng directive Thought Experiment
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:
it’s a pity they enabled the “let” option on ngif. what if i dont want to hide the section before the data arrive? May East (@mayeast) May 2, 2018
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