Demo Application - Hosted on a Free dyno. (May take 10-20 seconds to wake up)
Github - Git repo for the hosted application.
Tools
- Ruby on Rails 6.0.2.2 - Ruby Web Framework
- StimulusJS - A minimal Javascript framework developed by Basecamp.
- Webpacker - Rails gem for using Webpack to bundle assets including Javascript modules.
- Tippy.js - Popup Javascript library.
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 thepacks
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 thecontent
option.- In order to allow for HTML rendering from Tippy.js we set the the
allowHTML
option totrue
.
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