DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,503 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Stephane Paquet
Stephane Paquet

Posted on

Rails, Popper, Tailwind & Stimulus

Popper is a javascript positioning engine to speed up the development of popovers and tooltips.

More information about it can be found here

Project setup:

  • Rails 7
  • Stimulus 2
  • esbuild / jsbuild
  • Tailwind CSS

rails new project_name --css=tailwind --javascript=esbuild

Step 1: add popper to the project

yarn add @popperjs/core
Enter fullscreen mode Exit fullscreen mode

Step2: create a stimulus controller

rails g stimulus popper
Enter fullscreen mode Exit fullscreen mode

Open the popper_controller.js and perform the following edits:

Add at the top of the file:

import { createPopper } from "@popperjs/core";
Enter fullscreen mode Exit fullscreen mode

Before creating an instance of popper in the controller, let's add "target" and "values" to make this controller more reusable.

Image description

Popper instance is instantiated in the connect() method

connect() {
    // Create a new Popper instance
    this.popperInstance = createPopper(this.elementTarget, this.tooltipTarget, {
      placement: this.placementValue,
      modifiers: [
        {
          name: "offset",
          options: {
            offset: this.offsetValue,
          },
        },
      ],
    });
  }
Enter fullscreen mode Exit fullscreen mode

Note that the event listeners are not added to the target or element from within the stimulus controller. As written in Better Stimulus and the official stimulus documentation, event management should be handled by the Stimulus framework. We will attached the event management to the element using the data-action tag as explained below.

Let's create the show and hide methods as well as the disconnect one that us used to remove the popper instance.

show(event) {
    this.tooltipTarget.setAttribute("data-show", "");

    // We need to tell Popper to update the tooltip position
    // after we show the tooltip, otherwise it will be incorrect
    this.popperInstance.update();
  }

  hide(event) {
    this.tooltipTarget.removeAttribute("data-show");
  }

  // Destroy the Popper instance
  disconnect() {
    if (this.popperInstance) {
      this.popperInstance.destroy();
    }
  }
Enter fullscreen mode Exit fullscreen mode

So far we are pretty much sticking to popper's documentation. The main difference is that the event listeners are not attached to the element programmatically within the stimulus controller.

At this point, the popper_controller.js file should look like this:

import { Controller } from "@hotwired/stimulus";
import { createPopper } from "@popperjs/core";

// Connects to data-controller="popper"
export default class extends Controller {
  static targets = ["element", "tooltip"];
  static values = {
    placement: { type: String, default: "top" },
    offset: { type: Array, default: [0, 8] },
  };

  connect() {
    // Create a new Popper instance
    this.popperInstance = createPopper(this.elementTarget, this.tooltipTarget, {
      placement: this.placementValue,
      modifiers: [
        {
          name: "offset",
          options: {
            offset: this.offsetValue,
          },
        },
      ],
    });
  }

  show(event) {
    this.tooltipTarget.setAttribute("data-show", "");

    // We need to tell Popper to update the tooltip position
    // after we show the tooltip, otherwise it will be incorrect
    this.popperInstance.update();
  }

  hide(event) {
    this.tooltipTarget.removeAttribute("data-show");
  }

  // Destroy the Popper instance
  disconnect() {
    if (this.popperInstance) {
      this.popperInstance.destroy();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Let's get stylish!

We can use the one provided by the popper team as an example on their website

#tooltip {
  background: #333;
  color: white;
  font-weight: bold;
  padding: 4px 8px;
  font-size: 13px;
  border-radius: 4px;
  display: none;
}
#arrow,
#arrow::before {
  position: absolute;
  width: 8px;
  height: 8px;
  background: inherit;
}

#arrow {
  visibility: hidden;
}

#arrow::before {
  visibility: visible;
  content: "";
  transform: rotate(45deg);
}
#tooltip[data-popper-placement^="top"] > #arrow {
  bottom: -4px;
}

#tooltip[data-popper-placement^="bottom"] > #arrow {
  top: -4px;
}

#tooltip[data-popper-placement^="left"] > #arrow {
  right: -4px;
}

#tooltip[data-popper-placement^="right"] > #arrow {
  left: -4px;
}

#tooltip[data-show] {
  display: block;
}
Enter fullscreen mode Exit fullscreen mode

Feel free to use tailwind CSS styles and animation or any other CSS trick that is required or will make your popover/tooltip look better.

Saved it in app/assets/stylesheets/popper.css and import it at the top of app/assets/stylesheets/application.tailwind.css

@import "popper.css";

/* Tailwind CSS */
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

It's now time to work on the frontend of the project!

Step 4: Let's create a button

<div data-controller="popper">
   <button id="button" data-popper-target="element" data-action="mouseenter->popper#show mouseleave->popper#hide" class="bg-blue-500 text-blue-100 px-3 py-2 rounded-xl">
      Click me
      <div id="tooltip" role="tooltip" data-popper-target="tooltip">
         My tooltip
         <div id="arrow" data-popper-arrow></div>
      </div>
   </button>
</div>
Enter fullscreen mode Exit fullscreen mode

As stated above, the event listeners are added using the data-action parameter. in this case we direct the mouseenter event, which is triggered when the mouse is over the button, to the show method defined in the controller. When the mouse is no longer over the button element, mouseleave is triggered and the hide method is called to hide the tooltip.

You can add more actions or adapt to your needs. For example, you can have data-action="click->popper#show" to open a popover when a user click on a certain element.

Step 5: Enjoy

Image description

Top comments (1)

Collapse
 
ffscalco profile image
Fabiano F. Scalco

That's great @spaquet ! Thanks for sharing this, it's very helpful!
One question tho, if I want to change the position of the tooltip, how can I do that? I saw that we have the css to show the arrow in different places, but I could not figure out where to set the position.

🌚 Life is too short to browse without dark mode