Turbo comes with turbo:before-stream-render
but unfortunately doesn’t ship with the equivalent turbo:after-stream-render
. Here’s how to run JavaScript after the stream renders.
Why we need this
If you are building your application with Hotwire, your Turbo streams will likely add, remove, and replace some HTML nodes. This mostly works except when you want to add HTML that comes with some JavaScript. Like a file picker, Trix editor, and the like.
Turbo itself won’t do anything about this. It’s a rather simple tool with simple purpose. JavaScript initialization should come with the HTML Turbo is about to add. Hotwire solves this with Stimulus.
The Hotwire way
The Hotwire answer to the problem is Stimulus controllers. Stimulus is build on top of MutationObserver which is a browser API providing the ability to watch for changes being made to the DOM.
When a Stimulus controller appears on the page, its connect
method is called automatically. If our HTML doesn’t come with a Stimulus controller, we should create a new controller and put our initialization code inside its connect
method:
// app/javascript/controllers/my_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
// Code to initialize something
}
}
Then we simply add the controller to an element inside our Turbo Stream:
<turbo-stream>
<div data-controller="my-controller">
Something that needs to be initialize with JavaScript.
</div>
</turbo-stream>
When Stimulus is not an option
Sometimes Stimulus might not be what we want and we would love to have a Turbo Stream event to hook into anyways.
Luckily, Steve shared his little implementation of turbo:after-stream-render
in this thread:
// application.js
const afterRenderEvent = new Event("turbo:after-stream-render");
addEventListener("turbo:before-stream-render", (event) => {
const originalRender = event.detail.render
event.detail.render = function (streamElement) {
originalRender(streamElement)
document.dispatchEvent(afterRenderEvent);
}
})
As you can see, the idea is quite simple. We create a new custom Event
and add an event listener for turbo:before-stream-render
which already exist. We then run our event after we are done with original rendering.
To use it we create an event listener and paste the JavaScript that needs to run after rendering:
document.addEventListener("turbo:after-stream-render", () => {
console.log("I will run after stream render")
});
Get Test Driving Rails and make your tests faster and easier to maintain.