DEV Community

PG
PG

Posted on

Coupdoeil Ruby gem

I have released a new Ruby (on Rails) gem: coupdoeil!

TLDR: This gem makes it easy to add powerful interactive popovers to your application, with good performances and modularity in mind. Be it a card to display details about a resource, interactive or not, or even a menu or a form.

Let me take a quick look at that

I'm a big fan of Wikipedia popups when you hover over a link to another article. I find this feature so useful to grasp the essence of a resource without having to navigate to another view and losing the current context (or creating a hidden todo by opening it in another tab!). To me, it's the web equivalent of the bottom page note in a book. It takes just a short moment to look at it without losing focus on what you're actually reading. With the glimpse of an eye, you get a bonus info to better apprehend what you are looking at.

Wikipedia popover example

Such popovers are also used on GitHub when hovering the link to a repository, a user, a pull request, an issue, etc. But they are even better! Given where you open it, you get additional details about the current viewing context. For example, if you hover over a user's avatar from within a repo they contributed to, you'll get a bit more detail. Like the last time they committed to this repo, or whether they're a member of the organization that owns this repo. Same thing for a pull request, you'll get to know if you're required to review it if it is pending.

Github popover example

What I also like about GitHub popovers (which they call hovercards since they only open on mouseover) is that they allow interactions, like quickly starring a repo or following a user. You can also encounter this kind of ux on many other apps; like Discord: when clicking on a user's avatar, you get to see details about them, but you can also send a short message from this popup; or Bluesky: hovering a user's name will display details and a "follow" button.

All that to say that I'm really found of this type of interaction, and I'd like it to be more common (as long as it does not reduce accessibility!).

Turns out, I'm a Ruby on Rails developer, and therefore I can help my fellow rails developers to easily implement such popovers! That for, I created Coupdoeil, a gem that makes it fairly easy.

Discover Coupdoeil

Let's take a quick look at how it works and how easy it is to add popovers to your application with this gem.

I'll show you two examples to help you understand the benefits of coupdoeil.

Let's take an imaginary app with managers that can be attached to projects. Both projects and managers can be viewed in their respective indices, arranged in tables. The first popover will allow a quick view of a manager details from a projects index, and the second one will contain a form to add a manager from a managers index. I'll use DaisyUI and TailwindCSS to quickly style these examples

First things first, installation:

bundle add "coupdoeil" bundle install bundle generate coupdoeil:install 
Enter fullscreen mode Exit fullscreen mode

This generator is going to make the following changes:

  • it creates the folder app/popovers and app/popovers/application_popover.rb, which will contain the base class for popovers.
  • it creates the folder app/popovers/layout and puts in it popover.html.erb which will be the default layout for popovers.
  • then it checks how you have plugged JavaScript into your application so it adds it through importmap or yarn.
  • Finally, it recommends using default style by adding <%= stylesheet_link_tag "coupdoeil/popover" %> to your HTML layout's head and indicates that an implementation of .hidden { display: none } CSS rule is required for popovers animations to work.

Manager details

Manager details example

First, generate the manager popover class for our first example.

rails generate coupdoeil:popover Manager details 
Enter fullscreen mode Exit fullscreen mode

The generator creates the files for the class (app/popovers/manager_popover.rb) and the template (app/popovers/manager_popover/details.html.erb) for this action.

The popover class contains the following code:

# app/popovers/manager_popover.rb class ManagerPopover < ApplicationPopover def details end end 
Enter fullscreen mode Exit fullscreen mode

and the template:

<!-- app/popovers/manager_popover/details.html.erb --> <p>Hello from ManagerPopover#details</p> 
Enter fullscreen mode Exit fullscreen mode

This popover will be plugged onto the project's manager full name displayed in the projects table so it opens when we hover over it.
To "plug" the popover we need to use the coupdoeil_popover_tag helper to wrap the element that will trigger its opening.

<!-- app/views/projects/index.html.erb --> <table class="table"> <thead><!-- … --></thead> <tbody> <% @projects.each do |project| %> <tr> <td><!-- … --><td> <%= coupdoeil_popover_tag ManagerPopover.with(manager: project.manager).details do %> <span class="underline decoration-dotted"><%= project.manager %></span> <% end %> </td> <td><!-- … --></td> </tr> <% end %> </tbody> </table> 
Enter fullscreen mode Exit fullscreen mode

The manager for which to display the popover is passed as an argument for the method .with, like ActionMailer::Parameterized. We then chain by calling details, which is the name of the popover action we want to use here.
Here, hovering the mouse over the span is what will trigger the popover opening, which is default behavior. To retrieve the manager back in the popover class and make it available in the template, we need to use the params within the action method, just like we would with a regular controller action. The slight difference is that when we pass an object that implements GlobalID::Identification, like an ActiveRecord instance, we can get the already deserialized object right from the params, similarly to ActiveJob arguments.

