symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony
https://ux.symfony.com/
MIT License
820 stars 297 forks source link

[LiveComponent] Smart rendering problems #781

Closed norkunas closed 1 year ago

norkunas commented 1 year ago

Thanks for bringing this up @norkunas - I figured there would be some real-world hiccups we'd need to work through.

so after rerender checkbox becomes unchecked but the class is still left here even backend returns the element without custom checked class.

That makes sense. Question: BEFORE the new re-rendering system, did you have the opposite problem? When the Stimulus controller added the checked class, when the live component re-rendered (not via the action, just a normal re-render), didn't it NOT include the custom class? And so, didn't the re-render cause the custom class to be lost? Or do you have the "if checked -> add custom class" logic both inside of Twig and also include if your Stimulus controller? And if so, why do you have it in both places? Is it just to make the experience feel faster / more responsive (i.e. as SOON as the user checks the box, the class is added, without needing to wait for the re-render)?

Btw, feel free to open a new issue - I don't want this to get lost on a merged PR - I sometimes catch these notifications, and sometimes not :)

Originally posted by @weaverryan in https://github.com/symfony/ux/issues/728#issuecomment-1503374589

Yes, in backend I use html_classes to apply them based on states. The component was made to work without live components - so for UI to know what color class to apply on checkbox If it's checked I need to expose it to stimulus. So now with in a different part of app it was integrated into a live component and then the problem arose. And yes - it is to make experience feel faster :)

weaverryan commented 1 year ago

I'm thinking the solution is to use browser events - like #794.

Here's the thinking: if you are "managing" the state of something (a check box, modal opened/closed, etc), you should choose to manage that either entirely client-side with custom JS or server-side with LiveComponents. In your situation, since you have Stimulus code to handle things, in your LiveAction, I would dispatch a browser event - e.g. uncheck and maybe even pass the id or color of the one you want unchecked if necessary (my event naming may be poor for your actual situation). Then, update your existing Stimulus controller to listen to this event and do the "uncheck" work needed.

I've done the same thing recently for a modal: I use Bootstrap's native JavaScript on a link to open a modal - no LiveComponents code needed for that part. Then, via a child component that lives in the modal, when I submit to a LiveAction on that child component, it dispatches a browser event which tells a modal Stimulus controller to close that modal. Hence: the modal state is entirely handled client-side.

Thoughts?

norkunas commented 1 year ago

I would dispatch a browser event - e.g. uncheck

Hmm :) that's optimistic. I want stimulus controllers to be agnostic from live components, so in this case checkbox controller listens only for change event and determines based on event.target.checked, so somehow I need to set target.checked = bool, but looking at dispatchBrowserEvent I can pass only event.detail :)

With modals I currently chose an <turbo-stream action="remove"> approach to avoid leaving modal in the DOM, but good to know that this opens possibility for another approach :)

weaverryan commented 1 year ago

Hmm :) that's optimistic. I want stimulus controllers to be agnostic from live components, so in this case checkbox controller listens only for change event and determines based on event.target.checked, so somehow I need to set target.checked = bool, but looking at dispatchBrowserEvent I can pass only event.detail

You're not really binding your Stimulus controllers to live components - you're giving them an extra hook point (e.g. if someone dispatches an uncheck event, you listen to that to do something). However, I realize there may be a hiccup in my logic. If you dispatchBrowserEvent, that'll dispatch on the root component element. So, your custom checkbox Stimulus controller would need to be registered on that same root component element (or a parent), else the DOM event would never bubble to them.

Assuming your Stimulus controller IS on the same element (or a parent) of the live component root element, you could do something like this to pull this off:

// in connect()
this.element.addEventListener('check:uncheck', (event) => {
    // pretend you pass a "value" to dispatchBrowserEvent with the value of the item that should be unchecked
    const targetedValue = event.detail.value;
    const checkbox = this.element.querySelector(`input[type="checkbox"][value="${targetedValue}"]`);
    checkbox.checked = false;
});

Still not the MOST elegant thing - but let me know what you think.

norkunas commented 1 year ago

Thanks, but I will need more time to check this as I'll be off for a week, so will get back to you later.