Rails, Popper, Tailwind & Stimulus
Tags: rails, stimulus, javascript, tailwindcss
Published: 2022-01-17 12:11:37

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

More information about it can be found [here](https://popper.js.org)

**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
```

# Step2: create a stimulus controller
```
rails g stimulus popper
```
Open the `popper_controller.js` and perform the following edits:

Add at the top of the file:
```javascript
import { createPopper } from "@popperjs/core";
```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ytqq1pvdxxit6ihzhk3.png)

Popper instance is instantiated in the `connect()` method 
```javascript
connect() {
    // Create a new Popper instance
    this.popperInstance = createPopper(this.elementTarget, this.tooltipTarget, {
      placement: this.placementValue,
      modifiers: [
        {
          name: "offset",
          options: {
            offset: this.offsetValue,
          },
        },
      ],
    });
  }
```
Note that the event listeners are not added to the target or element from within the stimulus controller. As written in [Better Stimulus](https://www.betterstimulus.com) 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.

```javascript
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();
    }
  }
```
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:
```javascript
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();
    }
  }
}
```
# Step 3: Let's get stylish!
We can use the one provided by the popper team as an example on their website

```css
#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;
}
```
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`

```css
@import "popper.css";

/* Tailwind CSS */
@tailwind base;
@tailwind components;
@tailwind utilities;
```
It's now time to work on the frontend of the project!

# Step 4: Let's create a button
```html
<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>
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q7wossn29907vmiz6vsh.png)