Use Popper.js in Angular projects
TL;DR Follow to the repository, clone it and explore
popper.directive.ts
andapp.component.html
Prerequisites: you should have basic knowledge of JavaScript, HTML, CSS, Angular 2+ and RxJs.
Topics to be discussed:
- Popper.js installation
- Make Popper.js work
- Style created tooltip
- Misleads I’ve encountered personally
I will start with 4th item in the list.
The most important thing to realise when starting using Popper.js at first is that it does not apply any styles and effects. Meaning it will only position your element and not set it any styles or show element only on hover/click etc.
What it does is simply positioning your element and tracking its placement. You can add some features though.
So let’s start.
Firstly we create a new Angular app (I will use Angular 8 with SCSS preprocessor)
ng new popper-app
Wait for the dependencies install and open the new project in your favourite code editor or IDE.
Then we need to install Popper.js
npm i popper.js
After Popper.js was added to the project we create a new directive.
ng g d popper
Then we navigate to the created directive.
First we import Popper.js
import Popper from 'popper.js';
We specify some inputs to work with
// The hint to display
@Input() target: HTMLElement;
// Its positioning (check docs for available options)
@Input() placement?: string;
// Optional hint target if you desire using other element than
// specified one
@Input() appPopper?: HTMLElement;
Now we need to implement OnInit
and OnDestroy
life cycles to create and dispose our tooltip and few properties to keep data.
// The popper instance
private popper: Popper;
private readonly defaultConfig: PopperOptions = {
placement: ‘top’,
removeOnDestroy: true
};
constructor(private readonly el: ElementRef) {}
ngOnInit(): void {
// An element to position the hint relative to
const reference = this.appPopper ? this.appPopper : this.el.nativeElement;
this.popper = new Popper(reference, this.target, this.defaultConfig);
}
ngOnDestroy(): void {
if (!this.popper) {
return;
}
this.popper.destroy();
}
Here we create and dispose popper following directive’s life cycle to avoid memory leaks.
Now it’s time to create a markup for popper and try it out.
For that we navigate to the app.component.html
and paste following (you are free to replace the default boilerplate with content)
<ng-container [appPopper]="target" [target]="tooltip"></ng-container>
<button #target>This is a long button but it’s worth your hovering</button>
<span #tooltip>Hooray!</span>
Then you need to run ng serve
in a command line and open http://localhost:4200/
Now you should see next result
Here we witness the work of Popper.js. The hint is being centred relative to the hint target.
You may notice that I’ve specified placement: "top"
whilst the hint is at the bottom. It’s the expected behaviour, because Popper.js moves an element to another side if it has no space (the button is at the very top and there is no space to show the hint at the top so it was moved to the bottom). You can configure this behaviour if you don’t want this behaviour (feel free to explore the docs for available options).
This is time for styling!
These are the styles.scss
(global styles)
body {
height: 200vh;
}
button {
margin-top: 100px;
}
.popper {
display: inline-block;
background: #eee;
border-radius: 3px;
padding: 10px;
&[x-placement^="bottom"] {
margin-top: 15px;
}
&[x-placement^="top"] {
margin-bottom: 15px;
}
&__arrow {
width: 0;
height: 0;
padding: 0;
margin: 0;
position: absolute;
border: {
style: solid;
width: 10px;
color: transparent;
}
}
&[x-placement^="top"] &__arrow {
border-bottom-width: 0;
border-top-color: #eee;
bottom: -10px;
}
&[x-placement^="bottom"] &__arrow {
border-top-width: 0;
border-bottom-color: #eee;
top: -10px;
}
}
This is app.component.html
now
<ng-container [appPopper]="target" [target]="tooltip"></ng-container>
<button #target>This is a long button but it's worth your hovering</button>
<span class="popper" #tooltip>
<span class="popper__arrow"></span>
Hooray!
</span>
And popper.directive.ts
(the changed part)
private readonly defaultConfig: PopperOptions = {
placement: "top",
removeOnDestroy: true,
modifiers: {
arrow: {
element: ".popper__arrow"
}
}
};
Here in arrow.element
I specify the selector to use for arrow (it has to be inline/inline-block element).
Now it’s time to save the changes and check the page after build.
Now you’ve done a great job and created your own hint directive which can be applied to elements.
Note: you don’t have to use ng-container
or any other extra element for it to work. I used this case to show that it doesn’t matter where to apply the directive.
You can do a markup even that simple
<button appPopper [target]="tooltip">This is a long button but it's worth your hovering</button>
<span class="popper" #tooltip>
<span class="popper__arrow"></span>
Hooray!
</span>
Check the result :)
Now the most interesting thing comes to us — showing the hint on hover.
Now you need to inject Renderer2
in constructor in order to apply styles/css in better and safer syntax than vanilla JS classList.add
.
constructor(
...
private readonly renderer: Renderer2) {}
Add a method to directive
private mouseHoverHandler(e: string): void {
if (e === "mouseenter") {
this.renderer.removeStyle(this.target, "display");
this.popper.enableEventListeners();
this.popper.scheduleUpdate();
} else {
this.renderer.setStyle(this.target, "display", "none");
this.popper.disableEventListeners();
}
}
And call it from ngOnInit
ngOnInit() {
...
this.renderer.setStyle(this.target, "display", "none");
merge(
fromEvent(reference, "mouseenter"),
fromEvent(reference, "mouseleave")
).pipe(
filter(() => this.popper != null),
pluck("type")
).subscribe((e: any) => this.mouseHoverHandler(e));
}
Now after you save you should see a hint only when hover the button.
For anyone who needs to see the code here is the repo