Build Angular Tooltip Without Libs - Angular Dynamic Component
Build Angular Tooltip Without Libs - Angular Dynamic Component

In this post you will learn how to implement a custom tooltip inside Angular without any additional libraries.

Finished project

And just from the beginning I want to tell you that implementing tooltip is not an easy task. This is why if you just need a quick solution you can take a library like ng-bootstrap or angular-material.

Ng-bootstrap

I prefer ng-bootstrap as it is easier to use. They both are configurable to render tooltip is body which is extremely important to avoid problems with overflow.

This is the main point why tooltip is difficult to implement. We must always render it in body which is quite tricky to do with Angular.

Additionally we need to calculate correct position of the tooltip if we want to make it flexible.

Architecture

<button>
  Hover to see tooltip
</button>

Here I created just a button which must have a tooltip later. Additionally I prepared an empty tooltip component and tooltip directive which we will implement in this post. The main idea is that component will render styles tooltip and directive will have logic of showing looking like clicking or hover.

@Component({
  selector: 'tooltip',
  templateUrl: './tooltip.component.html',
  styleUrls: ['./tooltip.component.css'],
})
export class TooltipComponent {}
@Directive({
  selector: '[tooltip]',
})
export class TooltipDirective {}

Also I prepared styling for our tooltip so we don't need to spend time on it later.

.tooltip {
  position: fixed;
  background-color: black;
  border-radius: 4px;
  color: #ffffff;
  font-family: Arial;
  padding: 3px 6px;
  font-size: 13px;
  margin-top: 5px;
  transform: translateX(-50%);
}

.tooltip::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-bottom: 5px solid black;
  position: absolute;
  left: calc(50% - 5px);
  top: -5px;
}

Adding inputs

Now let's add inputs to our tooltip. We need to provide here text that we want to render inside tooltip, but also left and top which are positions of our tooltip as it is positioned with absolute.

export class TooltipComponent {
  @Input() text = '';
  @Input() left = 0;
  @Input() top = 0;
}

Now we can use all these inputs inside HTML.

<div class="tooltip" [style.left]="left + 'px'" [style.top]="top + 'px'">
  {{ text }}
</div>

Here we set left and top to position our element correctly. We also rendered text inside.

Directive usage

Now it is time to implement our directive which is responsible for showing and hiding our tooltip as well as providing inside correct inputs.

<button tooltip tooltipText="This is our tooltip text">
  Hover to see tooltip
</button>

Here is the usage of our tooltip. We attach this directive to our button and provide inside tooltipText that we want to render. This code is enough to implement mouseover and mouseout inside our directive.

export class TooltipDirective {
  @Input() tooltipText = '';

  @HostListener('mouseenter')
  onMouseEnter(): void {
    console.log('onMouseEnter');
  }

  @HostListener('mouseleave')
  onMouseLeave(): void {
    console.log('onMouseLeave');
  }
}

Here we created our tooltipText input and 2 events for mouseenter and mouseleave.

export class TooltipDirective {
  @Input() tooltipText = '';

  private tooltipComponent?: ComponentRef<any>;

  @HostListener('mouseenter')
  onMouseEnter(): void {
    console.log('onMouseEnter');
    if (this.tooltipComponent) {
      return;
    }

    const tooltipComponentFactory = this.componentFactoryResolver.resolveComponentFactory(
      TooltipComponent
    );
    this.tooltipComponent = tooltipComponentFactory.create(this.injector);
    this.document.body.appendChild(
      this.tooltipComponent.location.nativeElement
    );
    this.tooltipComponent.hostView.detectChanges();
  }

  constructor(
    private componentFactoryResolver: ComponentFactoryResolver,
    private injector: Injector,
    private elementRef: ElementRef,
    private appRef: ApplicationRef,
    @Inject(DOCUMENT) private document: Document
  ) {}
};

Now inside our onMouseEnter we first check if our tooltip component is already rendered. If it is then we don't do anything. Here we created tooltipComponentFactory which knows how to create TooltipComponent. After this we created tooltipComponent and appended it to the body.

Setting position

export class TooltipDirective {
  @Input() tooltipText = '';

  private tooltipComponent?: ComponentRef<any>;

  @HostListener('mouseenter')
  onMouseEnter(): void {
    ...
    this.document.body.appendChild(
      this.tooltipComponent.location.nativeElement
    );
    this.setTooltipComponentProperties();
    this.tooltipComponent.hostView.detectChanges();
  }

  private setTooltipComponentProperties() {
    if (!this.tooltipComponent) {
      return;
    }
    this.tooltipComponent.instance.text = this.tooltipText;
    const {
      left,
      right,
      bottom,
    } = this.elementRef.nativeElement.getBoundingClientRect();
    this.tooltipComponent.instance.left = (right - left) / 2 + left;
    this.tooltipComponent.instance.top = bottom;
  }
  ...
}

Here we need to set left and top correctly in our tooltip component. This is why we called setTooltipComponentProperties function which does exactly that. Here we calculate left and top properties and provide them inside our tooltip instance.

Adding mouse leave

The last thing that we need to implement is mouseleave.

@HostListener('mouseleave')
onMouseLeave(): void {
  console.log('onMouseLeave');
  if (!this.tooltipComponent) {
    return;
  }
  this.appRef.detachView(this.tooltipComponent.hostView);
  this.tooltipComponent.destroy();
  this.tooltipComponent = undefined;
}

Here we remove component from Angular and destroy it.

Finished project

As you can see in browser our tooltip is fully implemented.

And actually if you want to learn Angular with NgRx from empty folder to a fully functional production application make sure to check my Angular and NgRx - Building Real Project From Scratch course.

📚 Source code of what we've done