Custom Tooltip Component using Angular's Style Directive and Coordinates from Web APIs MouseEvent
Tags: angular, typescript, webdev, javascript
Published: 2022-06-18 04:08:21

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](https://ik.imagekit.io/fuc9k9ckt2b/Blog_Post_Images/Dev_to/final-tooltip_U3GTOpNT8.gif?ik-sdk-version=javascript-1.4.3&updatedAt=1655525142032)
[Fork the repo](https://github.com/riapacheco/custom-tooltip)

---

### 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**](#create-app-and-add-dependencies)
 - [Install **SCSS** Package](#install-scss-package)
 - [Import **CommonModule**](#import-commonmodule)
- [Create a **List View** Component](#create-a-list-view-component)
 - [Add **Data**](#add-data)
 - [Add **Structure**](#add-structure)
 - [**Bind** the Data](#bind-the-data)
 - [**Narrow** the Container](#narrow-the-container)
- [Create the **Tooltip Component**](#create-the-tooltip-component)
 - [Add **Properties** and **Styles**](#add-properties-and-styles)
 - [**Bind** the Data Again](#bind-the-data-again)
- [Feeding the Tooltip the Right **Coordinates**](#feeding-the-tooltip-the-right-coordinates)
 - [**MouseEvent** Properties Explainer](#mouseevent-properties-explainer)
 - [Angular's **Style** **Directive**](#angulars-style-directive)
- [Capturing **Coordinates** **from** **Parent** Component](#capturing-coordinates-from-parent-component)
 - [**Bind** to the Parent](#bind-to-the-parent)
 - [**onHover** Method](#onhover-method)
- [Conditional **Tooltip** **Trigger** and Ellipsis](#conditional-tooltip-trigger-and-ellipsis)
 - [Ellipsis with **Slice** **Pipe**](#ellipsis-with-slice-pipe)
 - [Add an Ellipsis with a **Conditional** **Span**](#add-an-ellipsis-with-a-conditional-span)
 - [Make the Tooltip **Conditional**](#make-the-tooltip-conditional)
- [**Result**](#result)

---

# 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:
```bash
ng new global-tooltip --skip-tests
```
* 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:
```bash
npm install @riapacheco/yutes
```
Don't forget to add this to your main `styles.scss` file:
```scss
// styles.scss
@import '~@riapacheco/yutes/main.scss';
```
* Read [@riapacheco/yutes documentation on NPM](https://www.npmjs.com/package/@riapacheco/yutes)

## Import CommonModule
Add the following import to your `app.module.ts`:
```typescript
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 { }
```

---

# Create a List View Component
Create a new *List View* component by running the following in your terminal:
```bash
ng g c views/list-view
```
Replace the default content found in the `app.component.html` file with the new component's selector like this:
```html
<!--app.component.html-->
<app-list-view></app-list-view>
```

## Add Data
In the `list-view.component.ts` file, add the following `reactors` array so that we have data to work with:
```typescript
// ... 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 { }

}
```

## 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:
```html
<div class="mx-auto-850px pt-3">
  <ul class="list-unstyled">
    <li>
      <h2>
        This is a listed item
      </h2>
    </li>
  </ul>
</div>
```
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](https://www.npmjs.com/package/@riapacheco/yutes)

## 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:
```html
<!--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>
```
Now if you run `ng serve` in your terminal, you'll see this in the local app:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8g0wy1foxxom2y0xexjw.png)

## 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:
```scss
ul {
  width: 280px;
  max-width: 280px;
  border: 1px solid #00000030;
  padding-left: 1rem;
  
  li {
    white-space: nowrap;
    overflow: hidden;
  }
}
```

Now your local app should look like this:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ln55bkfaavfx3iryhlsm.png)


---

# 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:
```bash
ng g c components/tooltip
```

Now add the new component's selector to the top of the `list-view.component.html`.
```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>
```

## 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:
```typescript
// more code
export class TooltipComponent implements OnInit {
  
  showsTooltip = true;
  tooltipText = 'Default tooltip text';
  
  constructor() {}
  ngOnInit(): void {}
}
```

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:
```typescript
// 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 {
  }
}
```

## 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:
```html
<ng-container *ngIf="showsTooltip">
  <div class="tooltip">
    {{ tooltipText }}
  </div>
</ng-container>
```

To effectively see if the tooltip is visible or not, we'll add styles to the `tooltip` class in the `tooltip.component.scss` file:
```scss
.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;
}
```

Now your local app should look like this:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7q6wh6t28yqez3om0x8t.png)

---

# 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](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) (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:
```html
<div [style.color]="'red'">
  This text is red
</div>
```

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):
```typescript
// 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 { }
}
```

Now bind these to the tooltip in the `tooltip.component.html` file:
```html
<ng-container *ngIf="showsTooltip">
  <div
    class="tooltip"
    [style.top]="topPosition + 'px'"
    [style.left]="leftPosition + 'px'"
    >
    {{ tooltipText }}
  </div>
</ng-container>
```

This should move the tooltip to the location we specified:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9dcnkq1t7zvzwr96pr2f.png)

---

# 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:
```typescript
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 { }
}
```

## Bind to the Parent
To hook these values up, bind the data from the tooltip's selector:
```html
<app-tooltip
  [showsTooltip]="showsTooltip"
  [tooltipText]="tooltipText"
  [topPosition]="topPosition"
  [leftPosition]="leftPosition">
</app-tooltip>

<!-- more code -->
```

## 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):
```html
<!-- 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>
```

Now we'll create these methods in the template itself
```typescript
// 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;
  }
}
```

This is what it should look like (hooray!):
![Hovering tooltip](https://ik.imagekit.io/fuc9k9ckt2b/Blog_Post_Images/Dev_to/onhover-tooltip_pLDHZVH-Z.gif?ik-sdk-version=javascript-1.4.3&updatedAt=1655524839044)



---

# 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](https://angular.io/api/common/SlicePipe) 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.
```html
<!-- 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>
```

Now notice how it renders in the view:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nq2w6h1mstjl7xigy0gt.png)


## 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:
```html
<!-- 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>
```

Here's the result (I added a "Short Title" for easier differentiation):
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mk9zrc2f2hl6gniyskyf.png)

## 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. 
```html
<!-- 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>
```
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](https://ik.imagekit.io/fuc9k9ckt2b/Blog_Post_Images/Dev_to/final-tooltip_U3GTOpNT8.gif?ik-sdk-version=javascript-1.4.3&updatedAt=1655525142032)

[Back to "Skip Ahead" list](#skip-ahead)
[Fork the repo](https://github.com/riapacheco/custom-tooltip)

Ri