Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace two different turbo-frame from one response #56

Closed
davidjr82 opened this issue Dec 29, 2020 · 17 comments
Closed

Replace two different turbo-frame from one response #56

davidjr82 opened this issue Dec 29, 2020 · 17 comments

Comments

@davidjr82
Copy link

davidjr82 commented Dec 29, 2020

Situation:

  • There is a profile form where the user can update his own name.
  • There is, in the same page, a navigation bar where the username is shown.
  • When the user submit the profile form, the response is just the turbo-frame with the profile form and a "saved" message.
  • I want to return, in the same response, another turbo-frame with the navigation username updated (so it is updated in the navigation bar).
  • When I tried the two turbo-frames in the same response, this won't update the navigation username.

Question:

  • Is there anyway to do this without turbo-streams? Can 2 turbo-frames be returned in the same response that updates different parts of the page? If so, how?
  • If is not possible right now, would be possible in the future to write a comma-separated string in the data-turbo-frame attribute in forms, so we can replace two different turbo-frames in the page? (in the reference, there is a way where the form can replace the navigation turbo-frame, but then the self turbo-frame can not be replaced).

Great package. Thanks!

@davidjr82 davidjr82 changed the title Replace two diffrente turbo-frame from response Replace two different turbo-frame from response Dec 29, 2020
@davidjr82 davidjr82 changed the title Replace two different turbo-frame from response Replace two different turbo-frame from one response Dec 29, 2020
@robzolkos
Copy link

See this commit to see how to do this hotwired/turbo-rails@5c49e57

@davidjr82
Copy link
Author

Thanks @robzolkos

I am not using ruby, but this looks to me like it is related to turbo-streams, returning different responses depending on the existence of a template, and not related to turbo-frames, but maybe I am wrong and not understanding the PR. What I want is to return 2 turbo-frames in the same response, not to return one or the other.

Is that what the PR does and I am misunderstanding it?

@davidjr82
Copy link
Author

By the way, here is a discussion on the hotwire discuss, of someone trying to achieve the same thing:

https://discuss.hotwire.dev/t/loading-two-frames-from-a-single-application-visit-event/1556

@Intrepidd
Copy link
Contributor

As far as I understand, frames are for scoped navigation. If you want to replace 2 parts of your page at once after a form submit (or from websocket) you need to use turbo streams to replace both those elements.

If the only thing you do with those blocks is bulk replace them and they don't have their own navigation context, you don't even need to use frames, turbo streams just replace from DOM id.

@dhh
Copy link
Member

dhh commented Dec 30, 2020

If you want to replace multiple frames, you can also just target _top and replace the whole thing via drive. But otherwise there won't be a path to custom replace two frames. When you need that, you gotta go to turbo streams.

@dhh dhh closed this as completed Dec 30, 2020
@davidjr82
Copy link
Author

Yes, finally I am targeting _top to do it. The downside of this is that, as a redirect is needed to use _top, the whole page can make heavy operations (not my current case), and targeting _top would be unnecessarily slow because that parts of the page wouldn't change.

Using streams could avoid that, but it feels a little bit overcomplicated to me just to replace two frames. That is why I thought that maybe there would be an easy way where two frames can be used in a form response.

Thanks and good work!

@tothda
Copy link

tothda commented Jan 3, 2021

Hi! How about this solution?

  1. A form submitted from a turbo-frame
  2. The server applies the change and sends a redirect back to the browser
  3. Browser (turbo) calls the redirect's location
  4. In the result of this GET request I simply render two turbo stream fragments. The first one replaces the turbo-frame which was the source of the form submission. The second replaces any other element on the page. Note: the response content type has to be Content-Type: text/html; turbo-stream
<turbo-stream action="replace" target="order_form_5">
    <template>
        <turbo-frame id="order_form_5">
            <div class="py-8 border-b border-black">
            	Content of order form
            </div>
        </turbo-frame>
    </template>
</turbo-stream>
<turbo-stream action="replace" target="welcome">
    <template>
        <h1 id="welcome">i am changed</h1>
    </template>
</turbo-stream>

@davidjr82
Copy link
Author

@tothda fantastic! it works!

In fact, it's even simpler. There is no need to redirect to another location that loads the streams. Just return a response from the form submitted with turbo-frame, with:

  1. Content-Type: text/html; turbo-stream
  2. Status code 200
  3. All the streams you want to replace, with the formatting that you have written in your message

And all contents will be replaced without any kind of async calls.

@dhh I think this is a common case (example: the user name changes in the navbar dropdown when the user updates his profile name). Maybe it makes sense to add a little note in the turbo-frame/turbo-stream doc? Do you think it's a good idea?

Thanks to all!

@vivid
Copy link

vivid commented Dec 15, 2022

Thanks @tothda and @davidjr82!

Small update: with Rails 7 I used content type "text/vnd.turbo-stream.html" and it worked perfectly!

@adnen-chouibi
Copy link

For new searchers, how To target multiple elements with a single action, use the targets attribute with a CSS query selector
https://turbo.hotwired.dev/reference/streams#targeting-multiple-elements

@nickjj
Copy link

nickjj commented May 16, 2023

Here's another use case where this would be handy for GET requests using only frames.

Imagine having:

  • A video player (frame)
  • A table of contents sidebar to choose which video to play
  • A list of links under the video player (frame)

When navigating to different videos using the table of contents, both the video frame and links frame would get updated.

Using _top here to break out and use drive is painful because rendering the TOC is heavy.

@seanabrahams
Copy link

seanabrahams commented Jun 9, 2023

There can be an infinite number of scenarios where having a single turbo-frame generated request update multiple frames in its response is desirable.

