Turbo Stream and ActionCable
Previously, I mentioned that Turbo Stream does not necessarily require ActionCable. But it would be nice to have so that all clients (browsers) can receive update. Here, I try to explore the use of Turbo Stream with ActionCable.
Usually setting up ActionCable requires changes in channels at server side and javascript at client side. With Turbo Stream, changes are minimal. I have plain Rails 6.1 installed. Everything under app/channels
is untouched. There are config settings in config/environments/
for ActionCable which involves in request origins. They are not modified, but you may need to in case Turbo Stream doesn’t work. If you intall turbo_rails
, it will use redis for ActionCable. In my case, it is still the default async
in config/cable.yml
. Those are places to look if this article doesn’t work for you.
Turbo Stream provides concerns to hook up with model, which is quite powerful. But there is too much magic inside and may not fit you need. Therefore, I would like to start with minimal settings to show a flexible use of Turbo Stream with ActionCable.
Let’s begin with the delete action, which is the easiest.
# show.html.slim
div
| To be deleted
div
= link_to 'delete', turbo_user_path(current_user)
We want user to delete the above HTML element after clicking the link. Therefore, we need defined a method in users#turbo
for that. Remember to add the corresponding route in config/routes.rb
.
In previous article, we can wrap this part in turbo_frame_tag
:
= turbo_frame_tag dom_id(current_user) do
div
| To be deleted
div
= link_to 'delete', turbo_user_path(current_user)
And ask controller to delete it like this:
# user_controller.rb
def turbo
respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.remove(current_user) }
format.html { redirect_to users_url }
end
end
Now, when you click the link, the turbo_frame will be removed. If you open two browers to the same page, you will notice that removing element in one browser does not affect the other. That is where ActionCable becomes useful. First, we will add the turbo_stream_from
to let Rails knows where the updated (removed) fragment should go:
# show.html.slim
= turbo_stream_from "user_stream"= turbo_frame_tag dom_id(current_user) do
div
| To be deleted
div
= link_to 'delete', turbo_user_path(current_user)
I intentionally use a string user_stream
as identifier for turbo_stream_from
. You can change it to a model later. Now, you will see that turbo_stream_from
insert a signed stream name into the view. To update the turbo_frame, we need to broadcast the remove action. It is mostly done via model instead of controller.
def turbo
current_user.broadcast_remove_to("user_stream") respond_to do |format|
format.turbo_stream
format.html { redirect_to users_url }
end
end
Now, instead of rendering turbo_stream, we ask model (current_user) to broadcast the action to user_stream
turbo_stream in the view. That is all we need. When you click the link, this element in both browser will be removed. If that doesn’t happen, restarting Rails server may help. Now, you can also name the turbo_stream
differently, like this:
# show.html.slim
= turbo_stream_from :show, current_user= turbo_frame_tag dom_id(current_user) do
div
| To be deleted
div
= link_to 'delete', turbo_user_path(current_user)# users_controller.rb
def turbo
current_user.broadcast_remove_to(:show, current_user)
end
As long as you keep the stream name the same in view and controller, it will work. You can also delete any element inside the turbo_frame like this:
# show.html.slim
= turbo_stream_from :show, current_user= turbo_frame_tag "turbo_frame" do
div id=dom_id(current_user)
| To be deleted
div
= link_to 'delete', turbo_user_path(current_user)
So now, the magic is more obvious. Just add turbo_stream_from
in a view and broadcast turbo_stream fragment to the same stream name.
We can try the replace action of turbo stream. While broadcast_replace_to
will work, it often asks partial for the model. We may not have an exact partial for this model or the partial is not suitable in this place. Luckily we can find a way around. It accepts content:
key. Therefore, we can do so in controller;
current_user.broadcast_replace_to(:show, current_user, content: "<div>replaced</div>")
Now, element in both browsers can be replaced. If for some reasons, you want to replace element with different id, you can use Turbo::StreamsChannel
like this:
Turbo::StreamsChannel.broadcast_replace_to(:show, current_user, target: "another_id", content: "<div>replaced</div>")
From this point, you can start to build your app around Turbo Stream with ActionCable. It eliminates all the work in setting up channels at server side and javascript at client side. Just add turbo_stream_from
in view and broadcast in controller (or model). In another word, it is a simplier version of ActionCable. There are broadcast_later
variants which will also use ActiveJob for broadcast, save you another step in setting up ActiveJob.