Crocsx
Crocsx

Reputation: 7620

Is it possible to add a fallback template ref to a directive?

In Angular 8, I am using a directive to read a value in the store, and not render div depending on the value :

Directive :

@Directive({
    selector: '[appOperator]'
})
export class OperatorDirective implements OnInit, OnDestroy {

    private isOperator = false;
    private subscription: Subscription;

    constructor(private elementRef: ElementRef,
                private viewContainer: ViewContainerRef,
                private templateRef: TemplateRef<any>,
                private store$: Store<RootStoreState.IAppState>) { }

    ngOnInit() {
        this.subscription = this.store$.pipe(select(AuthStoreSelectors.isOperator)).subscribe((isOperator) => {
            this.isOperator = isOperator;
            this.setElementOperation();
        });
    }

    setElementOperation(): void {
        if (this.isOperator) {
            this.viewContainer.createEmbeddedView(this.templateRef);
        } else {
            this.viewContainer.clear();
        }
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }
}

Use case :

      <app-name-input
        class="header_name"
        [name]="waypoint.name"
        (nameChange)="applyRename(waypoint, $event)"
        *appOperator
      ></app-name-input>

I would like to have some sort of fallback template in case the original content is hidden. So I was trying to add a template fallback to the directive that would be displayed in this situation :

I added :

@Input() fallBackTemplateRef: TemplateRef<any>;
setElementOperation(): void {
    if (this.isOperator) {
        this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
        this.viewContainer.clear();

        if(this.fallBackTemplateRef) {
            this.viewContainer.createEmbeddedView(this.fallBackTemplateRef);
        }
    }
}

And I am trying to use it as follow

      <ng-template #myTemplate let-waypoint="waypoint">
        <div class="header_name">{{waypoint.name}}</div>
      </ng-template>
      <app-name-input
        class="header_name"
        [name]="waypoint.name"
        (nameChange)="applyRename(waypoint, $event)"
        appOperator
        *appOperator
        fallBackTemplateRef="myTemplate"
      ></app-name-input>

The problem I face is this compile but do not work in the app, fallBackTemplateRef is always undefined and I can't nothing is drawn

Is it possible to achieve what I am trying to do ? what am I missing ?

Upvotes: 0

Views: 534

Answers (1)

Kurt Hamilton
Kurt Hamilton

Reputation: 13515

In your version, you're not binding to fallbackTemplateRef. And trying to use the [fallbackTemplateRef] syntax would be attempting to bind to an input property on the component.

To bind to additional properties on a structural directive, you can take *ngFor as an example.

*ngFor="let item of collection;trackBy: trackByFn"

In the source code, this is achieved by creating an input property prefixed with the directive selector:

@Input() set ngForTrackBy(fn: TrackByFn) {  
  this._trackByFn = fn;
}

The solution

DEMO: https://stackblitz.com/edit/router-template-vm1hsv

I have modified your directive to take an additional input property in the style of *ngForTrackBy:

import { Directive, OnInit, OnDestroy, ElementRef, ViewContainerRef, TemplateRef, Input } from '@angular/core';

import { Subject, interval } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Directive({
  selector: '[appOperator]'
})
export class OperatorDirective implements OnInit, OnDestroy {  

  constructor(private elementRef: ElementRef,
    private viewContainer: ViewContainerRef,
    private templateRef: TemplateRef<any>
  ) {     
  }

  // dummy default input
  @Input('appOperator') dummy: string;
  @Input('appOperatorFallback') fallback: TemplateRef<any>;

  private isOperator = false;
  private destroyed: Subject<void> = new Subject<void>();

  ngOnInit() {      
    interval(1000).pipe(
      takeUntil(this.destroyed)
    ).subscribe(() => {
      this.isOperator = !this.isOperator;
      this.setElementOperation();
    });

    this.setElementOperation();
  }

  setElementOperation(): void {
    this.viewContainer.clear();

    if (this.isOperator) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else if(this.fallback) {      
      this.viewContainer.createEmbeddedView(this.fallback);
    }
  }

  ngOnDestroy() {
    this.destroyed.next();
    this.destroyed.complete();
  }
}

I used an rxjs interval to allow me to create a working demo. You would replace this with your observable.

This is then used in the html like this:

<ng-template #myfallback>
  <div>
    FALLBACK
  </div>
</ng-template>

<other *appOperator="'';fallback:myfallback">
</other>

Where <other> is some other component.

The breakdown of '';fallback:myfallback is this:

  • '' - this is the input for the @Input('appOperator') dummy input property. It can be literally anything except blank. I chose an empty string, but it could also be an undeclared variable: _;fallback:myfallback
  • ; - input property separator
  • fallback:myfallback is passing the template #myfallback in to the @Input('appOperatorFallback') fallback: TemplateRef<any>; property

My problem with this is that *appLoading="'';fallback:fallback" is ugly. I chose to leave the default input as a dummy since it doesn't make sense given that the fallback isn't a primary input value for the operator directive.

In my research I couldn't find a way to specifiy additional inputs without specifying something in that initial spot.

If you were to only ever have one input and don't like the syntax I have reluctantly chosen, you could always pass the fallback in as the primary input:

@Input('appOperator') fallback: TemplateRef<any>;

And then it's simply a case of using it like this:

*appOperator="myfallback"

To me there is some cognitive dissonance with this, but at least it's pretty :)

Upvotes: 1

Related Questions