A Page screen shot of a working Popup component from the following tutorial

Popup with Tippy.js, Ruby on Rails 6, Stimulus and Webpacker

October 30, 2020 RSS Feed Only Files Quick View

Demo Application - Hosted on a Free dyno. (May take 10-20 seconds to wake up)

Github - Git repo for the hosted application.

Tools

Set Up

This tutorial assumes that you have a Rails application with webpacker and stimulus installed (along with some familiarity using each).

If you are using Rails >= 6.0 version, you can generate the application with the following command.

rails new sample_app --webpacker=stimulus

The model we will be working with from the demo is Fabric.

We have a typical index listing of the Fabric in the system:

app/views/fabrics/_fabric.html.haml

%li
  .avatar
    = img_pack_tag(fabric.image, class: "circular")
  .data
    = fabric.name

However, the avatar is too small for comparing fabrics, and it would be nice to have the image enlarged when hovering over it.

We will need to install a popup library to accomplish the desired feature.

Install with Yarn & Webpacker

Add the Tippy.js library using yarn

Command Line

yarn add tippy.js

Afterwords, create a scss file to load the module’s styles:

app/javascript/stylesheets/popup.scss

@import 'tippy.js/dist/tippy.css';

.popup {
  padding: 7px;

  .image {
    text-align: center;
    margin-bottom: 10px;

    img {
      height: 200px;
      width: 200px;
      border-radius: 5px;
    }
  }
}

NOTE: If you do not use a css style path such as app/javascripts/stylesheets/ in your build, you can load the the Tippy.js library styles in the packs file:

app/javascripts/packs/application.js

import 'tippy.js/dist/tippy.css';

Stimulus Integration

app/javascript/controllers/popup_controller.js

import { Controller } from "stimulus";
import tippy from 'tippy.js';
import "../stylesheets/popup.scss";

export default class extends Controller {
  static targets = ["trigger", "content"]

  initialize() {
    this.initPopup();

    this.contentTarget.style.display = "none";
  }

  initPopup() {
    tippy(this.triggerTarget, {
      content: this.contentTarget.innerHTML,
      allowHTML: true,
    });
  }
}

We load the tippy module at the top of the stimulus controller file along with the stylesheet:

NOTE: Do not load the stylesheet if already imported in app/javascript/packs/application.js

import tippy from 'tippy.js';
import "../stylesheets/popup.scss";

We have two stimulus targets:

static targets = ["trigger", "content"]
  • this.triggerTarget

    This is the HTML element we will be passing into the tippy() function. It will turn the element into a trigger for the popup component.

  • this.contentTarget

    This is the HTML content that the popup will contain.

We use the initialize() stimulus life-cycle function:

initialize() {
  this.initPopup();

  this.contentTarget.style.display = "none";
}

In the initPopup() method, we set this.popup to the return call of tippy() in order to access the popup:

initPopup() {
  this.popup = tippy(this.triggerTarget, {
    content: this.contentTarget.innerHTML,
    allowHTML: true,
  });
}

The first argument passed into the tippy() function is the element that will toggle the popup. In this case our this.triggerTarget.

The second argument is a options object.

  • this.contentTarget.innerHTML is set as the content option.
  • In order to allow for HTML rendering from Tippy.js we set the the allowHTML option to true.

Additional options can be found at Tippy.js README.

To prevent from the content from rendering outside the popup we hide it:

this.contentTarget.style.display = "none";

HAML View Changes

Now we have to update our fabric partial to utilize the popup controller:

app/views/fabrics/_fabric.html.haml

%li
  .avatar{ data: { controller: :popup, target: "popup.trigger" } }
    = image_pack_tag(fabric.image, class: "circular")

    %div{ data: { target: "popup.content" } }
      .popup
        .image
          = image_pack_tag(fabric.image)
        .data
          Created On:
          = display_date fabric.created_at

  .data
    = fabric.name.titleize

Looks Good!

Now lets do some refactoring to clean up our view and make the popup controller more modular.

app/javascript/controllers/popup_controller.js

import { Controller } from "stimulus";
import tippy from 'tippy.js';
import "../stylesheets/popup.scss";

export default class extends Controller {
  static targets = ["trigger", "content"]

  initialize() {
    this.trigger = this.getTrigger();
    this.content = this.getContent();

    this.initPopup();

    this.content.style.display = "none";
  }

  initPopup() {
    this.popup = tippy(this.trigger, {
      content: this.content.innerHTML,
      allowHTML: true,
    });
  }

  getContent() {
    if (this.hasContentTarget) {
      return this.contentTarget;
    } else {
      var content = document.createElement('div')
      content.innerHTML = this.data.get("content");

      return content
    }
  }

  getTrigger() {
    if (this.hasTriggerTarget) {
      return this.triggerTarget;
    } else {
      return this.element;
    }
  }
}

app/views/fabrics/_fabric.html.haml

%li
  .avatar{ data: { controller: :popup,
                   popup: { content: render("popup", fabric: fabric) } } }

    = image_pack_tag(fabric.image, class: "circular")

  .data
    = fabric.name.titleize

app/views/fabrics/_popup.html.haml

.popup
  .image
    = image_pack_tag(fabric.image)
  .data
    Created On:
    = display_date fabric.created_at