4

I am trying to improve my code rather than having conditions, so I decided to create a directive or maybe a pipe if possible that could help me print the title of a tab depending on its type (string or templateRef), my code is the following, this code is used in my Tabs/Tab component, but also in my Stepper/step component, so I believe it would be great to create something reusable. I've tried doing it with ElementRef, Renderer2, ViewContainerRef, TemplateRef... but I was unsuccessful.

<ng-container *ngIf="tab.isLabelTemplate">
     <ng-container *ngTemplateOutlet="tab.title">
     </ng-container>
</ng-container>
<ng-container *ngIf="!tab.isLabelTemplate">{{ tab.title }}</ng-container>

The isLabelTemplate looks like this:

get isLabelTemplate(): boolean {
  return this.title instanceof TemplateRef;
}

Thanks a lot :)

Youness Houdass
  • 204
  • 4
  • 15
  • Can you please replicate the issue in stackblitz? – yurzui Jan 08 '20 at 19:19
  • https://stackblitz.com/edit/angular-vjmkav I would like to create a directive or pipe something like ``` or titleTemplate="title"``` rather than repeating ``` {{ tab.title }}``` I would like a directive that do this stuff of printing the title rather than having ifs in html – Youness Houdass Jan 08 '20 at 19:29

2 Answers2

3

You can leverage Angular low-level API to dynamically manipulate DOM's structure:

Here's an example of how that directive can look like:

title-template.directive.ts

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

@Directive({
  selector: '[titleTemplate]',
})
export class TitleTemplateDirective {
  @Input()
  set titleTemplate(value: string | TemplateRef<any>) {
    this.updateView(value);
  }

  textNode: Text;

  constructor(private vcRef: ViewContainerRef, private renderer: Renderer2) {}

  private updateView(value: string | TemplateRef<any>) {
    this.clear();

    if (!value) {
      return;
    }

    if (value instanceof TemplateRef) {
      this.vcRef.createEmbeddedView(value);
    } else {
      this.textNode = this.renderer.createText(value);
      const elem = this.vcRef.element.nativeElement;

      this.renderer.insertBefore(elem.parentNode, this.textNode, elem);
    }
  }

  private clear() {
    this.vcRef.clear();
    if (this.textNode) {
      this.renderer.removeChild(this.textNode.parentNode, this.textNode);
    }
  }

  ngOnDestroy() {
    this.clear();
  }
}

Usage:

<ng-container [titleTemplate]="title"></ng-container>  

Forked Stackblitz

yurzui
  • 205,937
  • 32
  • 433
  • 399
2

Or you can just move this logic to a "helper" component and use it in your TabComponent. So this logic is on a single place (DRY). The helper component (TitleComponent) is the same you used in TabComponent. This way you just move the "problem" but don't have to repeat it again and again. And it is still easy to read.

...
import { Component, Input, TemplateRef } from '@angular/core';

@Component({
  selector: 'app-title',
  template: `
    <ng-container *ngIf="isLabelTemplate">
      <ng-container *ngTemplateOutlet="title">
      </ng-container>
    </ng-container>
    <ng-container *ngIf="!isLabelTemplate">{{ title }}</ng-container>
  `,
})
export class TitleComponent  {
  @Input() title: string | TemplateRef<any>;

  get isLabelTemplate(): boolean {
    return this.title instanceof TemplateRef;
  }
}


@Component({
  selector: 'app-tab',
  template: `
    <app-title [title]="title"></app-title>
  `,
})
export class TabComponent  {
  @Input() title: string | TemplateRef<any>;
}

...
brandt.codes
  • 923
  • 2
  • 9
  • 19