
Security News
Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.
@turbo-boost/streams
Advanced tools
TurboBoost Streams extends Turbo Streams to give you full control of the browser's Document Object Model (DOM).
turbo_stream.invoke "console.log", args: ["Hello World!"]
That's right!
You can invoke any DOM method on the client with Turbo Streams.
Turbo Streams intentionally restrict official actions to CRUD-related activity. These official actions work well for a considerable number of use cases. We recommend that you push Turbo Streams as far as possible before reaching for boosted streams.
If you find that CRUD isn't enough, boosted streams are there to handle pretty much everything else.
Proudly sponsored by
Come join the party with over 2200+ like-minded friendly Rails/Hotwire enthusiasts on our Discord server.
>= 6.1>= 1.1>= 7.2Be sure to install the same version for each library.
bundle add "turbo_boost-streams --version VERSION"
npm install "@turbo-boost/streams@VERSION"
Import and initialize Turbo Boost Streams in your application.
# Gemfile
gem "turbo-rails", ">= 1.1", "< 2"
+gem "turbo_boost-streams", "~> VERSION"
# package.json
"dependencies": {
"@hotwired/turbo-rails": ">=7.2",
+ "@turbo-boost/streams": "^VERSION"
# app/javascript/application.js
import '@hotwired/turbo-rails'
+import '@turbo-boost/streams'
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!"]
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
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
You can morph elements with the morph method.
turbo_stream.invoke(:morph, args: [render("path/to/partial")], selector: "#my-element")
[!NOTE] TurboBoost Streams uses Idiomorph for morphing.
The following options are used to morph elements.
{
morphStyle: 'outerHTML',
ignoreActiveValue: true,
head: { style: 'merge' },
callbacks: { beforeNodeMorphed: (oldNode, _) => ... }
}
[!TIP] The callbacks honor the
data-turbo-permanentattribute and is aware of the Trix editor.
The morph method is also exported to the TurboBoost.Streams global and is available for client side morphing.
TurboBoost.Streams.morph.method // → function(targetNode, htmlString, options = {})
You can also override the morph method if desired.
TurboBoost.Streams.morph.method = (targetNode, htmlString, options = {}) => {
// your custom implementation
}
It also support adding a delay before morphing is performed.
TurboBoost.Streams.morph.delay = 50 // → 50ms
[!TIP] Complex test suites may require a delay to ensure the DOM is ready before morphing.
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
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 = { coolStuff: (arg) => { ... } }
# Ruby on the server
turbo_stream.invoke "MyNamespace.coolStuff", args: ["Hello World!"]
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
selectoris 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.
You can also broadcast DOM invocations to subscribed users.
First, setup the stream subscription.
<!-- app/views/posts/show.html.erb -->
<%= turbo_stream_from @post %>
<!-- |
|- *streamables - model(s), string(s), etc...
-->
Then, broadcast to the subscription.
# app/models/post.rb
class Post < ApplicationRecord
after_save do
# emit a message in the browser console for anyone subscribed to this post
broadcast_invoke "console.log", args: ["Post was saved! #{to_gid}"]
# broadcast with a background job
broadcast_invoke_later "console.log", args: ["Post was saved! #{to_gid}"]
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 console for anyone subscribed to this post
@post.broadcast_invoke "console.log", args: ["Post was saved! #{@post.to_gid}"]
# broadcast with a background job
@post.broadcast_invoke_later "console.log", args: ["Post was saved! #{@postto_gid}"]
# you can also broadcast directly from the channel
Turbo::StreamsChannel.broadcast_invoke_to @post, "console.log",
args: ["Post was saved! #{@post.to_gid}"]
# broadcast with a background job
Turbo::StreamsChannel.broadcast_invoke_later_to @post, "console.log",
args: ["Post was saved! #{@post.to_gid}"]
end
end
end
📘 NOTE: Method Chaining is not currently supported when broadcasting.
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
TurboBoost::Streams::BroadcastInvokeJob.queue_name = :turbo_streams
Isn't this just RJS?
No. But, perhaps it could be considered RJS's "modern" spiritual successor. 🤷♂️ Though it embraces JavaScript instead of trying to avoid it.
Does it use eval?
No. The
invokestream can only execute existing functions on the client. It's not a carte blanche invitation to emit free-form JavaScript to be evaluated on the client.
TurboBoost Streams 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 boosted streams a substrate for building additional libraries and abstractions.
Please don't use TurboBoost Streams 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.
This project supports a fully Dockerized development experience.
Simply run the following commands to get started.
git clone -o github https://github.com/hopsoft/turbo_boost-streams.git
cd turbo_boost-streams
docker compose up -d # start the environment (will take a few minutes on 1st run)
docker exec -it turbo_boost-streams-web rake # run the test suite
open http://localhost:3000 # open the `test/dummy` app in a browser
And, if you're using the containers gem (WIP).
containers up # start the environment (will take a few minutes on 1st run)
containers rake # run the test suite
open http://localhost:3000 # open the `test/dummy` app in a browser
Edit files using your preferred tools on the host machine.
That's it!
This project supports Dockerized deployment via the same configuration used for development,
and... it actually runs the test/dummy application in "production". 🤯
The test/dummy app serves the following purposes.
You can see it in action here. How's that for innovative simplicity?
fly deploy
[!TIP] Run these commands on the host machine (i.e. not inside the dev container)
npm update and bundle update to pick up the latest dependencieslib/turbo_boost/streams/version.rb - pre-release versions should use .preNapp/javascript/version.js - pre-release versions use -preNpackage.json - pre-release versions use -preNbin/standardizerake buildnpm run buildrake releasenpm publish --access publicTurboBoost is a suite of libraries that enhance Rails, Hotwire, and Turbo... making them even more powerful and boosting your productivity. Be sure to check out all of the various libraries.
These libraries are available as open source under the terms of the MIT License.
FAQs
Take full control of the DOM with Turbo Streams
We found that @turbo-boost/streams demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.