Custom Tooltip Component using Angular's Style Directive and Coordinates from Web APIs MouseEvent

Ria Pacheco - Jun 18 '22 - - Dev Community

In this article, we will create a tooltip component that reflects data fed from a parent component and appears next to the cursor when hovering over a target element.

final tooltip
Fork the repo


Execution Workflow

  1. Create an app and add dependencies for quick styling
  2. Create a list view (parent) component for data the tooltip will read
  3. Create the actual tooltip component with basic styles
  4. Provide a way for the tooltip to consume coordinates
  5. Pull coordinates from the parent component
  6. Pull data from the parent component
  7. Show data on hover
  8. Add conditions for the tooltip and for an ellipsis

Skip Ahead


Create App and Add Dependencies

Assuming you have npm and @angular/cli installed, create a new app by running the following command in your terminal:



ng new global-tooltip --skip-tests


Enter fullscreen mode Exit fullscreen mode
  • The --skip-tests flag stops @angular/cli from generating test files
  • Reply N, when prompted with Would you like to add Angular routing?
  • Select SCSS when prompted with Which stylesheet format would you like to use? so we can use an SCSS package.

Install SCSS Package

To keep this article high-level, we'll install @riapacheco/yutes to use its shorthand utility classes for quicker setup. Install from your terminal:



npm install @riapacheco/yutes


Enter fullscreen mode Exit fullscreen mode

Don't forget to add this to your main styles.scss file:



// styles.scss
@import '~@riapacheco/yutes/main.scss';


Enter fullscreen mode Exit fullscreen mode

Import CommonModule

Add the following import to your app.module.ts:



import { AppComponent } from './app.component';
import { BrowserModule } from '@angular/platform-browser';
// Add this ⤵️
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    // and ⤵️
    CommonModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


Enter fullscreen mode Exit fullscreen mode

Create a List View Component

Create a new List View component by running the following in your terminal:



ng g c views/list-view


Enter fullscreen mode Exit fullscreen mode

Replace the default content found in the app.component.html file with the new component's selector like this:



<!--app.component.html-->
<app-list-view></app-list-view>


Enter fullscreen mode Exit fullscreen mode

Add Data

In the list-view.component.ts file, add the following reactors array so that we have data to work with:



// ... other code

export class ListViewComponent implements OnInit {
  // Add this ⤵️
  reactors = [
    { name: 'Pressurized Water Reactor (PWR)' },
    { name: 'Boiling Water Reactor (BWR)' },
    { name: 'Advanced Gas-Cooled Reactor (AGR)' },
    { name: 'Light Water Graphite-Moderated Reactor (LWGR)' },
    { name: 'Fast Neutron Reactor (FNR)' },
    { name: 'Operable Nuclear Power Plants' }
  ];

  constructor() { }

  ngOnInit(): void { }

}


Enter fullscreen mode Exit fullscreen mode

Add Structure

We want to create a container that will display our data in a list. We'll then make this container narrow enough to cut off the listed text within it.

Add the following code to the list-view.component.html file:



<div class="mx-auto-850px pt-3">
  <ul class="list-unstyled">
    <li>
      <h2>
        This is a listed item
      </h2>
    </li>
  </ul>
</div>


Enter fullscreen mode Exit fullscreen mode

The classes used here are from the @riapacheco/yutes package:

  • mx-auto-850px creates an 850px-wide wrapping container that's centered horizontally
  • pt-3 adds 3rem of padding to the top of the element
  • list-unstyled strips the default browser styling from the unordered list (<ul></ul>)
  • Learn more from @riapacheco/yutes docs on NPM

Bind the Data

Now we can bind the data so that the template will render a list item (<li></li>) for each of the items found in the reactors array we created earlier. We do this by employing Angular's *ngFor directive:



<!--list-view.component.html-->
<div class="mx-auto-850px pt-3">
  <ul class="list-unstyled">
    <li *ngFor="let reactor of reactors">
      <h2>
        {{ reactor.name }}
      </h2>
    </li>
  </ul>
</div>


Enter fullscreen mode Exit fullscreen mode

Now if you run ng serve in your terminal, you'll see this in the local app:
Image description

Narrow the Container

