Hotwire Turbo Streams let you create custom turbo stream actions.

Example actions you could create:

  • log text into console
  • full-page redirect with a turbo_stream
  • refresh a select turbo frame
  • change css class of an element

Before: How do we full-page redirect outside of a turbo_frame modal? #

A common problem I have is being able to have a full-page redirect after submitting a form in a turbo_frame modal.

With modals, in some cases we would want a response to be:

  • within the frame
  • turbo streams
  • full page refresh
# app/controllers/*_controller.rb
  # request.variant = :turbo_frame
  def create
    respond_to do |format|
      format.turbo_stream do
        # impossible full-page redirect?
      end
    end
  end

Previously, to perform a full-page redirect in this scenario I would turbo_stream a link to the top of the page <body> and auto-click it.

Stimulus controller to autoclick an element:

// app/javascript/controllers/autoclick_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
  connect() {
    this.element.click()
  }
}

Next I would add a helper that:

  • creates a basic HTML <a href=> link with a given url
  • autoclicks this link when the link is available on a page
  • turbo_stream.append_all("body") adds the generated link to the top of the document
# app/helpers/turbo_stream_actions_helper.rb
module TurboStreamActionsHelper
  def turbo_stream_navigate(url)
    link = tag.a(
      nil,
      style: 'display: none;'
      href: url,
      data: {controller: "autoclick", turbo_cache: false}
    )

    turbo_stream.append_all("body") { link }
  end
end

So, a turbo response would add the link to a page and click it.

# app/controllers/*_controller.rb
  # request.variant = :turbo_frame
  def create
    respond_to do |format|
      format.turbo_stream do
+       render turbo_stream: helpers.turbo_stream_navigate(admin_assessment_form_path(@assessment_form))
      end
    end
  end

Smart workaround!

But a “correct” approach would be to use custom Turbo Stream Actions.

Basic Custom Turbo Stream example: console.log #

You can add custom turbo stream actions directily in your app/javascript/application.js.

// app/javascript/application.js
import { Turbo } from "@hotwired/turbo-rails"

// <turbo-stream action="console_log" message="<%= Time.zone.now"></turbo-stream>  
// turbo_stream.action(:console_log, message: "foo") // will this work?
Turbo.StreamActions.console_log = function() {
  const message = this.getAttribute("message")
  console.log(message)
}

Look here if you have JS errors with importing StreamActions.

Now you can add a helper to use the usual Rails syntax for rendering turbo_streams:

# rails generate helper TurboStreamActions

# app/helpers/turbo_stream_actions_helper.rb
module TurboStreamActionsHelper
  # render turbo_stream: turbo_stream.console_log("foobar")
  def console_log(message)
    turbo_stream_action_tag :console_log, message: message
  end 
end

# you need this line to tell the app that this file includes custom turbo stream action helpers
Turbo::Streams::TagBuilder.prepend(TurboStreamActionsHelper)

Now you have 3 ways of invoking this turbo_stream:

<turbo-stream action="console_log" message="<%= Time.zone.now"></turbo-stream>  
turbo_stream.action(:console_log, message: "foo") // will this work?
turbo_stream.console_log("foobar")

Redirect with custom Turbo Stream Actions #

Add the javascript redirect:

// app/javascript/application.js

// enable Turbo.StreamActions
import { Turbo } from "@hotwired/turbo-rails"

// <turbo-stream action="redirect" target="/projects"><template></template></turbo-stream>
// turbo_stream.action(:redirect, projects_path)
Turbo.StreamActions.redirect = function () {
  Turbo.visit(this.target);
};

Use it in your controller:

  # request.variant = :turbo_frame
  def create
    respond_to do |format|
      format.turbo_stream do
-       render turbo_stream: helpers.turbo_stream_navigate(projects_path)
+       render turbo_stream: turbo_stream.action(:redirect, projects_path)
      end
    end
  end

And it will redirect! YAY!

Don’t lose the flash message #

Just add flash.keep to make flash work on a double-redirect 😉

  flash[:notice] = "Comment created."
  flash.keep(:notice)

Advanced redirect with custom Turbo Stream Actions #

In this case, we will pass the url not as a target but as a param. You could pass multiple params like this.

// app/javascript/application.js
import { Turbo } from "@hotwired/turbo-rails"

// <turbo-stream action="redirect_advanced" url="<%= projects_path %>"></turbo-stream>
Turbo.StreamActions.redirect_advanced = function () {
  const url = this.getAttribute('url') || '/'
  // Turbo.visit(url, { frame: '_top', action: 'advance' })
  Turbo.visit(url)
}

Create a helper:

# app/helpers/turbo_stream_actions_helper.rb
module TurboStreamActionsHelper
  # render turbo_stream: turbo_stream.redirect_advanced(projects_path)
  def redirect_advanced(url)
    turbo_stream_action_tag :redirect_advanced, url: url
  end
end

Turbo::Streams::TagBuilder.prepend(TurboStreamActionsHelper)

Use it in your controller:

  # request.variant = :turbo_frame
  def create
    respond_to do |format|
      format.turbo_stream do
-       render turbo_stream: helpers.turbo_stream_navigate(projects_path)
-       render turbo_stream: turbo_stream.action(:redirect, projects_path)
+       render turbo_stream: turbo_stream.redirect_advanced(projects_path)
      end
    end
  end

Test the helper:

# spec/helpers/turbo_stream_actions_helper_spec.rb
  it "returns a turbo-stream tag" do
    expect(helper.redirect("/projects")).to eq(
      "<turbo-stream url=\"/projects\" action=\"redirect_advanced\"><template></template></turbo-stream>"
    )
  end

That’s it!

P.S. The gem marcoroth/turbo_power-rails offers many custom turbo stream actions that you can import into your app. No need to reinvent the wheel!