20
Modern Rails flash messages (part 3): Refactoring with Turbo
I can not believe it has been almost 10 months since I wrote the first part. Several things have happened since then. Like Turbo...
The demo application still works in the same way, but a few changes needed to be made to support Turbo. I also refactored it a little bit to make it easier to use with multiple controllers.
I've updated the code at the repository on GitHub with the support for Turbo.
That was really easy, just run bundle add turbo-rails
and run bin/rails turbo:install
.
Don't forget to replace any mention of turbolinks
to turbo
(eg. data-turbolinks-track
=> data-turbo-track
).
The original solution worked with UJS and Turbolinks. Let's make it work in the same way but with Turbo. You can see mentioned changes here.
Thanks to Turbo, we can remove code related to Turbolinks cache (this.isPreview
at app/javascript/controllers/notification_controller.js
) and replace it with data-turbo-cache="false"
at app/components/notification_component.html.erb
.
We also need to replace Turbolinks.visit
with window.Turbo.visit
in app/javascript/controllers/notification_controller.js
.
I also started using the dom_id
helper to identify HTML parts related to each post.
The last thing I need to mention is adding data: { turbo: false }
to each remote: true
link to disable Turbo on them. After that, it should work as before with Turbolinks.
BTW you should check out mrujs as a modern replacement for UJS. Konnor Rogers did an amazing job there.
You can see mentioned changes here.
The main goal here is to refactor the JS response for the undo action at app/views/posts/destroy.js.erb
and replace it with Turbo stream.
The first thing that it does is adding a .hidden
class to hide the post. That is something that Turbo can't do by default. Luckily, someone clever wrote an article about how to add a custom action to turbo streams. With that, we can refactor it.
From this:
// app/views/posts/destroy.js.erb
// finding and hiding the record
document.getElementById('<%= dom_id(post) %>').classList.toggle('hidden');
// displaying the notification
<% flash.each do |type, data| %>
document.getElementById('notifications').insertAdjacentHTML("afterBegin", "<%=j render(NotificationComponent.new(type: type, data: data)) %>");
<% end %>
To the same functionality with Turbo stream:
<!-- app/views/undo/destroy.turbo_stream.erb -->
<turbo-stream action="addclass" target="<%= dom_id(record) %>">
<template>hidden</template>
</turbo-stream>
<% flash.each do |type, data| %>
<%= turbo_stream.prepend('notifications', render(NotificationComponent.new(type: type, data: data))) %>
<% end %>
Now, we could use that. Let's move the response from posts_controller.rb
and place it to app/controllers/concerns/destroy_with_undo_response.rb
, so we can easily reuse it. You can see that I also refactored it and added the Turbo stream support.
# app/controllers/concerns/destroy_with_undo_response.rb
module DestroyWithUndoResponse
extend ActiveSupport::Concern
private
def destroy_with_undo_response(record:, job_id:, redirect:)
respond_to do |format|
format.html do
flash[:success] = undo_flash_message(klass: record.class, job_id: job_id)
redirect_to redirect
end
format.turbo_stream do
if params[:redirect]
flash[:success] = undo_flash_message(klass: record.class, job_id: job_id)
redirect_to redirect
else
flash.now[:success] = undo_flash_message(klass: record.class, job_id: job_id, inline: true)
render 'undo/destroy', locals: { record: record }
end
end
end
end
def undo_flash_message(klass:, job_id:, inline: nil)
timeout = defined?(klass::UNDO_TIMEOUT) ? klass::UNDO_TIMEOUT : 10
{
title: "#{klass.model_name.human} was removed",
body: 'You can recover it using the undo action below.',
timeout: timeout, countdown: true,
action: {
url: undo_path(job_id, inline: inline),
method: 'delete',
name: 'Undo'
}
}
end
end
We can now include it in the PostsController
and update the destroy
action:
class PostsController < ApplicationController
include DestroyWithUndoResponse
def destroy
post = Post.active.find(params[:id])
job_id = post.schedule_destroy
destroy_with_undo_response(record: post, job_id: job_id, redirect: posts_path)
end
end
In app/views/posts/show.html.erb
and app/views/posts/_form.html.erb
we need to add a param redirect: true
to each post_path(@post)
. This param is then used in the turbo stream response to do the right action.
In app/views/posts/index.html.erb
we can remove remote: true, data: { turbo: false }
as we will leverage Turbo.
And that's all. The application now behaves exactly the same, but with Turbo.
20