To make it so we can cut the text off (and be able to see where it's cut off) add the following to your list-view.component.scss file:



ul {
  width: 280px;
  max-width: 280px;
  border: 1px solid #00000030;
  padding-left: 1rem;

  li {
    white-space: nowrap;
    overflow: hidden;
  }
}


Enter fullscreen mode Exit fullscreen mode

Now your local app should look like this:
Image description


Create the Tooltip Component

Now that we have something to read, we can create the tooltip component by running the following command in the terminal:



ng g c components/tooltip


Enter fullscreen mode Exit fullscreen mode

Now add the new component's selector to the top of the list-view.component.html.



<!--list-view.component.html-->
<app-tooltip></app-tooltip>

<div class="mx-auto-850px pt-3">
  <ul class="list-unstyled">
    <li *ngFor="let reactor of reactors">
      <h2>
        {{ reactor.name }}
      </h2>
    </li>
  </ul>
</div>


Enter fullscreen mode Exit fullscreen mode

Add Properties and Styles

First we'll add a property that tells us if the tooltip should show up or not and another one to bind some default text when it's not reading from a different source. Add the following to the tooltip.component.ts file:



// more code
export class TooltipComponent implements OnInit {

  showsTooltip = true;
  tooltipText = 'Default tooltip text';

  constructor() {}
  ngOnInit(): void {}
}


Enter fullscreen mode Exit fullscreen mode

And since we want to share these properties with other parent components, we'll prefix both properties with the @Input() decorator that we'll import from Angular's core package:



// import here ⤵️
import { Component, Input, OnInit } from '@angular/core';

@Component({
  selector: 'app-tooltip',
  templateUrl: './tooltip.component.html',
  styleUrls: ['./tooltip.component.scss']
})
export class TooltipComponent implements OnInit {
  // ⤵️ Prefix here
  @Input() showsTooltip = true;
  @Input() tooltipText = 'Default tooltip text';

  constructor() { }

  ngOnInit(): void {
  }
}


Enter fullscreen mode Exit fullscreen mode

Bind the Data Again

We'll bind the text to a div (that's assigned the class tooltip) and wrap that div in an ng-container that conditionally appears based on our showsTooltip boolean so that there's no chance for the element to show up in the DOM:



<ng-container *ngIf="showsTooltip">
  <div class="tooltip">
    {{ tooltipText }}
  </div>
</ng-container>


Enter fullscreen mode Exit fullscreen mode

To effectively see if the tooltip is visible or not, we'll add styles to the tooltip class in the tooltip.component.scss file:



.tooltip {
  position: absolute; // enables it to appear where we want it later
  font-size: 14px;
  line-height: 14px;
  padding: 5px 10px;
  background-color: #00000099;
  color: white;
  border-radius: 3px;
  display: inline-block;
  box-shadow: 4px 6px 12px #00000020;
}


Enter fullscreen mode Exit fullscreen mode

Now your local app should look like this:
Image description


Feeding the Tooltip the Right Coordinates

MouseEvent Properties Explainer

Since we want the tooltip to appear next to our cursor when it hovers over a listed item, we need to isolate exactly where our cursor is when it does. Since the mouse event (web API) can identify where it is along the viewport's X and Y axis, this is how we'll position our tooltip! (more on this in a bit)

Angular's Style Directive

After we successfully capture those coordinates, we'll need to feed them to the component so that it adjusts itself to that location. Angular's style directive allows you to target a specific attribute on an element (e.g. color) and populate its values with bounded data.

The syntax encloses style.<property> in square brackets, followed by the value to be applied to that property.

As an example:



<div [style.color]="'red'">
  This text is red
</div>


Enter fullscreen mode Exit fullscreen mode

Though I used a string ['red'] to populate the above property, it can be replaced with values pulled directly from the component file.

Let's create two properties to represent the values to be binded to our tooltip's top and left values (with some random coordinates to confirm that it works):



// tooltip.component.ts
export class TooltipComponent implements OnInit {
  @Input() showsTooltip = true;
  @Input() tooltipText = 'Default tooltip text';
  // Add these ⤵️
  @Input() topPosition = 215;
  @Input() leftPosition = 400;

  constructor() { }

  ngOnInit(): void { }
}


Enter fullscreen mode Exit fullscreen mode

Now bind these to the tooltip in the tooltip.component.html file:



<ng-container *ngIf="showsTooltip">
  <div
    class="tooltip"
    [style.top]="topPosition + 'px'"
    [style.left]="leftPosition + 'px'"
    >
    {{ tooltipText }}
  </div>
</ng-container>


Enter fullscreen mode Exit fullscreen mode

This should move the tooltip to the location we specified:
Image description


Capturing Coordinates from Parent Component

Now that the tooltip can consume location coordinates and has @Input() prefixed to each of its behavior-driving properties, we can now reflect data from the parent component.

In the list-view.component.ts file, create properties with the same labels like so:



export class ListViewComponent implements OnInit {
  reactors = [
        // more code
  ];

  // Add these ⤵️
  showsTooltip = false;
  tooltipText = 'This is default parent component text';
  topPosition: any;
  leftPosition: any;

  constructor() { }

  ngOnInit(): void { }
}


Enter fullscreen mode Exit fullscreen mode

Bind to the Parent

To hook these values up, bind the data from the tooltip's selector:



