In our application, I was faced with the task of creating a beautiful tooltip, in the Angular Material table. We drew the design, and I started searching the Internet for the necessary materials. But I came across either ready-made solutions (libraries) or very simple solutions that did not suit me. As a result, combining a bunch of articles and some notes, I made a tooltip that, when pointing, calculates the height of the table row, the length from the point of guidance to the end and shows a list of people. What are such difficulties for? Yes, simply because the number of people can be different and all need to be displayed without "hitting" each other, and the icon itself with the number of people (when pointing at which the tooltip is shown) can be in different places
The result is as follows:
I will not describe here the complete creation of the table, cells, etc., I will begin immediately with a tooltip.
The first is that we create a directive file and assign it a name: "tool-tip.directive.ts"
We start the creation of the directive:
import { Directive } from '@angular/core'; @Directive({ selector: '[tooltip]', }) export class ToolTipDirective { }
Each tooltip should appear when you hover the cursor and disappear when it "leaves" the element, which means you need to add lineeners
export class ToolTipDirective { @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { } @HostListener('mouseleave') hideTooltip() { } }
Add the variable "isClear" which will be responsible for displaying the tooltip, that is, if it has already been created, then we do not display it. I foresee the question: "Why?". The thing is that I am faced with a strange phenomenon, if the tooltip is created, and the element is quite large, so that you can move the mouse along it, then it starts to re-create, and is not always deleted. Very strange behavior, you want experiments - try to remove and see what happens.
export class ToolTipDirective { private isClear: boolean = true; @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { if (!this.isClear) { return; } } @HostListener('mouseleave') hideTooltip() { this.isClear = true; } }
In the folder with the directive, I created a subfolder into which I put all the components for the tooltip (in our application so far they are three different) called it "content".
Create a file with a class of options for the tooltip in the folder "content", I just called it "options.ts".
export class ContentOptions { x: number; y: number; height?: number; width?: number; content?: string; }
And import it into our file with the directive:
import { ContentOptions } from './content/options'; //
Next, we add a method that will calculate the frame for our tooltip and add a constructor, with the help of ElementRef we get access to the elements
import { Directive, ElementRef, HostListener, Input} from '@angular/core'; import { ContentOptions } from './content/options'; export class ToolTipDirective { @Input() public list: any[];// private isClear: boolean = true; constructor(private _ef: ElementRef) { } @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { if (!this.isClear) { return; } this.buildTooltip(event); // , } @HostListener('mouseleave') hideTooltip() { this.isClear = true; } private buildTooltip(event: any) { // let options: ContentOptions; let parent = this._ef.nativeElement.parentNode; // */. Angular Material, 'mat-row', , /* let matRow = this.findMatRowInClassList(parent.classList); if (!matRow) { do { parent = parent.parentNode; matRow = this.findMatRowInClassList(parent.classList); } while (!matRow); } const parentViewPort = parent.getBoundingClientRect(); // const cellViewPort = this._ef.nativeElement.getBoundingClientRect(); // , const rowHeight = parentViewPort.height; // const rightPoint = cellViewPort.right + 25; // let topPoint = parentViewPort.top; // let height = parentViewPort.height; // , const countPerson = this.list.length; // const width = parentViewPort.right - rightPoint; // const countInOneRow = Math.floor(width / 160); // 160 , 200 if (countInOneRow > 0) { // , , const countRow = Math.ceil(countPerson / countInOneRow); // "" if (this.list.length > countInOneRow) { // for (let i = 1; i <= countRow; i++) { if (i % 2 === 0) { topPoint -= rowHeight; } height = rowHeight * i; } } const options: ContentOptions = { // , x: rightPoint, y: topPoint, height: height, width: width } return options; } else { // , const leftEndPoint = cellViewPort.left - 25; const leftWidth = leftEndPoint - parentViewPort.left; const countInOneRowLeft = Math.floor(leftWidth / 160); if (countInOneRowLeft > 0) { const countRow = Math.ceil(countPerson / countInOneRowLeft); if (this.list.length > countInOneRowLeft) { for (let i = 1; i <= countRow; i++) { if (i % 2 === 0) { topPoint -= rowHeight; } height = rowHeight * i; } } const options: ContentOptions = { x: parentViewPort.left, y: topPoint, height: height, width: leftWidth, } return options; } } this.showTooltip(options); // ( ) } private findMatRowInClassList(classList: DOMTokenList): string { let matRow = undefined; if (classList.length > 0) { const index = classList.contains('mat-row'); if (index) { matRow = 'mat-row'; } } return matRow; } }
So that the code does not seem very long, I will divide it. I will not explain how the methods of creating elements work, because if you are a beginner, it will overload you with unnecessary information, especially since there are many sources where this can be found.
Next, we add a method for creating our tooltip, and accordingly make some changes to the constructor and import the necessary classes.
*/ /* import { Directive, Inject, ComponentFactoryResolver, Input, ElementRef, ViewContainerRef, ComponentRef, HostListener} from '@angular/core'; import { DOCUMENT } from '@angular/platform-browser'; import { ContentOptions } from './content/options'; export class ToolTipDirective { @Input() public list: any[];// private isClear: boolean = true; */ /* constructor(private _componentFactoryResolver: ComponentFactoryResolver, private _viewContainerRef: ViewContainerRef, private _ef: ElementRef, @Inject(DOCUMENT) private _document: any) { } @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { ..... } @HostListener('mouseleave') hideTooltip() { ..... } private buildTooltip(event: any) { ...... } private showTooltip(options: any) { let componentFactory: any; componentFactory = this._componentFactoryResolver.resolveComponentFactory(*/ /*); this.contentCmpRef = this._viewContainerRef.createComponent(componentFactory); // this._document.querySelector('body').appendChild(this.contentCmpRef.location.nativeElement); // , this.contentCmpRef.instance.options = options; this.contentCmpRef.instance.empolyees = this.list; this.isClear = false; // } private findMatRowInClassList(classList: DOMTokenList): string { .... } }
With the directive we can say finished, there will still be a couple of small changes.
Next, we proceed to create a component that will display our tooltip.
In the folder "content" create files: "tooltip-employees.component.ts" and "tooltip-employees.component.scss".
Let's start with "tooltip-employees.component.ts"
import { Component, AfterContentInit, ElementRef} from '@angular/core'; import { ContentOptions } from './options'; // @Component({ template : ` <div class="ng-tool-tip-content" [ngStyle]="{'width.px': options.width, 'height.px': options.height, 'top.px': options.y, 'left.px': options.x}"> <div *ngFor="let employee of empolyees" class="employee"> <div fxLayout="row" fxLayoutGap="1em"> <img mat-card-image [src]="employee.userPhoto" class="avatar"> <div fxLayout="column"> <div class="employee-name">{{employee.name}}</div> <div class="department-name"> {{employee.department.name}}</div> </div> <div> </div> </div> `, styleUrls : ['tooltip-employees.component.scss'] }) export class TooltipEmployeesComponent { public empolyees: any[]; private _options: ContentOptions; set options(op: ContentOptions) { if (op) { this._options = op; this.options.height -= 8; // add padding in css } } get options(): ContentOptions { return this._options; } constructor(private elRef: ElementRef) { } }
Next, add the file to the "tooltip-employees.component.scss":
$small-font-size: 12px; .ng-tool-tip-content{ z-index : 10; display: flex; flex-wrap: wrap; padding-top: 8px; background-color: #757575; position: absolute; .employee{ margin-left: 0.5em; margin-right: 0.5em; margin-bottom: 0.2em; width: 160px; .photo{ width: 40px; height: 40px; } .employee-name{ color: #FFFFFF; font-size: $small-font-size; } .department-name{ color:#C4C4C4; font-size: $small-font-size; } } }
Now everything is created for our component!
I have in the main file for table styles there is such a class:
" .mousehover-person-icon{ color: black; .mat-icon{ color: black; } } "
to change the color of our icon when you hover.
We return to the file with our directive:
import ...... */ /* import { TooltipEmployeesComponent } from './content/tooltip-employees.component'; export class ToolTipDirective { @Input() public list: any[];// */ /* /** set it to true, */ @Input() showOnClick: boolean = false; @Input() autoShowHide: boolean = true; private isClear: boolean = true; constructor(.......) { } */ /* @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { if (!this.autoShowHide || this.showOnClick) { return; } if (!this.isClear) { return; } this.iconElement = event.srcElement.parentElement.parentElement; this.iconElement.classList.add('mousehover-person-icon'); this.buildTooltip(event); } @HostListener('mouseleave') hideTooltip() { this.iconElement.classList.remove('mousehover-person-icon') // if (this.contentCmpRef) { this.contentCmpRef.destroy(); // this.isClear = true; } } private buildTooltip(event: any) { ...... } private showTooltip(options: any) { let componentFactory: any; */ /* componentFactory = this._componentFactoryResolver.resolveComponentFactory(TooltipEmployeesComponent); this.contentCmpRef = this._viewContainerRef.createComponent(componentFactory); // this._document.querySelector('body').appendChild(this.contentCmpRef.location.nativeElement); // , this.contentCmpRef.instance.options = options; this.contentCmpRef.instance.empolyees = this.list; this.isClear = false; // } private findMatRowInClassList(classList: DOMTokenList): string { .... } }
That's basically it! We created a directive and component for the tooltip.
Also, if you wish, you can add events to the directive that will inform you about the beginning of the creation of a tooltip and so on, but here I did not overload it with this information. You can also make many other components for the tooltip and reuse them, but this is also a complication, and if you first deal with this version, then adding components will not be difficult.
Well, the very use of our tooltip:
@Component({ template: ` <mat-icon class="mat-24" tooltip [list]="listItem">{{icon}}</mat-icon> ` export class IconComponent { public icon: string; public listItem: any[]; @Input() set data(cellData: any) { if (cellData) { if (cellData['icon']) { this.icon = (cellData['icon'] as string).toLowerCase(); } this.listItem = cellData['list']; } }; }
That's all! We have completed the creation of a tooltip!
How it looks with a lot of employees:
I hope this article was helpful to you! I would gladly point out the sources, from where I took some material, but unfortunately it was a long time ago that I had no links left!
Good luck in mastering dynamic tooltips!
Source: https://habr.com/ru/post/349212/
All Articles