Turbo Stream's Swiss Army Knife

Welcome to TurboReady 👋
The only Stream Action you really need

Lines of Code GEM Version GEM Downloads Ruby Style NPM Version NPM Downloads NPM Bundle Size JavaScript Style Tests Twitter Follow

TurboReady extends Turbo Streams to give you full control of the browser's Document Object Model (DOM).

turbo_stream.invoke "console.log", args: ["Hello World!"]

Thats right! You can invoke any DOM method on the client with Turbo Streams.

Table of Contents

Why TurboReady?

Turbo Streams intentionally restricts official actions to CRUD related activity. These official actions work well for a considerable number of use cases. Try pushing Turbo Streams as far as possible before reaching for TurboReady.

If you find that CRUD isn't enough, TurboReady is there to handle pretty much everything else.

⚠️ TurboReady is intended for Rails apps that use Hotwire but not CableReady. This is because CableReady already provides a rich set of powerful DOM operations.

📘 NOTE: Efforts are underway to bring CableReady's DOM operations to Turbo Streams.

Sponsors

Proudly sponsored by

Dependencies

Installation

Be sure to install the same version for each libary.

bundle add "turbo_ready --version VERSION"
yarn add "turbo_ready@VERSION --exact"

Setup

Import and intialize TurboReady in your application.

