DEV Community

julianrubisch
julianrubisch

Posted on • Originally published at blog.minthesize.com on

Tooltips with Futurism

As I read the excellent post about how to implement lazy loaded tooltips with Hotwire by Sean P. Doyle and Steve Polito, I couldn’t help but think that this is actually a very distinct example of how hard it is to draw the line between an all-in REST approach (i.e., controllers and routes for allthethings) and a more nuanced take on things that tries to balance REST and RPC ways of implementing Reactive Rails.

So I decided to write up a quick reply using futurism, the lazy loading solution of the CableReady ecosystem, just to serve as an example of an alternative which maybe aligns better with existing legacy codebases (because it relies on plain Rails partials - that might even already exist in your application!).

Let’s get started!

1. Install futurism

First, let’s install futurism. Sean’s repository uses importmaps, so we’ll have to do a bit of manual installation in app/javascript/controllers/index.js as long as the Futurism installer hasn’t been fully ported.

$ bundle add futurism -v 1.2.0.pre9
$ bin/importmap pin @stimulus_reflex/futurism@1.2.0-pre9


// app/javascript/controllers/index.js
import { Application } from "@hotwired/stimulus";

// NEW: import cable from turbo-rails and futurism
import { cable } from "@hotwired/turbo-rails";
import * as Futurism from "@stimulus_reflex/futurism";

const application = Application.start();

// Configure Stimulus development experience
application.debug = false;
window.Stimulus = application;

export { application };

// NEW: initialize futurism
const consumer = await cable.getConsumer();

Futurism.initializeElements();
Futurism.createSubscription(consumer);

Enter fullscreen mode Exit fullscreen mode

Essentially, this initializes Futurism with the default ActionCable consumer, and intializes the custom elements used under the hood.

2. Implement Lazy Loading

Sean and Steve use a Turbo frame in the show view, as well as in the user partial to load the tooltip. To quote the original article/source code:

<!-- app/views/tooltips/show.html.erb -->
<turbo-frame id="<%= params.fetch :turbo_frame, dom_id(@user) %>" target="_top">
 <div class="relative">
 <div class="flex gap-2 items-center p-1 bg-black rounded-md text-white">
<%= render partial: "users/user", object: @user, formats: :svg %>
 <strong>Name:</strong>
<%= link_to @user.name, @user, class: "text-white" %>
 </div>
 <div class="h-2 w-2 bg-black rotate-45 -top-1 -left-2 ml-[50%] relative"></div>
 </div>
</turbo-frame>

<!-- app/views/users/_user.html.erb -->
<turbo-frame id="<%= dom_id user, :tooltip %>" target="_top" role="tooltip"
 src="<%= user_tooltip_path(user, turbo_frame: dom_id(user, :tooltip)) %>"
 class="hidden absolute translate-y-[-150%] z-10
 peer-hover:block peer-focus:block hover:block focus-within:block"
>

Enter fullscreen mode Exit fullscreen mode

Now, among other things, this leads to a bit of confusion for the first-time reader regarding the addressing of the frame: To have Turbo swap out the correct frame, we need to pass the identifier as a parameter down to the TooltipsController using user_tooltip_path(user, turbo_frame: dom_id(user, :tooltip)).

Using futurism, I’m going to take a slightly different approach. First I’m going to move the contents from the tooltips show.html.erb to a _tooltip.html.erb partial (note that the content can stay exactly the same, we’re just dumping the surrounding <turbo-frame>):

<!-- app/views/tooltips/_tooltip.html.erb -->
<div class="relative">
 <div class="flex gap-2 items-center p-1 bg-black rounded-md text-white">
<%= render partial: "users/user", object: user, formats: :svg %>
 <strong>Name:</strong>
<%= link_to user.name, user, class: "text-white" %>
 </div>
 <div class="h-2 w-2 bg-black rotate-45 -top-1 -left-2 ml-[50%] relative"></div>
</div>

Enter fullscreen mode Exit fullscreen mode

In _user.html.erb, I exchange the Turbo frame for a futurize call and restructure the markup a bit (to make the peer-hover work, I wrapped it in an enclosing <span>).

All you have to know at this moment is that it obeys the API of render partial: and you can pass a placeholder to the block (which I omitted here):

<!-- app/views/users/_user.html.erb -->
<div id="<%= dom_id user %>" class="scaffold_record">
 <p>
 <strong>name:</strong>
<%= user.name %>
 </p>

 <p class="relative">
<%= link_to "show this user", user, class: "peer", aria: { describedby: dom_id(user, :tooltip) } %>
 <span class="hidden absolute translate-y-[-150%] z-10 peer-hover:block peer-focus:block hover:block focus-within:block">
<%= futurize partial: "tooltips/tooltip",
                   locals: {user: user},
                   extends: :div do %>
<% end %>
 </span>
 </p>
</div>

Enter fullscreen mode Exit fullscreen mode

The result is an equivalent functionality, albeit with a full RESTful route including all the boilerplate less. This time, we’re using a <futurism-element> to communicate with ActionCable and CableReady does the heavy lifting in the background. Observe: ../assets/images/posts/2022/2022-01-31-tooltips-futurism.gif

3. Conclusion

I hope you enjoyed this comparison. It’s important to me to spell out that there’s no right or wrong here. I wrote futurism prior to Turbo frames even being a thing, and find myself using the latter in a majority of cases these days. But sometimes, a less intrusive approach is preferable, especially when dealing with legacy codebases, or really small DOM fragments, where a full-fledged ActionDispatch route would be an overdo.

The interested reader might want to take a look at the futurism README for further information on the library.

Discussion (0)