DEV Community

Roland Studer
Roland Studer

Posted on

Custom Turbo Stream Actions

Turbo Hotwire is neat, I really like the simplicity of Turbo frames. But after having used CableReady the turbo streams feel a bit limiting. I was surprised, when I could not find any gem/package that enhances turbo streams to create custom turbo actions.

Disclaimer: This post won't make much sense to you, if you are not familiar with Turbo streams and StimulusJS

Let's say if you want to redirect to a new page after some job is done, or you want to reset a form, there is no way to do that with a turbo stream action. With CableReady you could do
cable_ready.redirect_to(url: some_url) (docs).

So I was wondering how one would go about creating custom turbo stream actions. From how DHH (the original creator of Rails) has discussed turbo streams, it is unlikely that turbo will support more actions, there is a
pending PR but not much has moved.

So what can we do today?

Stimulus controllers to the rescue

We can mimic the pattern turbo streams use with StimulusJS. StimulusJS has a lifecycle method called connect, that is executed when a new element with a stimulus controller is added to the DOM.

For the redirect example, we can write something like this:


import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
      // take a value as an argument where to redirect
    static values = { url: String } 
    connect() {
        // perform wanted behavior
        Turbo.visit(this.urlValue)
        // clean up after action is executed
      this.element.remove()
    }
}
Enter fullscreen mode Exit fullscreen mode

So the following html added anywhere in the body would perform a redirect:

<template data-controller="redirect" data-redirect-url-value="<%= your_redirect_url %>"></template>
Enter fullscreen mode Exit fullscreen mode

So with a turbo stream we can attach this to the body:

<%= turbo_stream.append_all "body" do %>
    <template data-controller="redirect" data-redirect-url-value="<%= your_redirect_url %>"></template>
<% end %>
Enter fullscreen mode Exit fullscreen mode

And the browser will redirect, given this turbo stream response. BTW: We use append_all so we can append to the body, withouth having to rely on the presence of an element with a certain id.

Better developer experience

It's not the most elegant solution, but it works and has only little overhead. We can improve the developer experience by writing a helper to allow something like:

<%= turbo_stream_action :redirect, url: "http://www.rstuder.ch" %>
Enter fullscreen mode Exit fullscreen mode

So we can write a helper that creates a turbo stream tag that will append to the body and convert a hash of values to values for a stimulus controller:

module TurboStreamHelper
  def turbo_stream_action(action, **values)
    controller_name = action.to_s.dasherize

    data_attrs = {
      "data-controller" => controller_name
    }

    data_attrs = values.each_with_object(data_attrs) do |(key, value), attrs|
      attrs["data-#{controller_name}-#{key.to_s.dasherize}-value"] = value
    end

    turbo_stream.append_all "body" do
      content_tag(:template, nil, data_attrs.merge)
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

So we got a simple API around some conventions, that gives us the power to create custom actions and invoke them with arguments from a turbo stream template.

Conclusion

Turbo streams with Stimulus Controllers give us enough power to achieve custom actions. Without further dependencies. If you use CableReady anyway, then have a look at
this package from the wonderful Marco Roth that enables you to use the cable ready operations with turbo streams.

ps: This post was originally published on rstuder.ch

Top comments (2)

Collapse
 
superails profile image
Yaroslav Shmarov

.append_all "body" - very interesting solution. Thanks!

Collapse
 
gathuku profile image
Moses Gathuku

Thank for this cool idea @rolandstuder