Notes to self

Running JavaScript after a Turbo Stream renders

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")
});
Check out my book
Interested in Ruby on Rails default testing stack? Take Minitest and fixtures for a spin with my latest book.

Get Test Driving Rails and make your tests faster and easier to maintain.

by Josef Strzibny
RSS