DEV Community

Dr Nic Williams
Dr Nic Williams

Posted on

Broadcasting custom Turbo actions like set_title, morph, and more

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 an turbo_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
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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 %>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

Turbo Power actions

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!") %>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

Image description

Top comments (0)