Closed davidjr82 closed 3 years ago
See this commit to see how to do this https://github.com/hotwired/turbo-rails/commit/5c49e57f3431956818e9e40a7e87f7bcdaf3a0e5
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?
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
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.
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.
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!
Hi! How about this solution?
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>
@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:
Content-Type: text/html; turbo-stream
Status code 200
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!
Thanks @tothda and @davidjr82!
Small update: with Rails 7 I used content type "text/vnd.turbo-stream.html" and it worked perfectly!
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
Here's another use case where this would be handy for GET requests using only frames.
Imagine having:
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.
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?
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.
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>
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.
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.
@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.
Situation:
Question:
Great package. Thanks!