0

I'm implementing a generic tooltip in Angular 5. For the proper positioning (especially centering relative to the target element) I need to get the width and height of the tooltip before rendering it.

I already know how to get the dimensions of a component from this question. However, I need to set the content of the tooltip first (to let the browser determine the size) to calculate the position. Then, second, I want to set the position and add a class to show it.

Here is my code:

// tooltip.component.html
<div class="popup fade"
  [ngClass]="tooltip?.isOpen ? 'show' : ''"
  [style.left.px]="calculatedPos?.left"
  [style.top.px]="calculatedPos?.top">
  {{ tooltip?.text }}
</div>

// tooltip.component.ts
export class TooltipComponent {
  public tooltip: any;
  public calculatedPos: TooltipPosition = {
    left: 0,
    top: 0
  }

  constructor(private store: Store<any>, private ref: ElementRef) {
    this.store.subscribe((state) => this.render(state));
  }

  render () {
    let targetRect = this.tooltip.targetEl 
          && this.tooltip.targetEl.nativeElement
          && this.tooltip.targetEl.nativeElement.getBoundingClientRect()
          || null;
    let tooltipRect = this.ref 
          && this.ref.nativeElement
          && this.ref.nativeElement.getBoundingClientRect()
          || null;
    if (targetRect && tooltipRect) { // <-- tooltipRect.height /.width is zero here!
      switch (this.tooltip.placement) {
        case 'right':
          this.calculatedPos = this.calculatePositionRight(targetRect, tooltipRect, this.tooltip.offset)
          break;
          // and so on...
  }
}

I tried using lifecycleHooks:

  • ngAfterContentChecked leads to the same result
  • ngAfterViewChecked gives me (foreseeable) an ExpressionChangedAfterItHasBeenCheckedError
  • ngOnChanges is not called at all when rendering

So: How can I get the dimensions of the Component before the content is rendered? Or how can I detect the content being rendered and then updating the position?

My next thought would be using a timeout to first let the component render and then setting the position (without the user noticing) - but maybe there is a best practice, which I don't know about yet.

Florian Gössele
  • 4,376
  • 7
  • 25
  • 49
  • You can't get the height from a element that hasn't been computed by the DOM yet. So like you suggest; Let it render and hide, or give the element a fixed width. Note using `%` to set a width is also possible. – JoeriShoeby Apr 20 '18 at 15:50

3 Answers3

2

I think there is no way around setting up the subscription in ngAfterViewInit. To get around the ExpressionChangedAfterItHasBeenCheckedError, you can tell Angular that you "know what you are doing". Inject the ChangeDetectorRef and place a detectChanges-invocation at the end of your render-Method.

dummdidumm
  • 4,828
  • 15
  • 26
1

In order to get the dimensions of an element, you will have to wait for it to be rendered.

Without the element being present in the DOM, you cannot get its dimensions.

Use ngAfterViewInit, by this time, view is initialized.

ashfaq.p
  • 5,379
  • 21
  • 35
  • I tried it with `ngAfterViewInit` but it gets called only once. The Tooltip however is reused with different content every time. As I said, I also cannot use `ngAfterViewChecked` either... – Florian Gössele Apr 21 '18 at 09:59
0

I trigger this from ngAfterViewInit and then it fires again whenever the resize event happens. But instead of assigning in the constructor, reference a wrapper element in the template.

  <div #componentElement>
    ... normal template contents
  </div>

and then in ts.

  @ViewChild("componentElement") componentElementRef: ElementRef


  ngAfterViewInit(): void {
    this.render()
  }

  @HostListener("window:resize")
  onResize(): void {
    this.render()
  }

  render () {
    console.log(this.componentElementRef.nativeElement.getBoundingClientRect())
    ... your code here ...
  }
Lowell
  • 61
  • 1
  • 4