Event to know a `turbo-stream` has been rendered

Thanks @evenreven, actually Stimulus connect is working well. When a broadcast stream is appended to the DOM, the Stimulus connect() method does fire. (Mostly what has been advised higher in the thread) Though it seems that it fires before the whole stream is downloaded/appended… Then the JS in the connect method() doesn’t do its job properly as some DOM nodes are missing.

events like DOMcontentloaded or tubo:load obviously don’t work. turbo:frame-load does work but only on first page paint, not with the streams.

Then having a hook for a completed stream would be really helpful… Although today I will move the controller outside the stream and play with MutationObserver, there is a chance I can still do what I need.

EDIT: The more I think about, the more I realize it doesn’t probably have to do with Turbo or Stimulus. My problem is probably rather about the paint of the page…

@sam Can you recommend a way to differentiate if a controller was added/connected on page load or later via turbo stream?

1 Like

I just set up Bearer | Infinite scrolling pagination with Rails, Hotwire, and Turbo – however some of my posts require external JS libs like Anime.js (https://codepen.io/favoriteusername/pen/BaPwzRM).

How do I initialize Anime on my posts after they’ve been rendered by the Turbo stream? I tried this but to no avail:

// app/javascripts/application.js
addEventListener("turbo:after-stream-render", (event) => {
  const fallbackToDefaultActions = event.detail.render;

  event.detail.render = function (streamElement) {
    var wave = anime.timeline().add({
      targets: ".wave path",
      strokeDashoffset: [anime.setDashoffset, 0],
      delay: function (el, i) {
        return i * 250;
      },
      easing: "easeInOutSine",
      duration: 600
    });
  };
});
// app/javascript/controllers/infinite_scroll_controller.js:
import ApplicationController from "./application_controller";
import { useIntersection } from "stimulus-use";

import anime from "animejs";

export default class extends ApplicationController {
  static targets = ["button"];

  connect() {
    super.connect();
    useIntersection(this, { element: this.buttonTarget });

    var wave = anime
      .timeline()
      .add({
        targets: ".wave path",
        strokeDashoffset: [anime.setDashoffset, 0],
        delay: function (el, i) {
          return i * 250;
        },
        easing: "easeInOutSine",
        duration: 600
      });
    }
}

Thanks!

I don’t understand where the resistance to adding this event is coming from.

Aside from offering nice parity with turbo:load and turbo:frame-load (and good symmetry with the before-stream-render event), it’s a convenient solution to a real problem: how to init 3rd party libraries that need to hook into your markup.

There are libraries which already listen to turbo:load and turbo:frame-load in order to do whatever init they need, but without a similar event for when a stream has completed render, these libraries are incomplete.

The world has moved on since then, and we now have MutationObserver to detect changes to the DOM

Fine, then why not remove all events? No need for turbo:load or turbo:frame-load or probably a bunch of the other ones.

Is it really so much trouble to emit these events, to build a library that is easy to work with in various ways? Does it add a ton of weight to turbo or create some sort of maintenance nightmare? Help me understand!

You can put a controller on the element and implement the disconnect() method to do something when it’s removed from the page.

I suppose that works for replace and remove, but streams support 7 actions, and the one I’ve used the most is append. Adding a controller for every message just to init 3rd party libraries seems … onerous … plus presents a problem on normal page load—no need to re-init on each connect() during initial page load. But maybe I’m missing something obvious here?

2 Likes

For anyone else who stumbles on this and wishes for this event, you can actually create it yourself! This is what I’ve added to my 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);
  }
})

This introduces a turbo:after-stream-render event so you can re-init your 3rd party libraries after Turbo Stream renders. There’s probably some way to make it even better that I haven’t thought of / haven’t needed yet (eg, by firing it on a more appropriate target or maybe by attaching more data to the event). But for now this serves my needs :smile:

Hope this helps someone else :beers:

4 Likes

I have a specific case where it’s really not possible to add a controller to elements that I need to check after stream is rendered, unless I will force in my team to spend tremendous time to rebuild area where it’s needed.

Yes, world moved forward, but for web apps it’s often not simple to keep up with the pace and in such cases it would help to have supporting solution. For me refusing to add this even seems to counter the idea of progressive enhancement, since I just need to decide whether to use turbo for my project if I can’t adapt it to what I already have.

For what I needed @sdhull’s solution is perfect.

I was content with the “Use a Stimulus controller on the turbo stream response” answer until I hit a need for a generic solution.

In my case, I’m implementing a confirm dialog (using SweetAlert) on turbo stream links (I couldn’t get button_to’s to work). I’m using the Turbo.setConfirmMethod method to handle this. So, I want to be able to close the SweetAlert modal once the turbostream request completes. I’ve currently solved this by adding a Stimulus controller that does this into the turbostream response. I only have a few links currently so its not bad, but definitely is repetitive, puts a burden on other developers to remember this, and (imo) couples concerns.