Stay Ahead in Ruby!
From development processes to the most useful gems, get it all straight in your inbox. Join our Ruby blog today!

Skip to main content

Flash Messages with Hotwire

Table of Contents
Attention! This article might be outdated, refer to latest documentation if solution does not work.
Flash Messages with Hotwire - cover image
Have you ever thought of implementation of ⚡️Flash Messages⚡️ in SPA-like? How easy is it?

Intro #

Flash messages is a powerful instrument of notifying your users about the results of execution or changes in your application. According to official Rails documentation: “The flash is a special part of the session which is cleared with each request. This means that values stored there will only be available in the next request, which is useful for passing error messages”. But what if we mix classic Rails magic with the latest technologies such as Hotwire?

Let’s imagine that you are working with Single-Page Application-like, Bootstrap and Hotwire, and there is no easy way to “reload” page layout to render flash messages. How should we notify our users about the changes in this case?

To do so we use Hotwire functionallity, ViewComponent and Stimulus controller!

How does it work? #

First of all we need to create a Stimulus controller.

Let’s generate this controller with simple rails command:

  rails g stimulus FlashController

Although, you can create it manually.

  /* app/javascript/controllers/index.js */

  ...
  import FlashController from "./flash_controller.js"
  application.register("flash", FlashController)
  ...

You can see example of our Stimulus controller code below.

  /* app/javascript/controllers/flash_controller.js */

  import { Controller } from "@hotwired/stimulus"

  export default class extends Controller {
    initialize() {
      const toast = this.element.querySelector('.alerts');
      new bootstrap.Toast(toast, {delay: 100000}).show();
    }

    close () {
      setTimeout(() => this.element.remove(), 1000)
    }
  }

Now we should modify our layout file and prepare ViewComponent which we will use as our partial.

  # app/components/flash_component.rb

  # frozen_string_literal: true

  class FlashComponent < ViewComponent::Base

    attr_accessor :flash

    # Here we initialize our flash variable to use it inside of flash.html.erb
    def initialize(flash)
      @flash = flash
    end

    # We use this method to choose appearance of our flash message
    def toast_class(flash_type)
      case flash_type
      when "notice"
        "toast-info"
      when "alert"
        "toast-error"
      else
        "toast-success"
      end
    end
  end

We should assign a data-controller="flash" on div container inside of our cycle to trigger it each time we add a new flash message.

Also we will use data-action="click" to trigger action after clicking on the ‘Close’ button. In our case we will just hide them after short timeout to make the interaction more smoother.

  # app/components/flash_component.html.erb

  <% flash.each do |flash_type, messages| %>
    <div class="toast-container position-fixed top-0 start-50 translate-middle-x p-3 mt-5" data-controller="flash">
      <div class="toast align-items-center alerts #{toast_class(flash_type)}" role="alert" aria-live="assertive" aria-atomic="true">
        <div class="d-flex">
          <div class="toast-body">
            <% if flash_type == :alert %>
              <div>
                The following errors have been encountered:
              </div>
            <% end %>

            <% if messages.is_a? Array %>
              <ul>
                <% messages.each do |message| %>
                <li>
                  <%= message.gsub("'","").html_safe %>
                </li>
            <% else %>
              <%= messages.gsub("'","").html_safe %>
            <% end %>
          </div>

          <button aria-label="Close" class="btn-close me-2 m-auto" data-action="click->notifications#close" data-bs-dismiss="toast" type="button"></button>
        </div>
      </div>
    </div>
  <% end %>

Why did we choose ViewComponent? It allows us to build reusable components with additional and more readable ruby-logic and helps to incapsulate everything needed only for this component, without usage of helpers. It has closely similar functionality to Trailblazer Cells but can be used separately from the Trailblazer and be combined with any gem or stack.

But we still need to add our FlashComponent to the layout of our application. It is neseccary to add it in the following way, because of the two possible scenarios:

  • User can enter our application and get some notification.
  • User can recieve it during surfing between our “pages”.

For both cases we should notify them in the same way.

  # app/views/layouts/welcome.html.erb

  <!DOCTYPE html>
  <html>
    <head>
      ...
    </head>

    <body>
      <%= turbo_frame_tag :flash do %>
        <%= render FlashComponent.new(flash) %>
      <% end %>
      ...
      <%= yield %>
      ...
    </body>
  </html>

To use our new “flash functionality” we should just add a helper method to our application_controller.rb or to any controller that you need, to simplify interaction with our notifications.

  # app/controllers/application_controller.rb

  class ApplicationController < ActionController::Base
    ...
    def render_turbo_stream_flash_messages(type, message)
      flash.now[type] = message
      render turbo_stream: turbo_stream.prepend(:flash, FlashComponent.new(flash).render_in(view_context))
    end
    ...
  end

And from the Rails side in any controller inherited from the ApplicationController we can replace this flash <turbo-frame ...> with the new flash messages any time we need it just by using our method render_turbo_stream_flash_messages(key, message).

  class PostsController < ApplicationController
    ...
    def send
      result = Post::Send.new.call

      if result.success?
        render_turbo_stream_flash_messages(:success, "Post was sent!")
      else
        render_turbo_stream_flash_messages(:alert, "Post was not sent!")
      end
    end
    ...
  end

Once our flash appears, it will be removed from the session storage.

Example #

Here is an example of the flash messages implemented with our technologies and it works like a charm!

An example of Flash Messages with Hotwire
Flash messages with Hotwire and ViewComponent

We are ready to provide expert's help with your product
or build a new one from scratch for you!

Contact MobiDev’s tech experts!