# Gemfile
gem "turbo-rails", ">= 1.1", "< 2"
+gem "turbo_ready", "~> 0.0.6"
# package.json
"dependencies": {
  "@hotwired/turbo-rails": ">=7.2",
+  "turbo_ready": "^0.0.6"
# app/javascript/application.js
import '@hotwired/turbo-rails'
+import 'turbo_ready'

Usage

Manipulate the DOM from anywhere you use official Turbo Streams. The possibilities are endless. Learn more about the DOM at MDN.

turbo_stream.invoke "console.log", args: ["Hello World!"]

Method Chaining

You can use dot notation or selectors and even combine them!

turbo_stream
  .invoke("document.body.insertAdjacentHTML", args: ["afterbegin", "<h1>Hello World!</h1>"]) # dot notation
  .invoke("setAttribute", args: ["data-turbo-ready", true], selector: ".button") # selector
  .invoke("classList.add", args: ["turbo-ready"], selector: "a") # dot notation + selector

Event Dispatch

It's possible to fire events on window, document, and element(s).

turbo_stream
  .invoke(:dispatch_event, args: ["turbo-ready:demo"]) # fires on window
  .invoke("document.dispatchEvent", args: ["turbo-ready:demo"]) # fires on document
  .invoke(:dispatch_event, args: ["turbo-ready:demo"], selector: "#my-element") # fires on matching element(s)
  .invoke(:dispatch_event, args: ["turbo-ready:demo", {bubbles: true, detail: {...}}]) # set event options

Syntax Styles

You can use snake_case when invoking DOM functionality. It will implicitly convert to camelCase.

turbo_stream.invoke :event,
  args: ["turbo-ready:demo", {detail: {converts_to_camel_case: true}}]

Need to opt-out? No problem... just disable it.

turbo_stream.invoke :contrived_demo, camelize: false

Extending Behavior

If you add new capabilities to the browser, you can control them from the server.

// JavaScript on the client
import morphdom from 'morphdom'

window.MyNamespace = {
  morph: (from, to, options = {}) => {
    morphdom(document.querySelector(from), to, options)
  }
}
# Ruby on the server
turbo_stream.invoke "MyNamespace.morph",
  args: [
    "#demo",
    "<div id='demo'><p>You've changed...</p></div>",
    {children_only: true}
  ]

Implementation Details

There's basically one method to learn... invoke

# Ruby
turbo_stream
  .invoke(method, args: [], selector: nil, camelize: true, id: nil)
#         |       |         |              |               |
#         |       |         |              |               |- Identifies this invocation (optional)
#         |       |         |              |
#         |       |         |              |- Should we camelize the JavaScript stuff? (optional)
#         |       |         |                 (allows us to write snake_case in Ruby)
#         |       |         |
#         |       |         |- A CSS selector for the element(s) to target (optional)
#         |       |
#         |       |- The arguments to pass to the JavaScript method (optional)
#         |
#         |- The JavaScript method to invoke (can use dot notation)

📘 NOTE: The method will be invoked on all matching elements if a selector is present.

The following Ruby code,

turbo_stream.invoke "console.log", args: ["Hello World!"], id: "123ABC"

emits this HTML markup.

<turbo-stream action="invoke" target="DOM">
  <template>{"id":"123ABC","receiver":"console","method":"log","args":["Hello World!"]}</template>
</turbo-stream>

When this element enters the DOM, Turbo Streams automatically executes invoke on the client with the template's JSON payload and then removes the element from the DOM.

Broadcasting

You can also broadcast DOM invocations to subscribed users.

  1. First, setup the stream subscription.

    <!-- app/views/posts/show.html.erb -->
    <%= turbo_stream_from @post %>
    <!--                  |
                          |- *streamables - model(s), string(s), etc...
    -->
    
  2. Then, broadcast to the subscription.

    # app/models/post.rb
    class Post < ApplicationRecord
      after_save do
        # emit a message in the browser conosle for anyone subscribed to this post
        broadcast_invoke "console.log", args: ["Post was saved! #{to_gid.to_s}"]
    
        # broadcast with a background job
        broadcast_invoke_later "console.log", args: ["Post was saved! #{to_gid.to_s}"]
      end
    end
    
    # app/controllers/posts_controller.rb
    class PostsController < ApplicationController
      def create
        @post = Post.find params[:id]
    
        if @post.update post_params
          # emit a message in the browser conosle for anyone subscribed to this post
          @post.broadcast_invoke "console.log", args: ["Post was saved! #{to_gid.to_s}"]
    
          # broadcast with a background job
          @post.broadcast_invoke_later "console.log", args: ["Post was saved! #{to_gid.to_s}"]
    
          # you can also broadcast directly from the channel
          Turbo::StreamsChannel.broadcast_invoke_to @post, "console.log",
            args: ["Post was saved! #{@post.to_gid.to_s}"]
    
          # broadcast with a background job
          Turbo::StreamsChannel.broadcast_invoke_later_to @post, "console.log",
            args: ["Post was saved! #{@post.to_gid.to_s}"]
        end
      end
    end
    

📘 NOTE: Method Chaining is not currently supported when broadcasting.

Background Job Queues

You may want to change the queue name for Turbo Stream background jobs in order to isolate, prioritize, and scale the workers independently.

# config/initializers/turbo_streams.rb
Turbo::Streams::BroadcastJob.queue_name = :turbo_streams
TurboReady::BroadcastInvokeJob.queue_name = :turbo_streams

FAQ

  • Isn't this just RJS?

No. But, perhaps it could be considered RJS's "modern" spirtual successor. 🤷‍♂️ Though it embraces JavaScript instead of trying to avoid it.

  • Does it use eval?

No. TurboReady can only invoke existing functions on the client. It's not a carte blanche invitation to emit free-form JavaScript to be evaluated on the client.

A Word of Warning

TurboReady is a foundational tool designed to help you build modern, maintainable, and scalable reactive web apps with Hotwire. It allows you to break free from the strict CRUD/REST conventions that Rails and Hotwire wisely encourage. You should consider TurboReady a substrate for building additional libraries and abstractions.

Please don't use TurboReady to manually orchestrate micro DOM updates (from the server). Such techniques are what gave rise to Full Stack Frontend and sent the industry on a decade long journey of complexity and frustration.

Community

Discord

Please join nearly 2000 of us on Discord for support getting started, as well as active discussions around Rails, Hotwire, Stimulus, Turbo (Drive, Frames, Streams), TurboReady, CableReady, StimulusReflex, ViewComponent, Phlex, and more.

Discord

Be sure to introduce yourselves in the #newcomers channel!

Discussions

Feel free to add to the conversation here on GitHub Discussions.

GitHub Discussions

Twitter

Connect with the core team on Twitter.

Twitter Follow

Releasing

  1. Run yarn and bundle to pick up the latest
  2. Bump version number at lib/turbo_ready/version.rb. Pre-release versions use .preN
  3. Run rake build and yarn build
  4. Run bin/standardize
  5. Commit and push changes to GitHub
  6. Run rake release
  7. Run yarn publish --no-git-tag-version
  8. Yarn will prompt you for the new version. Pre-release versions use -preN
  9. Commit and push changes to GitHub
  10. Create a new release on GitHub (here) and generate the changelog for the stable release for it

License

The gem is available as open source under the terms of the MIT License.