Using data-turbo-frame="_top" results in a terrible experience when the page update is complicated and results in content shifting around during the repaint and potentially losing scroll position.

The scenario that brought me here is that we have a system where we allow access to edit the view templates but not the backend logic. There are workarounds to do things with turbo-streams but it would be much simpler to allow a view author to just list the elements to be replaced: data-turbo-frame="_self nav #toc" to replace the <nav> and <div id="toc"> elements in addition to the <turbo-frame> element that initiated the request. Or, if <nav> and <div id="toc"> need to be <turbo-frame> elements, then data-turbo-frame="_self nav toc" and <turbo-frame id="nav"> and <turbo-frame id="toc">.

What are the compelling reasons to not support this use case?

@onEXHovia
Copy link

Another solution to the problem could be to custom renderer. Use only one frame per page and exclude blocks you don't want to update.

<html>
    <head>
        <title>Example</title>
    </head>
    <body>
        <turbo-frame id="detail-page" data-turbo-action="advance" data-turbo-marphdom="true">
            <div class="row">
                <div class="col-12 col-lg-4 col-xl-3" data-morphdom-ignore>
                    Not update aside col
                </div>
                <div class="col-12 col-lg-8 col-xl-9">
                    <a href="/link" data-turbo="true">Link 1</a>
                    <a href="/link2" data-turbo="true">Link 2</a>
                </div>
            </div>
        </turbo-frame>
    </body>
</html>
import morphdom from 'morphdom'

document.addEventListener('turbo:before-frame-render', (event) => {
  const { target } = event;
  if (!target.hasAttribute('data-turbo-morphdom')) {
    return;
  }
  
  event.detail.render = (fromNode, toNode) => {
    morphdom(fromNode, toNode, {
      onBeforeElUpdated: (fromEl, toEl) => {
        if (fromEl.isEqualNode(toEl)) {
          return false
        }

        return !fromEl.hasAttribute('data-morphdom-ignore');
      },
    });
  };
});

Sure, you can set up nested frames on the page in the same way.

@cryptogopher
Copy link

This may or may not solve the problem for you, but there is one more option that is not obvious nor seem to be documented. You can nest turbo streams inside turbo frame like this:

<turbo-frame id="some_frame">
  <turbo-stream action="update" target="element1">
    ...
  </turbo-stream>

  <turbo-stream action="update" target="element2">
    ...
  </turbo-stream>
</turbo-frame>

@krschacht
Copy link
Contributor

I'm struggling with this same issue of wanting to update two frames in response to a single click. My situation is much like @nickjj described with his video player example.

Turbo 8 creates a possible solution to this problem. You can target _top and if rails determines this is a Page Refresh then it will use page morphing and smartly update only the parts of the page that changed, but there can be many separate parts sprinkled throughout the page within different frames. This new behavior is explained pretty well in this article: https://jonsully.net/blog/turbo-8-page-refreshes-morphing-explained-at-length

But rails only does a Page Refresh morph when you have a route such as /videos and clicking a button POSTs to a route such as /videos/123 then redirects back to /videos. But if you instead have a route such as /videos which has a link that does a GET that targets _top, rails decides to do a full page Turbo Drive refresh instead.

I "solved" this issue by turning my link into a POST which then does a redirect back to my original URL, but this does not feel like the right solution. It feels dirty and hacky and caused me to introduce a new route. I'm still investigating if the new Page Refresh action can be explicitly invoked rather than implicitly being triggered by comparing the URLs. It feels like we should just be able to put data-turbo-action="morph" on an element to explicitly trigger it, but it does not appear to work this way.

@nickjj
Copy link

nickjj commented Jan 29, 2024

Thanks @krschacht. I'm not sure if Turbo 8 will have a solution in the end because the page morphing happens client side right?

If that's the case then the expensive queries and view rendering will happen server side to generate the full HTML payload to send back to the client where it will be diff'd. This includes expensive table of contents rendering, questions and answers and other content that could exist on the page in addition to the video frame.

Using individual frames and streams allows you to bypass having to render those other expensive areas of the page because on the server it will only render what needs to be rendered in the frame or stream (in this case the video player).

Unless I'm drastically misunderstanding how Turbo 8 works? It's one of those things where I really want to use it to simplify everything but I'm not sure I can due to the efficiency loss.

@krschacht
Copy link
Contributor

krschacht commented Jan 29, 2024

@nickjj It does still render server side and morph on the client, but when you follow the full Rails Turbo playbook, I think this isn't an issue in practice.

At the end of the day, full page refreshes always end up fast through two techniques: caching and decomposition through frames. (1) There is such a heavy incentive to make the full page render fast and rails caching provides multiple ways to address this. So if the concern over morphing is that the full page render is expensive then your issue is likely elsewhere—we generally need to make the full page render fast, regardless. (2) In the cases where there is still a challenge on full page load, you throw the offending section into a turbo frame. Not only does this facilitate caching, but you can then load the frame in parallel/async so that the rest of the page doesn't block on it. You can also put a data-turbo-permanent and/or refresh=morph on your frame. These two additions to your frame are golden. First, you trigger a full page reload and the permanent tag ensures this frame won't be re-requested. This sounds like the solution to your expensive table-of-contents situation. Second, in the rare case when you do want the frame to be reloaded without reloading the full page, you trigger that explicitly with javascript and having refresh=morph will even do morphing of the newly updated frame.

I'm still piecing all this together, but overall Turbo 8 does bring the right new tools for replacing multiple turbo-frames in one response. I'm still just trying to figure out how to trigger full page morphs without having to redirect back to the original page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests