February 23, 2021

Flash messages with Hotwire and Turbo Streams 

The latest version of Spina CMS uses flash messages to subtly tell the user when something has been saved (or not).
Flash message

You can add flash messages to any Rails view using the flash object.
<!-- layouts/application.html.erb -->
<%= render 'shared/flash' %>

<!-- shared/_flash.html.erb -->
<div class="flash-messages">
  <% flash.each do |type, message| %>
    <div class="flash-message">
      <%= message %>
    </div>
  <% end %>
</div>
After submitting a form, you set a flash message and redirect the user. The flash message will be rendered by the _flash partial.
# pages_controller.rb
def update
  if @page.update(page_params)
    flash[:success] = "Page saved"
    redirect_to @page
  else
    render :edit, status: :unprocessable_entity
  end
end
Flash messages without redirects
Adding a partial to your layout works fine when using redirects, but sometimes you don't want to redirect the user. A good example is sorting pages in Spina CMS. Users can drag and drop pages in any order. After dragging, a hidden form will be submitted that will save the new order. A flash message immediately appears telling the user that the sort has been saved. Without any redirects!

Step 1
Start by wrapping your flash partial with a turbo-frame.
<!-- layouts/application.html.erb -->
<turbo-frame id="flash">
  <%= render 'shared/flash' %>
</turbo-frame>
Step 2
Instead of responding to your form submission with a redirect, respond using turbo_stream. Remember to use flash.now to set your flash message. Use render turbo_stream to render a turbo-stream-tag that updates your flash frame with a new flash partial.
# pages_controller.rb
def sort
  # ...
  flash.now[:success] = "Sorting saved"
  render turbo_stream: turbo_stream.update("flash", partial: "shared/flash")
end
Step 3
You're done. It's that simple with Hotwire.

It's a good idea to refactor the turbo_stream call into a separate method render_flash so it's easy to reuse.
# application_controller.rb
def render_flash
  render turbo_stream: turbo_stream.update("flash", partial: "shared/flash")
end
Optional: confetti!
In Spina CMS publishing a page is cause for celebration. That's why there's a special flash message type flash[:confetti]. When used it adds the data-controller="confetti" attribute to a flash message, resulting in a huge blast of confetti.

Feel free to steal my confetti_controller.js using canvas-confetti:
// confetti_controller.js
import { Controller } from "stimulus"
import confetti from "canvas-confetti"

export default class extends Controller {
  
  connect() {
    if(this.canvas == undefined) this.createCanvas()
    
    // Create confetti object
    let canvas = this.canvas
    this.confetti = confetti.create(canvas, {resize: true})
    
    // Fire!
    this.fire(0.25, {spread: 26, startVelocity: 55})
    this.fire(0.2, {spread: 60})
    this.fire(0.35, {spread: 100, decay: 0.91, scalar: 0.8})
    this.fire(0.1, {speed: 120, startVelocity: 25, decay: 0.92, scalar: 1.2})
    this.fire(0.1, {speed: 120, startVelocity: 45})
    
    // Remove canvas after 5 seconds
    setTimeout(function() {
      document.body.removeChild(canvas)
    }, 5000)
  }
  
  fire(particleRatio, options) {
    let count = 200
    let defaults = {origin: {y: 0.7}}
    
    this.confetti(Object.assign({}, defaults, options, {
      particleCount: Math.floor(count * particleRatio)
    }))
  }
  
  createCanvas() {
    document.body.insertAdjacentHTML('beforeend', `<canvas id="confetti" class="fixed inset-0 w-full h-full pointer-events-none z-50"></canvas>`)
  }
  
  get canvas() {
    return document.querySelector("#confetti")
  }
  
}