[LiveComponent] JavaScript hooks are not fired for custom controller on mount
Hello ! I believe this issue is the same as #1038, which was never truly resolved. When a component mounts, and you listen to pretty much any event like connect or render:finished, the event is never fired. However, it is registered, as events that are triggered later on are indeed fired correctly. The hooks property of the component also contains the callbacks I gave it.
This is a dead-simple reproduction of my case (I can't share the real code), using the example from the documentation :
import { Controller } from '@hotwired/stimulus';
import { getComponent, type Component } from '@symfony/ux-live-component';
export default class MyController extends Controller<HTMLElement> {
component: Component | null = null;
async initialize() {
this.component = await getComponent(this.element);
this.component.on('render:finished', (component: Component) => {
console.log('render:finished', component);
});
}
}
<ul
class="{{ ('flex ' ~ attributes.render('class')|default)|tailwind_merge }}"
style="gap: {{ gap }}px; {{ attributes.render('style')|default }}"
{{ attributes.defaults({
'data-controller': 'my-controller',
}) }}
>
{# ... #}
</ul>
Nothing gets logged on the first initial render, but updating the model later on (with a simple button for example) works just fine. What I'm suspecting is that there's a race condition happening, where my controller loads after the events are dispatched. I tried removing the lazy load on the controller, or even add the defer attribute on the Live Component, but no success. I'd really like to see this work and possibly control the first render myself (I need to get the screen width of the user and update the component accordingly, ideally showing a loader in the meantime).
Side notes :
- Would it be possible to improve the TypeScript definitions for
onso that it would automatically type the function arguments based on the event ? - The TypeScript definitions for
component.setandcomponent.renderalways return aPromise<default>, however in the documentation, noawaitor.thenis present between the calls, is this intended ?
Could you show what the generated HTML looks like for your component root tag (before any script execution) ?
<ul
class="{{ ('flex ' ~ attributes.render('class')|default)|tailwind_merge }}"
style="gap: {{ gap }}px; {{ attributes.render('style')|default }}"
{{ attributes.defaults({
'data-controller': 'my-controller',
}) }}
>
This is what the root HTML for the component looks like on page load (tried my best to format it well) :
<ul
class="flex container"
style="gap: 26px; "
data-controller="my-controller live"
data-live-name-value="MyComponent"
data-live-url-value="/_components/MyComponent"
id="live-533890787-0"
data-live-props-value="{"items":[],"breakpoints":{"640":{"columns":1,"gap":0},"768":{"columns":2,"gap":20},"1024":{"columns":3,"gap":20},"1280":{"columns":4,"gap":20},"1536":{"columns":5,"gap":20},"1920":{"columns":6,"gap":20}},"columns":1,"gap":26,"@attributes":{"id":"live-533890787-0","data-host-template":"91a8098fefae38ed812f521103703c29","data-embedded-template-index":5338907871,"class":"container"},"@checksum":"fxEa5BhRmxntsHDcybMGUTFYorJJdMLF8XygQhG9bG8="}"
data-live-csrf-value="97a28.xN4WNxOIF1NHpxcaJ8oCgCBPtr1Q92YKumlfzUP6hZU._Y5_BmHYTX4Gw0BiV5llsVEW9NYSg1RI0lA8t3GQ66eCrVBkQ_F8fmrOUg"
>
Ok i was not sure if your data-controller would coexist will the live one.
Nothing gets logged on the first initial render
That is expected, the render events are only dispatched on fetch/xhr requests, as the first render is done with the full HTML response
I did not succeeded with the connect event, but depending on what you want to do you can simply listen to the connect event directly on your stimulus controller, and from then find the component i guess.
You can also try to use a wrapper component instead of the same node, maybe the race condition comes from this.
For me, the following events worked as excepected, but i agree there is something weird with the connect :)
@smnandre Thank you ! I'll try to experiment a bit more with the connect event. But shouldn't the render:started and render:finished events be triggered if my component is lazy loaded (via lazy or defer) ? Is it even possible to modify the state directly before the first render that way ?
Honestly i don"t know :) As the component changes by nature after the defer/lazy render, it's very possible the event listener is disconnected.
But "no", i don't think we will offer the possibility to intercept those render, because the idea was in fact to offer "the same rendering, just after the page load / intersect" ..
WDYT ?
Regarding the TS things, would you be ok to open a PR ?
I think it makes sense to keep the same logic as a traditional render. For my use case, since I don't want to show anything before the screen width is known, I would probably have to control the loading state manually instead. Which is not necessarily a bad thing, it just requires an additional prop.
And sure, I'll make a PR as soon as I can !
Concerning the whole await thing, I think there's definitely something wrong here. The TypeScript definitions indicate that a Promise is returned from each of the component's methods, which implies that they should be awaited (this is an ESLint rule I have setup). Yet, awaiting them does nothing, the component never renders, because this creates a loop where set waits for the render promise to be fired, but it won't be fired because render cannot be called. This change was introduced with this commit, but I'm not sure why.
// This works
this.component.set('columns', columns);
this.component.render();
// This doesn't
await this.component.set('columns', columns);
await this.component.render();
Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?
@carsonbot I'd say partially
Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?
Just a quick reminder to make a comment on this. If I don't hear anything I'll close this.
Hey,
I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!
Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?