Building GitHub-style Hovercards with StimulusJS and HTML-over-the-wire
Somewhere along the way toward our current JavaScript hellscape, programmers decided that HTML was over. We’re done with it.
The emergence of tools like React shifted programmers away from writing HTML, instead writing JSX, a fancier tag-based markup language that worked nicely inside your JavaScript.
Backends were then relegated to being dumb JSON API endpoints. Or if you were fancy and chasing upvotes, you’d use GraphQL!
But HTML? Yuck!
A Brief History of HTML-over-the-wire
One of the key pillars of Rails is to “Value integrated systems”. While the industry moves towards microservices, highly decoupled front-ends and teams, and the siren song of Programming via LEGO Bricks, Rails leans into one system that does it all – termed the Majestic Monolith.
Instead of rebuilding much of what already works in Rails in a client-side JavaScript MVC framework, apps like Basecamp, GitHub, and Shopify are able to achieve snappy page loads using the concept of “HTML-over-the-wire”.
In his seminal RailsConf 2016 talk, Sam Stephenson walks through the pieces of this stack.
By using Turbolinks (or similar libraries like pjax or Inertia) and fast HTML responses (aided by caching and avoiding excessive database queries to get sub-100ms response times), you could build high performance pages, while still hanging on to the understated benefits of stateless HTTP responses and server-side logic.
As Sam points out, it was truly a “Golden Age of Web Development”.
So while much of the industry went down the JavaScript rabbit hole – creating new innovations for reactive rendering, functional state management containers, and approximately seventy different client-side routing libraries – the quiet rebellion in Rails-land was honing these techniques and plugging along building apps out of boring server-rendered HTML.
We’re seeing a renaissance of these tools in 2020 and the excitement (at least in a small corner of the Twitter!) is reaching a fever pitch as Basecamp launches HEY: a fully-featured email client with a tiny JavaScript footprint that pushed the boundaries of the HTML-over-the-wire approach.
Turbolinks / Stimulus 20XX: The Future
The stack in 2014-2016 was:
- Turbolinks/pjax
- Rails UJS +
js.erb
templates (Server-generated JavaScript Responses) - Heavy HTML fragment caching
- Rails Asset Pipeline and CoffeeScript
You can even trace the origin of these techniques back even further. I was recently sent a link to a nearly 15 year old REST “microformat” called “AHAH: Asynchronous HTML and HTTP”, which is an early version of the same ideas we’re so excited about today. (You shouldn’t be surprised to see David Hansson listed as a contributor!)
Now a “state-of-the-art” 2020 version also includes:
- StimulusJS (see also AlpineJS) for lightweight event management, data binding, and “sprinkles” of behavior
- Partial updates with Turbolinks via a new
<template>
command approach (replacingjs.erb
and supporting CSP) - Real-time Turbolinks updates via ActionCable (see also StimulusReflex/CableReady)
- First-party support for Webpack, ES6, and new CSS approaches like Tailwind and PurgeCSS
This stack is extremely powerful and the development experience allows you to really fly. You can build fast and interactive applications with a small team, all while still experiencing the joy of a 2014-era vanilla Rails codebase.
But years of a JavaScript SPA-heavy monoculture have made it hard to learn about this stack. The community is filled with practitioners, using the tools to build software and businesses. There simply has not been the same level of content produced and so many of these tools are unknown and can be unapproachable.
One of the ways I can contribute is to light the way for those want to know more by showing some real-world examples (not a TODO list or a Counter). Once you see how you can use tools like Stimulus and HTML responses to build features where you might instead reach for a tool like React, things will start to click.
Let’s Build Something Real: Hovercards
Hovercards show extra contextual information in a popup bubble when you hover over something in your app. You can see examples of this UI pattern on GitHub, Twitter, and even Wikipedia.
This feature is really easy to build with Rails using an HTML-over-the-wire approach.
Here’s the plan:
- Build a controller action to render the hovercard as HTML
- Write a tiny Stimulus controller to fetch the hovercard HTML when you hover
…and that’s it.
We don’t need to make API endpoints and figure out how to structure all of the data we need. We don’t need to reach for React or Vue to make this a client-side component.
The beauty of this boring Rails approach is that the feature is dead-simple and it’s equally straightforward to build. It’s easy to reason about the code and super extensible.
For this example, let’s build the event feed for a sneaker marketplace app.
When you hover over a shoe, you see a picture, the name, the price, etc. Same for the user, you can see a mini-profile for each user.
The Frontend (Stimulus + fetch)
The markup for the link looks like:
<!-- app/views/shoes/feed.html.erb -->
<div
class="inline-block"
data-controller="hovercard"
data-hovercard-url-value="<%= hovercard_shoe_path(shoe) %>"
data-action="mouseenter->hovercard#show mouseleave->hovercard#hide"
>
<%= link_to shoe.name, shoe, class: "branded-link" %>
</div>
Note: we are using the APIs from the Stimulus 2.0 preview release!
One of the great features of Stimulus is that you can read the markup and understand what’s happening without diving into the JavaScript.
Without knowing anything else about the implementation, you could guess how it’s going to work: this link is wrapped in a hovercard
controller, when you hover (via mouseenter
and mouseleave
events) the card is shown or hidden.
As recommended in Writing Better Stimulus Controllers, you should pass in the URL for the hover card endpoint as a data property so that we can re-use the hovercard_controller
for multiple types of cards. This also keeps us from having to duplicate the application routes in JavaScript.
// app/javascript/controllers/hovercard_controller.js
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ["card"];
static values = { url: String };
show() {
if (this.hasCardTarget) {
this.cardTarget.classList.remove("hidden");
} else {
fetch(this.urlValue)
.then((r) => r.text())
.then((html) => {
const fragment = document
.createRange()
.createContextualFragment(html);
this.element.appendChild(fragment);
});
}
}
hide() {
if (this.hasCardTarget) {
this.cardTarget.classList.add("hidden");
}
}
disconnect() {
if (this.hasCardTarget) {
this.cardTarget.remove();
}
}
}
This is all of the JavaScript we’re going to be writing for this feature: it’s only ~30 lines and we can use this for any other hovercards in the app. There isn’t really anything app specific about this controller either, you could pull it into a separate module and re-use it across projects. It’s totally generic.
The controller uses the fetch
API to call the provided Rails endpoint, gets some HTML back, and then inserts it into the DOM. As a small improvement, we use the Stimulus target
API for data binding to save a reference to the card so that subsequent hovers over this link can simply show/hide the markup without making another network request.
We also choose to remove the card when leaving the page (via the disconnect
lifecycle method), but you could also opt to hide the card instead depending on how you want caching to work.
The Backend (Rails + Server rendered HTML)
There is nothing magic on the frontend and it’s the same story on the backend.
# config/routes.rb
Rails.application.routes.draw do
resources :shoes do
member do
get :hovercard
end
end
end
Setup a route for /shoes/:id/hovercard
# app/controllers/shoes_controller.rb
class ShoesController < ApplicationController
...
def hovercard
@shoe = Shoe.find(params[:id])
render layout: false
end
end
Write a basic controller action, the only difference being that we set layout: false
so that we do not use the global application layout for this endpoint.
You can even visit this path directly in your browser to quickly iterate on the content and design. The workflow gets even better when using a utility-based styling approach like Tailwind since you don’t even need to wait for your asset bundles to rebuild!
<!-- app/views/shoes/hovercard.html.erb -->
<div class="relative" data-hovercard-target="card">
<div data-tooltip-arrow class="absolute bottom-8 left-0 z-50 bg-white shadow-lg rounded-lg p-2 min-w-max-content">
<div class="flex space-x-3 items-center w-64">
<%= image_tag @shoe.image_url, class: "flex-shrink-0 h-24 w-24 object-cover border border-gray-200 bg-gray-100 rounded", alt: @shoe.name %>
<div class="flex flex-col">
<span class="text-sm leading-5 font-medium text-indigo-600">
<%= @shoe.brand %>
</span>
<span class="text-lg leading-0 font-semibold text-gray-900">
<%= @shoe.name %>
</span>
<span class="flex text-sm text-gray-500">
<%= @shoe.colorway %>
<span class="mx-1">
·
</span>
<%= number_to_currency(@shoe.price.to_f / 100) %>
</span>
</div>
</div>
</div>
</div>
The hovercard is built with a server-rended ERB template, same as any other page in the Rails app. We set the data-hovercard-target
as a convenience to bind to this element back in the Stimulus controller.
Finishing Touches
The data-tooltip-arrow
allows us to add a little triangle to the bubble with a bit of CSS. You can add a library like Popper if you have more advanced needs, but this single CSS rule works great and doesn’t require any external dependencies.
/* app/javascript/stylesheets/application.css */
[data-tooltip-arrow]::after {
content: " ";
position: absolute;
top: 100%;
left: 1rem;
border-width: 2rem;
border-color: white transparent transparent transparent;
}
And voila! We’ve built hovercards!
If we want to add a hovercard to another model type in our application (like User profiles), it almost feels like cheating. We can use the same Stimulus controller. All we need to do is add User specific template.
<!-- app/views/users/hovercard.html.erb -->
<div class="relative" data-hovercard-target="card">
<div data-tooltip-arrow class="absolute bottom-8 left-0 z-50 bg-white shadow-lg rounded-lg p-2 min-w-max-content">
<div class="flex space-x-3 items-center p-1">
<%= image_tag @user.gravatar_url, class: "flex-shrink-0 h-16 w-16 object-cover bg-gray-100 rounded inset shadow-inner", alt: @user.name %>
<div class="flex-1 flex flex-col">
<span class="font-bold text-lg"><%= @user.name %></span>
<div class="flex space-x-1 items-center text-sm">
<svg class="text-orange-400 fill-current h-4 w-4" viewBox="0 0 20 20">...</svg>
<span class="text-gray-500 italic"><%= @user.bio %></span>
</div>
<span class="text-gray-400 text-xs mt-1">
Kickin' it since <%= @user.created_at.year %>
</span>
</div>
</div>
</div>
</div>
Taking it to the next level
If you want to expand this feature even further, there are a few ideas you might consider:
- Removing some duplication in the hovercard templates by either: extracting a Rails
partial
, using a gem like github/view_component, or using the Tailwind@apply
directive to create components in your stylesheets - Animating the hovercard using CSS transitions to fade in and out
- Adding a delay or fancy “directional aiming” (like the Amazon mega dropdown) so that you can move your mouse more easily to the hovercard
- Cancel a pending AJAX request if you move away using the
AbortController
for thefetch
API - Explore caching the hovercards (assuming the data is not specific to a user or session) in Rails with Fragment Caching
Wrap it up
This stack is a love letter to the web. Use links and forms. Render HTML. Keep your state on the server and in the database. Let the browser handle navigation. Add sprinkles of interactivity to improve the experience. For many it feels like a step backward, but in my opinion it’s going back to the way things should be.
It’s natural to be skeptical, especially in the current climate of “JS all the things”. But you really have to give these tools a try before you really get it. Once you see that the classic ways of building software can still get the job done, it’s hard to go back to debugging node_modules
conflicts or rebuilding HTML forms inside of this years framework du jour.
In this year’s RailsConf remote keynote, DHH talked about the cyclical pendulum of Hegel’s dialectics that happens in software. New ideas are recycled and rediscovered every few years and now is a great time to hop along for the ride.
Was this article valuable? Subscribe to the low-volume, high-signal newsletter. No spam. All killer, no filler.