Creating reusable flashes in Rails using Shoelace

Purpose

To document how I made a reusable flash system leveraging Shoelace + Rails

The Problem

We all like beautiful flashes! But sometimes it takes work to make a reusable flash system. I'm going to show how I implemented mine with Shoelace, but this could easily be extended to any other CSS framework or your own CSS.

Building a reusable interface

There are 2 steps to building a reusable interface for flashes. The first is for the backend.

Creating a controller concern

Flashes are accessed from a Controller, so as such, its easiest to create a controller concern called: Flashable

In your app/controllers/concerns directory, create a file called flashable.rb

In your app/controllers/concerns/flashable.rb file we're going to add a number of convenience methods to interface with flash.

Basically what we want to do is be able to call things like flash_success("My message") or flash_danger("Dangerous operation!") from a controller. So lets first start by building a #show_flash base.

# app/controllers/concerns/flashable.rb

module Flashable
  extend ActiveSupport::Concern

  # These are the icons that come from the Shoelace docs on Alert.
  ICONS = {
    primary: "info-circle",
    success: "check2-circle",
    info: "gear",
    warning: "exclamation-triangle",
    danger: "exclamation-octagon"
  }.freeze

  included do
    def show_flash(type, message, now: false, icon: nil)
      icon ||= ::Flashable::ICONS[type]

      hash = { message: message, icon: icon }

      return flash.now[type] = hash if now

      flash[type] = hash
    end
  end
end

This is the building block for our convenience methods. Our convenience methods will build off the structure we have in place here.

Our final result will look like this:

# app/controllers/concerns/flashable.rb

module Flashable
  extend ActiveSupport::Concern

  ICONS = {
    primary: "info-circle",
    success: "check2-circle",
    info: "gear",
    warning: "exclamation-triangle",
    danger: "exclamation-octagon"
  }.freeze

  included do
    def flash_primary(message, now: false, icon: nil)
      show_flash(:primary, message, now: now)
    end

    def flash_success(message, now: false, icon: nil)
      show_flash(:success, message, now: now)
    end

    def flash_info(message, now: false, icon: nil)
      show_flash(:success, message, now: now, icon: icon)
    end

    def flash_warning(message, now: false, icon: nil)
      show_flash(:warning, message, now: now, icon: icon)
    end

    def flash_danger(message, now: false, icon: nil)
      show_flash(:danger, message, now: now, icon: icon)
    end

    def show_flash(type, message, now: false, icon: nil)
      icon ||= ::Flashable::ICONS[type]

      hash = {message: message, icon: icon}

      return flash.now[type] = hash if now

      flash[type] = hash
    end
  end
end

Yes there are some ways we could've meta-programmed our flash messages, but sometimes verbosity is okay! So now, wherever the Flashable module gets included, it will define these methods in the context of the Class or Module that includes it.

The next step is to go into your app/controllers/application_controller.rb and include Flashable like so:

# app/controllers/application_helper.rb

class ApplicationController
  include Flashable
end

This means every controller now has access to our new flash methods.

Reusable frontend interface

Now that our controller can successfully call flash_success("message") now we have to create a way for the frontend to interpret the flash hash and render it accordingly.

In your app/helpers/application_helper.rb we're going to make the following helper method: #show_alert

This will take in the following parameters: type, message, icon and will use these 3 arguments to render a shoelace alert.

Heres what the method ends up looking like:

# app/helpers/application_helper.rb

module ApplicationHelper
  def show_alert(type:, message:, icon:)
    tag.sl_alert(type: type, open: true, closable: true) do
      tag.sl_icon(nil, slot: "icon", name: icon) + "\n" + message
    end
  end
end

Example

In any one of our views we can do call the following code to display a Shoelace flash:

<%= show_alert(type: "success", message: "You have successfully logged in!", icon: ::Flashable::ICONS[:success] %>

And heres what it will look like:

Final piece of the puzzle

Alright now we have a way for displaying flashes, but now we actually need to add a flash handler in our layout.

Go to app/views/layouts/application.html.erb and we'll add the flash handler code.

<html>
  <head>
  </head>
  <body>
    <div>
      <% flash.each do |type, hash| %>
        <div style="margin: 1rem 0;"></div>
        <%= show_alert(type: type, message: hash[:message], icon: hash[:icon]) %>
      <% end %>
    </div>

    <main>
      <%= yield %>
    </main>
  </body>
</html>

What it all looks like

In a controller of your choice, add some flashes, and then go check out a page! Heres how all the flashes would stack up on the same page!

class HomeController < ApplicationController
  def index
    %w[primary success info warning danger].each do |str|
      flash_method = "flash_#{str}".to_sym
      public_send(flash_method, str, now: true)
    end
  end
end

21