One of the great new features of Hotwire Turbo 7.2 was custom actions. Originally, Turbo allowed us to send stream actions to the browser to add, remove, or replace some HTML. Now, we can tell the browser to do anything: console log, set title, play sounds, and use the morphdom library for powerful changes.
There are two ways for a Rails app to emit Stream Actions:
- return one or more of them from a controller action with a
turbo_stream
format, and anturbo_stream.erb
action template file. - broadcast them to subscribers of a Turbo Stream
In this post I will recap how to send custom actions via turbo_stream
action responses, and then cover the new thing I had to figure out: how to broadcast custom actions to all subscribers of a Turbo Stream.
What are the built-in actions?
Hotwire Turbo provides a small set of Stream Actions out of the box:
- append
- prepend
- remove
- replace
- update
What makes a Turbo Stream Action is determined by the JavaScript side of the Turbo library. When an append
or remove
action is sent to the browser -- by controller action response, or broadcast stream -- the client-side JavaScript looks at the Action and determines how to handle it.
In all 5 built-in actions above, the client-side JavaScript mutates the browser DOM to add, change, or remove elements.
Sending built-in actions to the browser
As mentioned above, we can send Stream Actions to the browser from controller action responses, or via broadcasts.
For controller action responses,
1) your action needs to specify the turbo_stream
response format:
def create
respond_to do |format|
format.html
format.turbo_stream
end
end
2) you need a corresponding create.turbo_stream.erb
view template file. Within it, you are yielded a turbo_stream
object upon which you construct Stream Actions that will be send back to the browser:
<%= turbo_stream.remove :new_button %>
For broadcasting actions,
1) you setup client-side subscriptions called Streams, by using the turbo_stream_from
helper. Below, the resulting page will subscribe to any actions associated with a specific Book
instance.
<%= turbo_stream_from @book %>
2) now broadcast actions from anywhere on the server-side and they will be sent to the required browsers. For example, we can broadcast replacement HTML if a Book
instance changes from within the Book
class:
class Book < ApplicationRecord
after_update_commit -> {
broadcast_replace_later_to self,
target: dom_id(self),
partial: "books/book_summary",
locals: {book: self}
}
end
The broadcast_replace_later_to
helper will construct a replace
Stream Action, including the newly rendered partial app/views/books/_book_summary.html.erb
, and ask the background job system to send out the request to 0+ subscribers.
What are custom actions?
So if an action is implemented in client-side JavaScript, why can't we do arbitrary things? Log something to the browser console? Dispatch events to the DOM? Activate client-side JavaScript?
Thanks to Turbo 7.2 we now can dispatch arbitrary "custom" actions, and provide our own JavaScript to handle them.
Marco Roth's article Turbo 7.2: A guide to Custom Turbo Stream Actions is the go-to guide.
Moar custom actions
Marco also wrote a huge library of Stream Actions you might want to use called Turbo Power.
Here's a screenshot for dramatic effect:
In your controller turbo_stream.erb
response you can set the page title, and log a message to the console:
<%= turbo_stream.set_title("New Page Title goes here") %>
<%= turbo_stream.console_log("We're hiring if you can see this!") %>
Broadcasting custom actions
But what if you want to broadcast a set_title
custom action to all subscribers of a stream, not just one user?
The good news is that a Stream Action that is sent to the browser via controller actions or via broadcasting is the same message. What changes is how we build the Stream Action and broadcast it.
Whilst there are nice helpers like broadcast_replace_later_to
for built-in actions, I could not find an equivalently concise way to broadcast arbitrary custom actions.
As of writing, I found I had to use some low-level methods to broadcast custom actions.
class Book < ApplicationRecord
include Turbo::Streams::ActionHelper
include Turbo::Streams::StreamName
after_update_commit -> {
content = turbo_stream_action_tag(:set_title, title: "Book: #{title}")
ActionCable.server.broadcast(stream_name_from(self), content)
}
end
Send multiple Stream Actions to the same subscribers by concatenating them together:
content = turbo_stream_action_tag(:set_title, title: "Book: #{title}")
content += turbo_stream_action_tag(:console_log, message: "Book: #{title}")
ActionCable.server.broadcast(stream_name_from(self), content)
Excellent, now we can broadcast to all stream subscriber any arbitrary custom Stream Action; and thanks to Marco's turbo-power
library we can now do just about anything to the browser without needing to write some bespoke JavaScript to handle it. Lovely.
Epilogue
After posting, I chatted with Marco and after a few iterations he suggested the following syntax idea that I like a lot:
Top comments (0)