<app-tooltip
  [showsTooltip]="showsTooltip"
  [tooltipText]="tooltipText"
  [topPosition]="topPosition"
  [leftPosition]="leftPosition">
</app-tooltip>

<!-- more code -->


Enter fullscreen mode Exit fullscreen mode

onHover Method

We want the text string that appears in the tooltip to reflect the item that we're hovering over. Add a (mouseover) event to the listed item in the template and have it trigger a method that takes in that listed item's name and the event itself. Additionally, add a (mouse out) event to hide the tooltip when the cursor leaves the element (it will not take in any arguments):



<!-- tooltip.component.html -->

<!-- more code -->

<div class="mx-auto-850px pt-3">
  <ul class="list-unstyled">

    <!-- Add this ⤵️ -->
    <li
      *ngFor="let reactor of reactors"
      (mouseover)="onHover(reactor.name, $event)"
      (mouseout)="onMouseout()"
      >
      <h2>
        {{ reactor.name }}
      </h2>
    </li>
  </ul>
</div>


Enter fullscreen mode Exit fullscreen mode

Now we'll create these methods in the template itself



// tooltip.component.ts file
export class ListViewComponent implements OnInit {

  // ... more code

  constructor() { }

  ngOnInit(): void { }

  // Methods here ⤵️
  onHover(tooltipText: string, e: MouseEvent) {
    this.showsTooltip = true;
    this.tooltipText = tooltipText;
    this.topPosition = e.clientY;
    this.leftPosition = e.clientX;
  }
  onMouseout() {
    this.showsTooltip = false;
    this.tooltipText = '';
    this.topPosition = null;
    this.leftPosition = null;
  }
}


Enter fullscreen mode Exit fullscreen mode

This is what it should look like (hooray!):
Hovering tooltip


Conditional Tooltip Trigger and Ellipsis

Now that we have a tooltip, we want to trigger the tooltip only when necessary. In this case, it should only appear if the content is cut off from the width of the container.

Ellipsis with Slice Pipe

Angular's slice pipe allows you to specify characters found in a concatenated template string as though they're items in an array. You follow the | slice: syntax with a number that indicates the first array item's (character's) position, followed by the count of characters that should be displayed.



<!-- more code -->

<div class="mx-auto-850px pt-3">
  <ul class="list-unstyled">
    <li
      *ngFor="let reactor of reactors"
      (mouseover)="onHover(reactor.name, $event)"
      (mouseout)="onMouseout()"
      >
      <h2>
        <!-- here's the slice pipe -->
        {{ reactor.name | slice: 0:17 }}
      </h2>
    </li>
  </ul>
</div>


Enter fullscreen mode Exit fullscreen mode

Now notice how it renders in the view:
Image description

Add an Ellipsis with a Conditional Span

We can create an ellipsis to immediately follow every listed item that contains an ellipsis. However, since we only want those visually cut-off items to show it, we can add *ngIf to the span itself, and specify the number of characters that would qualify it to appear:



<!-- more code -->
<div class="mx-auto-850px pt-3">
  <ul class="list-unstyled">
    <li
      *ngFor="let reactor of reactors"
      (mouseover)="onHover(reactor.name, $event)"
      (mouseout)="onMouseout()"
      >
      <h2>
        <!-- Access character count with `.length` -->
        {{ reactor.name | slice: 0:17 }}<span *ngIf="reactor.name.length >= 17">...</span>
      </h2>
    </li>
  </ul>
</div>


Enter fullscreen mode Exit fullscreen mode

Here's the result (I added a "Short Title" for easier differentiation):
Image description

Make the Tooltip Conditional

Since there wouldn't really be a point to showing a Tooltip on a fully displayed (shorter) title, we can add the same condition we applied to the span, but to the mouseover event.



<!-- tooltip.component.html -->
<div class="mx-auto-850px pt-3">
  <ul class="list-unstyled">
    <li
      *ngFor="let reactor of reactors"
      (mouseover)="reactor.name.length >= 17 ? onHover(reactor.name, $event) : ''"
      (mouseout)="onMouseout()"
      >
      <h2>
        {{ reactor.name | slice: 0:17 }}<span *ngIf="reactor.name.length >= 17">...</span>
      </h2>
    </li>
  </ul>
</div>


Enter fullscreen mode Exit fullscreen mode

Notice that the (mouseover) event is now defined with a ternary operator, which tells us: IF the character length of this item's name is greater or equal to 17, THEN trigger the onHover(...) method; else do nothing.


Result

Now when you hover over any item that's cut off (has an ellipsis), the tooltip will trigger and reflect the items full text. If you hover over the item that isn't, it won't do a damn thing.
final tooltip

Back to "Skip Ahead" list
Fork the repo

Ri

. . . . . . . . . . . . . . . . .