Building GitHub-style Hovercards with StimulusJS and HTML-over-the-wire
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.
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”.
Turbolinks / Stimulus 20XX: The Future
The stack in 2014-2016 was:
- Rails UJS +
- 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 (replacing
js.erband 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.
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!
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
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
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
# 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.
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.
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
@applydirective 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
- 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.