Flash messages with Turbo Streams (Bootstrap Toast)

The goal: show flash messages from turbo_stream via Bootstrap Toast

Rails controller: set the flash message

/app/controllers/tasks_controller.rb
def create
  @task = @list.tasks.build(task_params)

  respond_with(@task, location: location) do |format|
    if @task.save
      format.turbo_stream { flash.now[:notice] = "Task successfully created" }
    end
  end
end
We use flash.now here since there is no redirect with turbo_stream.
respond_with since we uses the responders gem from https://github.com/heartcombo/responders. Your mileage may vary. Plain rails 7 generators don’t use respond_to (nor respond_with). See app/controllers/categories_controller.rb.

Layout

Very simple: just a <div> with an id where to show flash messages from turbo_stream rendering. Otherwise render existing flashes as usual. In case of turbo_stream rendering the content of the <div> will be replaced by turbo_stream content.

/app/views/layout/application.html.erb
<div class="container-fluid">
  <div id="flash"> (1)
    <%= render "shared/flash_toast" %>
  </div>
  <%= yield %>
</div>
1 id for turbo_stream replacing

A Helper to render Turbo Toasts

A helper to call from each create|update|destroy.turbo_stream.erb view: simply add the following snippet to each view:

create|update|destroy.turbo_stream.erb
<%= render_turbo_toast %>

The helper itself

app/helpers/flash_toast_helper.rb
module FlashToastHelper
  def render_turbo_toast
    turbo_stream.prepend "flash", partial: "shared/flash_toast"
  end
end

A partial to go through each flash key

partial app/views/shared/_flash_toast.html.erb
<% flash.each do |severity,message| %>
  <%= render ToastComponent.new(severity: severity, message: message) %>
<% end %>

A view component for toasts

Since flashes goes with keys like :alert, :error, :notice, we need those translated in Bootstrap jargon like 'danger', 'info' or 'success'.

/app/components/toast_component.rb
# frozen_string_literal: true

class ToastComponent < ViewComponent::Base
  def initialize(severity:, message:)
    @level = update_severity(severity)
    @message = message
  end

private

  # bootstrapify names
  def update_severity(severity)
    case severity.to_sym
    when :alert, :error
      "danger"
    when :notice
      "info"
    else
      severity.to_s
    end
  end

end
/app/components/toast_component.html.erb
<div class="position-fixed top-0 end-0 p-3" style="z-index: 2000">
  <div id="liveToast" 
       class="toast align-items-center border-0 text-white bg-<%= @level %>" 
       role="alert" aria-live="assertive" aria-atomic="true"
       data-controller="toast">
    <div class="d-flex">
      <div class="toast-body">
        <%= @message %>
      </div>
      <button type="button" class="btn-close btn-close-white me-2 m-auto" 
              data-bs-dismiss="toast" aria-label="Close"></button>
    </div>
  </div>
</div>
z-index: 2000 to overcome the z-index coming with our navbar (z-index: 1030)