Search code examples
angulardomangular-directive

Angular styling viewContainerRef from Directive Input array


I have created a directive that uses a variable length array to populate a tooltip. This works great but I need to dynamically style the tooltip so it remains under the initial trigger component. Using a top or bottom value that changes based on the number of items.

<div tooltipDirective [tooltipDataArray]="['Person1', 'Person2', 'Person3', 'Person4', 'Person5', 'Person6']">See tooltip!
 <ng-template #tooltipTemplate > 
  <div class="tooltip" [ngStyle]="{'top.px': divStyle}">   // Not sure if this is the correct approach as can't bind to divStyle in the directive
  </div>      
 </ng-template>  
</div>

I tried using ngStyle but am unsure how to get access to the divStyle value because this is created using viewContainerRef.createEmbeddedView.

I thought a better option would be to add the styles from the ts file using style.bottom but I don't know how to add that. I need to calculate tooltipDataArray.length then add 10px or so to a variable that repositions the viewContainerRef. I'm not sure of the best way to proceed.

 @Input() tooltipDataArray: string[];

 @ContentChild("tooltipTemplate") private tooltipTemplateRef: TemplateRef<Object>;

 @HostListener("mouseenter") onMouseEnter(): void {
  console.log(this.tooltipDataArray);
   const view = this.viewContainerRef.createEmbeddedView(
     this.tooltipTemplateRef
   );

  this.tooltipDataArray.forEach(el => {
  const child = document.createElement("div");
  child.innerText = el;
  this.renderer.appendChild(view.rootNodes[1], child);
  });
  
  // Somthing like this.viewContainerRef.styles.bottom = 10 x this.tooltipDataArray.length + 'px'
  
  console.log(view.rootNodes)
  view.rootNodes.forEach(node => {
  this.renderer.appendChild(this.elementRef.nativeElement, node);
});
}

@HostListener("mouseleave") onMouseLeave(): void {
if (this.viewContainerRef) {
  this.viewContainerRef.clear();
}

stackBlitz here


Solution

  • If you were willing to pass in the templateRef as an input to the directive this would be a lot easier...

    With your current implementation you are replacing the content of the div with the rendered content of the template...

    • This essentially is not a tooltip and you would need to decouple them somehow to "simulate a tooltip"

    Below is one way you could accomplish this.

    Separate the ng-template from the div to decouple them, and pass your #tooltipTemplate as a value to the [templateRef] input on the directive

    <div tooltipDirective [templateRef]="tooltipTemplate" [tooltipDataArray]="['Person1', 'Person2']">See tooltip!
    </div>
    <ng-template #tooltipTemplate>      
        <div class="tooltip">   
            This is my tooltip!
        </div>      
    </ng-template>  
    

    In your directive convert your @ContentChild to an input to receive the templateRef, create your embeddedView and add your array elements.

    • This also simplifies your logic here
      @Input() templateRef: TemplateRef<Object>;
    
      @HostListener("mouseenter") onMouseEnter(): void {
        const view = this.viewContainerRef.createEmbeddedView(this.templateRef);
        this.tooltipDataArray.forEach(el => {
          const child = document.createElement("div");
          child.innerText = el;
          this.renderer.appendChild(view.rootNodes[1], child);
        });
      }
    

    Adjust your global styling

    .tooltip {
      position: absolute;
      /* bottom: -40px; */
      left: 15px;
      padding: 10px;
      background: red;
      border-radius: 5px;
      /* box-shadow: 0 2px 1px rgba(0, 0, 0, 0.6); */
    }
    

    STACKBLITZ

    https://stackblitz.com/edit/angular-zr2ydx?file=app/tooltip.directive.ts


    This would be the cleanest implementation with the scaffolding you have provided... with that said, if I were to implement a tooltip directive, I would research CDK Overlay to create a custom tooltip implementation.