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