symfony / ux

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

[LiveComponent] Multiple Turbo Streams in response - how to live component can communicate with Turbo Frames #374

Open sabat24 opened 2 years ago

sabat24 commented 2 years ago

My goal was to update turbo frames and other live components on my site as a result of some live action from another live component. Finally I managed how to do it, but I'm not sure it such behaviour is just a side effect or a correct one. I describe my use case and then I will present my thoughts, doubts and maybe some improvements in live components communication with Turbo Frames.

I develop a bookshop. One of it's feature is to allow clients to configure a product set to buy. Configurator works as a live component (without forms) and re-render the configurator to update prices, display validation errors and add additional products to the list. When client is ready to buy, he need to click the add to cart button. Then configurator validates itself and there are two possible scenarios: there were errors which have to be displayed to the client (re-render the component) or product was added to the cart.

obraz

# site skeleton
<header>
    <div id="cart" data-controller="live"></div>
</header>

<main>
    <div id="configurator" data-controller="live"></div>
</main>

<footer>
    <turbo-frame id="popup">
    </turbo-frame>
</footer>

After adding product to cart 3 things should happen:

  1. configurator should reset itself to the original state (empty inputs, remove prices etc.)
  2. "thank you" pop-up should appear (turbo frame in my case)
  3. the number of items in cart component should be increased

To achieve that I tried to return in live action a simple turbo response

<turbo-stream action="replace" target="popup">
    <template>
         Product was added to cart box
    </template>
</turbo-stream>

When a popup controller (turbo) receives new data it displays pop-up and dispatches an event for cart live component. cart live component listens for that event to update itself with new data (newly added product).

It works fine, but in such case my configurator live component disappears after button add to cart is clicked. It's happening because my response consist only new content for a turbo frame. That stream replaces configurator's html content and then it is handled by turbo and moved to the popup frame.

obraz

Notice that there is no configurator at the background.

A workaround for that is to return multiple turbo stream response and wrap configurator component with a new turbo frame. And because the _executeMorphdom method from live_controller takes only the first node child (const child = template.content.firstChild;) all response have to be wrapped with some kind of root tag.

# updated site skeleton
<header>
    <div id="cart" data-controller="live"></div>
</header>

<main>
    <turbo-frame id="configurator-live"> {# <-- added #}
        <div id="configurator" data-controller="live"></div>
    </turbo-frame> {# <-- added #}
</main>

<footer>
    <turbo-frame id="popup">
    </turbo-frame>
</footer>
# live component response

<div>
    <turbo-stream action="update" target="popup">
        <template>
            Product was added to cart.
        </template>
    </turbo-stream>
    <turbo-stream action="update" target="configurator-live">
        <template>
            New html for live component.
        </template>
    </turbo-stream>
</div>

Final effect

obraz

All in all it works, but wrapping live component with turbo frame is very hacky to me. If there is no other clear way to send response to turbo I would introduce a new tag in live component's response. For example: <live-template>

Then the response could looks like:

<live-stream> {# some root node if needed #}
    <turbo-stream action="update" target="popup"> {# turbo streams - can be more than one #}
        <template>
            Product was added to cart.
        </template>
    </turbo-stream>
    <live-template>{# only one #}
        New html for live component.
    </live-template>
</live-stream>
weaverryan commented 2 years ago

Hey @sabat24!

To achieve that I tried to return in live action a simple turbo response

To make sure I understand, you have a LiveAction inside of a live component and you are returning (via rendering a Twig template) a stream response like this?

<turbo-stream action="replace" target="popup">
    <template>
         Product was added to cart box
    </template>
</turbo-stream>

If so, this is, indeed, not a situation that has been considered before :). Live components are always meant to return their own HTML, not HTML for other parts of your page or a turbo stream. Btw, who is processing the Turbo Stream response that you return? I would not have expected that to happen automatically, even with Turbo active.

In theory, we could support something like this... though I'm not sure the best way. Certainly your <live-stream> idea is logical... but now would have 2 different formats returned for a live component based on the situation: the "normal" one and this one, where it is a combination of the live component HTML and turbo stream HTML. It's logical... just not sure how I feel about it :)

sabat24 commented 2 years ago

To make sure I understand, you have a LiveAction inside of a live component and you are returning (via rendering a Twig template) a stream response like this?

<turbo-stream action="replace" target="popup">
    <template>
         Product was added to cart box
    </template>
</turbo-stream>

Exactly.

If so, this is, indeed, not a situation that has been considered before :). Live components are always meant to return their own HTML, not HTML for other parts of your page or a turbo stream. Btw, who is processing the Turbo Stream response that you return? I would not have expected that to happen automatically, even with Turbo active.

Turbo makes this job out of the box. I've just installed a clear SF5.4 project with latest Turbo (v. 2.20) and Live Component (v. 2.20) bundles to test this behaviour in an isolated environment. I was able to reproduce this behaviour just by returning mentioned above piece of code from my LiveAction

In theory, we could support something like this... though I'm not sure the best way. Certainly your <live-stream> idea is logical... but now would have 2 different formats returned for a live component based on the situation: the "normal" one and this one, where it is a combination of the live component HTML and turbo stream HTML. It's logical... just not sure how I feel about it :)

Basically I agree with you that supporting 2 formats it's not a case which was planned and such behaviour is probably a side effect. However such effect works quite well. For example it's very easy to display some modal or update any part of site using data returned from live component's response triggered by some action. The best part is that I do not have to touch any js file at all.

So all in all my point is to tame that "side effect", tweak it a bit and change into something controlled by you and more planned.

carsonbot commented 2 months ago

Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?

carsonbot commented 1 month ago

Could I get a reply or should I close this?

carsonbot commented 1 month ago

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!