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.
Execution Workflow
- Create an app and add dependencies for quick styling
- Create a list view (parent) component for data the tooltip will read
- Create the actual tooltip component with basic styles
- Provide a way for the tooltip to consume coordinates
- Pull coordinates from the parent component
- Pull data from the parent component
- Show data on hover
- Add conditions for the tooltip and for an ellipsis
Skip Ahead
- Create App and Add Dependencies
- Create a List View Component
- Create the Tooltip Component
- Feeding the Tooltip the Right Coordinates
- Capturing Coordinates from Parent Component
- Conditional Tooltip Trigger and Ellipsis
- 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:
ng new global-tooltip --skip-tests
- The
--skip-tests
flag stops@angular/cli
from generating test files - Reply
N
, when prompted withWould you like to add Angular routing?
- Select
SCSS
when prompted withWhich 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
Don't forget to add this to your main styles.scss
file:
// styles.scss
@import '~@riapacheco/yutes/main.scss';
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 { }
Create a List View Component
Create a new List View component by running the following in your terminal:
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:
<!--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:
// ... 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:
<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
adds3rem
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>
Now if you run ng serve
in your terminal, you'll see this in the local app:
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;
}
}
Now your local app should look like this:
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
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>
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 {}
}
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 {
}
}
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>
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;
}
Now your local app should look like this:
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>
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 { }
}
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>
This should move the tooltip to the location we specified:
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 { }
}
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 -->
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>
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;
}
}
This is what it should look like (hooray!):
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>
Now notice how it renders in the view:
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>
Here's the result (I added a "Short Title" for easier differentiation):
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>
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.
Back to "Skip Ahead" list
Fork the repo
Ri
Top comments (0)