Custom Turbo Stream Actions
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!
Did you like this article? Did it save you some time?