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

The turbo:load event does not emit when the response contains a turbo-stream, which maybe is correct, since it is not a complete page load (not 100% sure).

However, I am not able to find any event that notifies me when a turbo stream has been rendered to the DOM, so that I can perform some DOM related stuff on that node

2 Likes

turbo:before-stream-render. :slight_smile:

(PR to get the new template included in the event.)

I am looking for after. Coz I want to select the DOM nodes from this new stream HTML. :slight_smile:

I believe the newTemplate will just be a raw string. What I want is to write document.querySelector('some-node-from-stream-html')

2 Likes

Ah! Sorry, misread. Could submit a PR for a new one?

The newTemplate is a DocumentFragment. :slight_smile:

1 Like

No worries :). Will create a PR, just waiting to see if the team wants it in or not

1 Like

There’s no built-in event for notification after a Turbo Streams render.

Instead, add a Stimulus data-controller attribute to the element you render inside the <turbo-stream> element. Then implement the connect() method on that controller to know when the element has been loaded into the document.

6 Likes

Having this as a short term solution is fine. But doesn’t seems like a perfect approach.

On a side note, is there any specific reason for not dispatching an event ?

1 Like

The specific reason is to encourage you to design your application not to care about where new HTML comes from or how it gets into the document :blush:

2 Likes

I thought of having this as a companion to the turbo:load event, but for streams. All the use cases that turbo:load solves for a regular visit, this non-existing event will solve for streams.

Maybe I am missing something obvious here. But this is my primary motivation

2 Likes

The turbo:load event dates all the way back to 2012 when it was called page:load. We used it as a way to initialize jQuery plugins and other JavaScript behavior when the page changed.

The world has moved on since then, and we now have MutationObserver to detect changes to the DOM, and libraries like Stimulus that present a nice API on top. That’s what we’re designing for.

3 Likes

This is interesting, I too was looking for the event listeners approach, but I’m curious on how I can achieve this with stimulus. Is there a way with stimulus to react to a child node being removed?

1 Like

@perezperret, you could make the child that may disappear a target and then check for the presence of the target in an action.

If you want to constantly be monitoring for changes to children in the DOM tree, you likely need a custom mutation observer with childList set to true.

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

https://stimulus.hotwire.dev/reference/lifecycle-callbacks

2 Likes

Is there any documentation on how to use that API? I can’t seem to find any docs on how to use them (@stimulus/mutation-observers).

1 Like

To be clear, the API I’m referring to is the set of Lifecycle Callbacks available to Stimulus controllers. The @stimulus/mutation-observers package is how that’s implemented.

All, this seems fine it you are using StimulusJS as your framework. But that’s not the case for us. We are in the process of implementing Hotwire for asynchronous UI updates in an existing application.

Our existing Javascript code has a hook that needs to be ran whenever new content is inserted into the page. It looks for matching DOM entries, and enables the JS needed for the things like date pickers, fancier select controls etc.

All we need to hook this up is an event that is fired after a stream is rendered. Ideally with the DOM element on the newly inserted content, but even that isn’t strictly necessary.

From what’s been said it sounds like the proposed solution would mean that we would have to rewrite all our existing JS code to use stimulus controllers, and that’s probably not tenable.

Is there any chance you could reconsider this event?

If not I’m not sure what other options are available to us (maintaining a fork maybe?)

3 Likes

What is the solution if we don’t want to use Stimulus?

@samstickland Why wouldn’t it be tenable to rewrite to Stimulus? It sounds like your JS is structured in such a way - DOM manipulation of nodes created after DOMContentLoaded - that Stimulus would be a match made in heaven. Sure, every migration to new tech takes time and testing, but you need to fix your javascript anyway. It’s also very easy to implement - you can probably keep most of your code and wrap it in the connect() { JS CODE HERE } method on a per-controller basis. Just add a data-controller="dropdown" to the DOM node in question, and put your code in a dropdown_controller.js and you won’t need an event at all.

If you really don’t want to add Stimulus to your project for this, it’s not like Stimulus is doing anything truly magic here. You can observe the relevant part of the DOM with your own Mutation Observer code and run the code on newly created elements when they appear in the DOM. Here’s a nice vanilla JS example I found through twenty seconds of googling. This is similar to what Stimulus is doing under the hood.

(Also, nitpick: you say you’re implementing Hotwire, but Hotwire includes Stimulus. You probably mean Turbo.)

@n-studio One solution is using Mutation Observers directly, see above.

1 Like

I quite have the same issue except it has a little quirk.

Basically I have a turbo-frame updated through a turbo Stream with a Stimulus controller attached to the first DOM element of this Turbo-frame. (mostly the debate of this thread but as I have Stimulus I thought I was covered)

Everything is fine, the Stimulus controller fires each time the frame is replaced with a broadcast.

Now I have a little problem because the Stimulus logic that lies in the connect method is about manipulationg the DOM just appended. And it seems that connect() fires before the full frame is loaded. (It is quite a long bit of HTML, with images etc…). Then basically I end up with flaky JS: sometimes the HTML is rendered properly (frame fully loaded before Stimulus connect starts ??), sometimes not.

I have found a very hacky and unreliable way: sleep 50 milliseconds in connect() before firing the function that modifies the content. Though it may work for certain visitors but those with a slow bandwith may still have the problem.

Otherwise I end up in the same situation as above: no hook seems to work inside my connect() method… Would someone have an idea? I have mostly been playing with the hooks , but I am thinking having my stimulus controller outside the turbo-frame and add a mutation observer… Though I am not sure at which point the mutation observer will fire as it is quite an expensive JS bit and can’t afford to have it triggered multiple times before the HTML is fully loaded …

EDIT: by no hook work, I mean no hook that satisfies detecting the Turbo stream is rendered / loaded…

1 Like

Without code this is hard to answer, but isn’t the connect lifecycle callback meant to run when there’s a change to the DOM? So if the DOM manipulation method you’re running is idempotent, it shouldn’t really matter if it fires twice?

There is also frame specific events you might be able to use to trigger the controller method instead of the generic connect callback. There’s a turbo:frame-load event, for instance. Might not work in your case since it’s broadcast from a stream, not sure.