Closed matthieumastadenis closed 3 months ago
The markup of the stimulus controller below is correctly updated each time the Live Component re-renders, but the controller is actually never re-rendered (and so the generated table never changes)
Could you precise what you mean by that ? I'm a bit confused
The markup of the stimulus controller below is correctly updated each time the Live Component re-renders, but the controller is actually never re-rendered (and so the generated table never changes)
Could you precise what you mean by that ? I'm a bit confused
Of course. By looking at the code I provided, you can see that the list of users is passed to the stimulus controller with data-UI--datatable-data-value='{{ users|json_encode }}'
. By using the inspector in my browser, I can see that this code is correctly refreshed when the component re-renders, meaning I can see that the array injected into stimulus contains the expected list of users, according to what I typed in the search form. More precisely with the data I use to test the component: by default I have a list of two users, and after searching for a precise username or email with the form, I can see that an array of only one user is now injected.
This is the expected behavior, so I'm satisfied with the "live component" part, which seems to work properly. But somehow that's not enough for the stimulus controller to react to that change. I even put a console.log() in the connect() method of the controller, and I only see the result once in the console (when I load the page for the first time), but never after any re-rendering of the live component.
Could you share your component code ?
Could you share your component code ?
Here it is, it's very simple and uses jspreadsheet-ce :
import { Controller } from '@hotwired/stimulus';
import jspreadsheet from 'jspreadsheet-ce';
import 'jsuites/dist/jsuites.min.css';
import 'jspreadsheet-ce/dist/jspreadsheet.min.css';
import '../../styles/entries/spreadsheets.scss';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static values = {
data: {
type: Array,
default: [],
},
columns: {
type: Array,
default: [],
}
}
static targets = [ 'table' ];
connect() {
// This is only displayed in the console once (after initial page load)
// I expect to see it each time the live component re-renders:
console.log('connected '+this.element.dataset.liveId)
jspreadsheet(this.tableTarget, {
data: this.dataValue,
columns: this.columnsValue,
filters: true,
allowInsertRow:false,
allowManualInsertRow:false,
allowInsertColumn:false,
allowManualInsertColumn:false,
allowDeleteRow:false,
allowDeleteColumn:false,
});
}
}
I found a solution but it's a bit ugly, I still think this should work without such a hack:
1 - I added a has-datatable
attribute on my live component root element:
<div has-datatable {{ attributes }}>
</div>
2 - I modified my stimulus controller so it can find that component using the added attribute, then listen to its render event and then recreate the table:
import { Controller } from '@hotwired/stimulus';
import { getComponent } from '@symfony/ux-live-component';
import jspreadsheet from 'jspreadsheet-ce';
import 'jsuites/dist/jsuites.min.css';
import 'jspreadsheet-ce/dist/jspreadsheet.min.css';
import '../../styles/entries/spreadsheets.scss';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static values = {
data: {
type: Array,
default: [],
},
columns: {
type: Array,
default: [],
}
}
static targets = [ 'table' ];
connect() {
this.spreadsheet = jspreadsheet(this.tableTarget, {
data: this.dataValue,
columns: this.columnsValue,
filters: true,
allowInsertRow:false,
allowManualInsertRow:false,
allowInsertColumn:false,
allowManualInsertColumn:false,
allowDeleteRow:false,
allowDeleteColumn:false,
});
}
async initialize() {
const parentComponent = this.element.closest('[has-datatable]');
if (parentComponent) {
this.component = await getComponent(parentComponent)
this.component.on('render:finished', component => {
if (this.spreadsheet) {
this.spreadsheet.destroy();
this.connect();
}
});
}
}
}
This works, but if anybody knows a simpler solution I'm still interested.
I think you can leverage the value change callbacks here: https://stimulus.hotwired.dev/reference/values#change-callbacks
I think you can leverage the value change callbacks here: https://stimulus.hotwired.dev/reference/values#change-callbacks
Thanks, I just tried this but the callback doesn't seem to be called, I don't know why. Looks like the controller doesn't react at all when the parent component is re-rendered.
Could it be related to the fact that my stimulus controller is actually nested into a child component (the twig:UI:Section
one) ? Yet I added the data-live-id
attribute on it, and as I was saying previously I can see its markup changing...
Not sure if related, but this should be lowercase
- data-UI--datatable-columns-value='{{ [
+ data-ui--datatable-columns-value='{{ [
That beeing said, for stimulus i think your component does not deconnect / connect, as the id is the same.
Not sure if related, but this should be lowercase
- data-UI--datatable-columns-value='{{ [ + data-ui--datatable-columns-value='{{ [
I tried with the lowercase attributes, it doesn't solve my problem but thanks for the advice. I still have to use data-controller="UI--spreadsheet"
for the controller name because it's in a UI
folder in uppercase.
That beeing said, for stimulus i think your component does not deconnect / connect, as the id is the same.
I tried with id="{{ microtime() }}"
on the controller element, but this doen't solve the problem either. The controller never reconnects.
Does it use morphing? Afaik a controller should disconnect and reconnect automatically, but morphing might not remove the whole tag but only replace deltas and if controller doesn't change it remains running as it is?
https://symfony.com/bundles/ux-live-component/current/index.html#overwrite-html-instead-of-morphing
This is the expected behavior, so I'm satisfied with the "live component" part, which seems to work properly. But somehow that's not enough for the stimulus controller to react to that change. I even put a console.log() in the connect() method of the controller, and I only see the result once in the console (when I load the page for the first time), but never after any re-rendering of the live component.
@matthieumastadenis This is the expected behavior. The live components don't reload the embedded stimulus controller. Your stimulus controller is connected on page load once, and then run for ever even if components change. This is one of the main think of stimulus, to force you to write javascrit that is load once and run for ever. This is design like that for performance and maintenance reasons. If you want to update your stimulus controller on your LiveComponent changes you have to listen to events. You can create your own browser events: https://symfony.com/bundles/ux-live-component/current/index.html#dispatching-browser-javascript-events Or listen to already made js events: https://symfony.com/bundles/ux-live-component/current/index.html#working-with-the-component-in-javascript
The UI--datatable
Stimulus controller is not on a live component here.
But any DOM update on its values data attribute (and @matthieumastadenis explained us the DOM was indeed correctly updated with the expected values) should trigger the value change callbacks from Stimulus.
I'm thinking this is something else.
@matthieumastadenis could you create a small / minimal reproducer (a git repository we can just pull and run locally) to better help you ?
Good article from SymfonyCast: https://symfonycasts.com/blog/symfony-reproducer Symfony doc: https://symfony.com/doc/current/contributing/code/reproducer.html
Does it use morphing? Afaik a controller should disconnect and reconnect automatically, but morphing might not remove the whole tag but only replace deltas and if controller doesn't change it remains running as it is?
https://symfony.com/bundles/ux-live-component/current/index.html#overwrite-html-instead-of-morphing
This works actually, thanks @CMH-Benny !
@smnandre if you're still interested by a reproducer I'll do it, but I'm not sure I'll have the time today sorry, it may be only next week
As you want, if you're happy with your working solution there is no need!
@smnandre Sorry for the delay.
Since last time I moved on other things and now it's not easy to find enough time to focus on this issue again. The workaround suggested by @CMH-Benny is good enough for me, so I'm closing this issue.
If I'm stuck with something similar in the future I may open a new issue, in that case I'll add a link to this one. But at the moment everything is good for me.
Thanks again everyone for your help.
We're 2 just today running into this issue in the canal #ux of the Symfony slack. I think this issue should be re-opened to be addressed correctly.
@Nek- let's open a new one, as the code will not be the same and this won't bother @matthieumastadenis with notifications.
You can link to this one in the message if you want to link them :)
Hello,
I have a Live Component containing a search form and a table of results dynamically generated by an embedded stimulus controller.
When I type something in the form, the Live Component is re-rendered as expected, but nothing happens on the child stimulus controller. After reading this issue I added a data-live-id attribute on the controller but it does not solve my problem.
I can see the HTML being succesfully updated by the live component, but the stimulus controller doesn't seem to react to that change at all.
Did I forget something here?