# app/popovers/manager_popover.rb class ManagerPopover < ApplicationPopover def details @manager = params[:manager] @manager.class # Manager end end 
Enter fullscreen mode Exit fullscreen mode

We can finally update the template.

<!-- app/popovers/manager_popover/details.html.erb --> <div class="flex"> <div class="mr-4 shrink-0 avatar"> <div class="size-16 rounded-full"> <%= image_tag @manager.avatar %> </div> </div> <div> <h4 class="text-lg font-semibold"><%= @manager.full_name %></h4> <%= mail_to @manager.email, class: "italic text-sm" %> <div class="mt-2 text-sm"> <span class="mr-1"></span>Member of <%= link_to @manager.role, role_path(@manager.role), class: "link" %> </div> </div> </div> 
Enter fullscreen mode Exit fullscreen mode

And voilà, our first popover now allows to quickly get details about a manager!

New manager form

The second popover will contain a form to add a manager to the application. This form should appear when a button is clicked.

New manager form example

Since the popover class already exists, we just need to add the action:

# app/popovers/manager_popover.rb class ManagerPopover < ApplicationPopover # ... def new_form @manager = Manager.new end end 
Enter fullscreen mode Exit fullscreen mode

and create the template file:

touch app/popovers/manager_popover/new_form.html.erb 
Enter fullscreen mode Exit fullscreen mode

The template consist of the following html.erb:

<!-- app/popovers/manager_popover/new_form.html.erb --> <div class="p-3"> <%= form_for @manager do |f| %> <div class="mb-2"> <%= f.text_field :first_name, placeholder: "first name", class: "input" %> </div> <div class="mb-2"> <%= f.text_field :last_name, placeholder: "last name", class: "input" %> </div> <div class="flex"> <%= f.submit class: "btn btn-sm btn-success block ml-auto" %> </div> <% end %> </div> 
Enter fullscreen mode Exit fullscreen mode

And finally, the popover is plugged around a button located above the managers table:

<!-- app/views/managers/index.html.erb --> <div class="p-2"> <div class="flex justify-start"> <%= coupdoeil_popover_tag ManagerPopover.new_form do %> <button type="button" class="btn btn-soft btn-primary">New manager</button> <% end %> </div> <table class="table"> <!-- ... --> </table> </div> 
Enter fullscreen mode Exit fullscreen mode

If we run the code at this point, we will notice that the popover does not render as shown above. It is positionned on the right of the button, not below, and there is an unwanted arrow displayed. Also, it opens on hover, and does not wait for the button to be clicked.

Wrong render of new manager form example

To resolve these matters, we need to customize this popover. First, we need to set options for the positionning and the event that triggers the opening. We can set these options directly on the helper.

coupdoeil_popover_tag ManagerPopopver.new_form, placement: "bottom-start", trigger: :click 
Enter fullscreen mode Exit fullscreen mode

But another neat option is to set these options as default for this specific popover.

# app/popovers/manager_popover.rb class ManagerPopover < ApplicationPopover # ... default_options_for :new_form, placement: "bottom-start", trigger: :click, offset: "0.5rem" def new_form @manager = Manager.new end end 
Enter fullscreen mode Exit fullscreen mode

This way, every time we render this popover it behaves the same. I also added a little offset so the popover doesn't stick to the button.

Then, we need to use another layout that does not contains an arrow and has a thinner padding.

touch app/popovers/layouts/action.html.erb 
Enter fullscreen mode Exit fullscreen mode
<!-- app/popovers/layouts/action.html.erb --> <div class="bg-white rounded border border-gray-300 shadow"> <%= yield %> </div> 
Enter fullscreen mode Exit fullscreen mode

The popover content will be inserted in place of the yield statement, same as for regular rails templates.

Finally, we need to add one last change to the popover action so it uses this layout.

# app/popovers/manager_popover.rb class ManagerPopover < ApplicationPopover # ... def new_form @manager = Manager.new render layout: "action" end end 
Enter fullscreen mode Exit fullscreen mode

And there it is, the popover now displays as expected!

Wrap-up

So this was a quick demo of Coupdoeil capabilities. As you can see, it is not limited to simple popups just to display details.

You will find more examples on the documentation home page or in the examples section. I tried to make the documentation as comprehensive as possible so you can rely on it to meet your needs.

I spent quite some time over the last few months to bring it as far as I could (yet!) and I like were it landed. But I still have some items in the roadmap I want to explore, like:

  • Adding lazy load option : quick load layout without content, and then the content. For popover with long rendering time but to ensure fast feedback to users.
  • Documenting accessibility of popovers.
  • Sending events on popover open/close.
  • Adding a "detached mode" option to create floating popovers.

I hope you find this gem useful!

Top comments (0)