Broadcasting custom Turbo actions like set_title, morph, and more
Tags: rails, hotwire
Published: 2022-12-28 22:33:42

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](https://www.rubydoc.info/gems/turbo-rails/0.5.2/Turbo/Streams/TagBuilder):

* 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:

```ruby
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:

```erb
<%= 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.

```erb
<%= 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:

```ruby
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](https://dev.to/marcoroth/turbo-72-a-guide-to-custom-turbo-stream-actions-4h0e) 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](https://github.com/marcoroth/turbo_power).

Here's a screenshot for dramatic effect:

![Turbo Power actions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wyvpt0x9ms6xvyio8pk3.png)

In your controller `turbo_stream.erb` response you can set the page title, and log a message to the console:

```erb
<%= 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.

```ruby
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:

```ruby
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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pf3ssomt855sdj84cgtz.png)