Modern Rails flash messages (part 3): Refactoring with Turbo
Tags: rails, turbo, stimulus
Published: 2021-07-07 19:57:48

I can not believe it has been almost 10 months since I wrote the first part. Several things have happened since then. Like [Turbo](https://turbo.hotwired.dev/)... 

The [demo](https://modern-rails-flash-messages.herokuapp.com/) 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](https://github.com/CiTroNaK/modern-rails-flash-messages/) with the support for Turbo.

## Installing 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`).

## Usage with UJS and Turbo

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](https://github.com/CiTroNaK/modern-rails-flash-messages/commit/e8b8583766aecbe3fbb6426a18632446148b3d40).

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](https://apidock.com/rails/v6.1.3.1/ActionView/RecordIdentifier/dom_id) 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](https://github.com/ParamagicDev/mrujs) as a modern replacement for UJS. Konnor Rogers did an amazing job there.

## Refactoring and usage with Turbo streams

You can see mentioned changes [here](https://github.com/CiTroNaK/modern-rails-flash-messages/commit/062239fc311dfeb46c101359343ed57a6440ea2e).

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](https://jpscaletti.com/p/5/adding-custom-actions-to-hotwire-turbo-streams) about how to add a custom action to turbo streams. With that, we can refactor it.

From this:

```javascript
// 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:

```html
<!-- 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.

```ruby
# 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:

```ruby
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.