turbo-flash

Problem:

When doing CRUD via turbo, without page redirect, you would STILL want to inform user with a flash message, right?

1. Basic Flash Setup:

  • Default flash types: notice, alert
  • flash.now[:success] - available only in current action (good for turbo)
  • flash[:success] - available in next action (good for redirect)
  • ActionDispatch::Flash
  • Use flash in redirect_to: redirect_to inboxes_path, notice: "Inbox '#{inbox.id}' deleted."
  • Use a custom flash type: redirect_to inboxes_path, flash: {new_type: "Inbox '#{inbox.id}' deleted."}

  • add a basic partial for flash messages

#app/views/shared/_flash.html.erb

<div id="flash">
  <% flash.each do |key, value| %>
    <%= content_tag :div, value, id: "#{key}" %>
  <% end %>
</div>
  • render flash messages in layout

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

<%= render 'shared/flash' %>
  • some css styling for the available flash message types:

#app/assets/stylesheets/application.css

#notice {
  border-radius: 6px;
  padding: 6px;
  color: white;
  background: green;
}

#alert {
  border-radius: 6px;
  padding: 6px;
  color: white;
  background: red;
}

2. Creating an inbox from index page with turbo:

#app/views/inboxes/index.html.erb

++ <div id="new_inbox">
++   <%= render partial: "inboxes/form", locals: { inbox: Inbox.new } %>
++ </div>

<div id="inboxes">
  <%= render @inboxes %>
</div>

#app/controllers/inboxes_controller.rb

  def create
    @inbox = Inbox.new(inbox_params)
    respond_to do |format|
      if @inbox.save
++      format.turbo_stream do
++        render turbo_stream: [
++          turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: Inbox.new }),
++          turbo_stream.prepend('inboxes', partial: 'inboxes/inbox', locals: { inbox: @inbox }),
++        ]
++      end
        format.html { redirect_to @inbox, notice: 'Inbox created.' }
      else
++      format.turbo_stream do
++        render turbo_stream: [
++          turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: @inbox }),
++        ]
++      end
        format.html { render :new, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @inbox.destroy
    respond_to do |format|
++    format.turbo_stream do
++      render turbo_stream: [
++        turbo_stream.remove(@inbox)
++      ]
      end
      format.html { redirect_to inboxes_url, notice: 'Inbox destroyed.' }
    end
  end

3. Finally, render flash with turbo:

#app/controllers/inboxes_controller.rb

  def create
    @inbox = Inbox.new(inbox_params)
    respond_to do |format|
      if @inbox.save
++      flash.now[:notice] = "Inbox #{@inbox.id} created!"
        format.turbo_stream do
          render turbo_stream: [
            turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: Inbox.new }),
            turbo_stream.prepend('inboxes', partial: 'inboxes/inbox', locals: { inbox: @inbox }),
++          turbo_stream.update("flash", partial: "shared/flash")
          ]
        end
        format.html { redirect_to @inbox, notice: 'Inbox created.' }
      else
++      flash.now[:alert] = "Something went wrong"
        format.turbo_stream do
          render turbo_stream: [
            turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: @inbox }),
++          turbo_stream.update("flash", partial: "shared/flash")
          ]
        end
        format.html { render :new, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @inbox.destroy
++  flash.now[:alert] = "Inbox #{@inbox.id} destroyed!"
    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: [
++        turbo_stream.update("flash", partial: "shared/flash"),
          turbo_stream.remove(@inbox)
        ]
      end
      format.html { redirect_to inboxes_url, notice: 'Inbox destroyed.' }
    end
  end

4. flash as a reusable turbo stream in the controller:

#app/controllers/application_controller.rb

++  def render_turbo_flash
++    turbo_stream.update("flash", partial: "shared/flash")
++  end

#app/controllers/inboxes_controller.rb

  def create
    @inbox = Inbox.new(inbox_params)
    respond_to do |format|
      if @inbox.save
++      flash.now[:notice] = "Inbox #{@inbox.id} created!"
        format.turbo_stream do
          render turbo_stream: [
            turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: Inbox.new }),
            turbo_stream.prepend('inboxes', partial: 'inboxes/inbox', locals: { inbox: @inbox }),
++          render_turbo_flash,
--          turbo_stream.update("flash", partial: "shared/flash")
          ]
        end
        format.html { redirect_to @inbox, notice: 'Inbox created.' }
      else
++      flash.now[:alert] = "Something went wrong"
        format.turbo_stream do
          render turbo_stream: [
            turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: @inbox }),
++          render_turbo_flash,
--          turbo_stream.update("flash", partial: "shared/flash")
          ]
        end
        format.html { render :new, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @inbox.destroy
++  flash.now[:alert] = "Inbox #{@inbox.id} destroyed!"
    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: [
++        render_turbo_flash,
--        turbo_stream.update("flash", partial: "shared/flash"),
          turbo_stream.remove(@inbox)
        ]
      end
      format.html { redirect_to inboxes_url, notice: 'Inbox destroyed.' }
    end
  end

5. Auto-dismiss flash messages with Stimulus

app/javascript/controllers/autohide_controller.js

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  connect() {
    setTimeout(() => {
      this.dismiss();
    }, 5000);
  }

  dismiss() {
    this.element.remove();
  }
}
# app/views/shared/_flash.html.erb
<div id="flash">
  <% flash.each do |key, value| %>
++    <div data-controller="autohide">
      <%= content_tag :div, value, id: "#{key}" %>
++    </div>
  <% end %>
</div>

6. Stream and Display multiple flash messsages

  • WRAP the parial into an ID
# app/views/layouts/application.html.erb
++    <div id="flash" style="position:absolute; z-index:2; right:10px; width:200px;">
      <%= render 'shared/flash' %>
++    </div>
  • not in INSIDE the partial
# app/views/shared/_flash.html.erb
--<div id="flash">
  <% flash.each do |key, value| %>
    <div data-controller="autohide">
      <%= content_tag :div, value, id: "#{key}" %>
    </div>
  <% end %>
--</div>
  • prepend new flash messages to have all of them visible on the page
-- turbo_stream.update("flash", partial: "shared/flash")
++ turbo_stream.prepend("flash", partial: "shared/flash")