Open acetinick opened 3 years ago
@lukepass But it looks like you can restore this functionality by adding a custom event handler to application.js
:
document.addEventListener("turbo:frame-missing", function (event) {
event.preventDefault()
event.detail.visit(event.detail.response)
})
event.target contains "turbo-frame#modal_inner", so you can check it if needed.
In the meantime, I wrote the render_turbo_stream gem. There are some helpers that make it easier to control the turbo actions in general.
Understood @elik-ru thanks! @chmich unfortunately I'm using Turbo along with Symfont UX but I can work on elik-ru's solution.
I have a very similar use-case as @elik-ru, and https://github.com/hotwired/turbo/pull/863 made this an issue that that is once again very relevant. The inability to specify from the server whether a response should be a full page load causes a lot of problems when dealing with handling failure cases for a form submission when inside a modal, since only the server knows whether the page should redirect or render an error, not the client. It seems there are 2 options at the moment, neither of which is ideal as an end user:
If someone is interested, 204 (no content) status can be used given how rare it is. To jump out of frame, I use this (Symfony):
class TurboRedirectResponse extends Response
{
public function __construct(string $url)
{
parent::__construct(status: 204, headers: ['location' => $url]);
}
}
The important bit, event listener. All these checks for nullability are because I don't want to risk failures and I am terrible with JS:
document.addEventListener('turbo:submit-end', (event) => {
let response = event.detail?.fetchResponse?.response;
let status = response?.status;
let url = response?.headers?.get('Location') ?? null;
if (status === 204 && url) {
Turbo.visit(url, {action: 'advance' }) // this should also have `frame` value, not tested yet
event.preventDefault();
return false;
}
})
And finally controller:
public function doSomething(): Response
{
if ($shouldControllerRedirect) {
return new TurboRedirectResponse($url); // should have target frame as second param here
}
return new Response('<turbo-frame id="test">This is where form is rendered as usual</turbo-frame>');
}
The idea is far from perfect and right now, issues a full visit but that is only because of missing frame
(commented above). But I expect it to work, someone could improve on this idea. I tried to catch 303 in event listener, but it didn't work at all.
I took me some time to understand how to make the workaround work perfectly.
First you must understand one thing about layouts. It you don't specify layout in your controller - turbo-rails does it for you with this piece of code:
layout -> { "turbo_rails/frame" if turbo_frame_request? }
So when doing frame requests it renders special tiny layout, which saves render time and transferred bytes, which is what we want.
But if for some reason you need custom layout and specify it as
layout "admin"
layout -> { turbo_frame_request? ? "turbo_rails/frame" : "admin" }
Now we have custom full layout by default, and tiny layout for turbo-frame requests. Nice.
Next we come to the break-out problem. That proposed solution kinda worked for me, but not ideally:
document.addEventListener("turbo:frame-missing", function(event) {
if (event.detail.response.redirected) {
event.preventDefault()
event.detail.visit(event.detail.response);
}
})
It is detected correctly, but Turbo is making full page reload, doing 2 requests in a row. I was digging around, tried to do Turbo.visit(event.detail.response.url);
, which was working better (without full page reload), but still doing double requests.
I was looking at the code in Turbo and it seems that it was supposed to work, but it was not. Until I realised that thing with layouts. Response was rendered with short layout! So Turbo detects that page head content is different and triggers a full-page reload! That means we must detect somehow this situation and render full layout after a redirect.
So here is the final solution:
class ApplicationController < ActionController::Base
add_flash_types :turbo_breakout
layout -> {
turbo_frame_request? && ! turbo_frame_breakout? ? "turbo_rails/frame" : "application"
}
def turbo_frame_breakout?
flash[:turbo_breakout].present?.tap { flash.delete(:turbo_breakout) }
end
...
def some_action
redirect_to target_path, success: "Congratulations!", turbo_breakout: true
end
end
And same js snippet:
document.addEventListener("turbo:frame-missing", function(event) {
if (event.detail.response.redirected) {
event.preventDefault()
event.detail.visit(event.detail.response);
}
})
So, what we are doing here?
Finally, redirect_to target_path, success: "Congratulations!", turbo_breakout: true
is doing a redirect setting 2 flash messages. One for the user and the other for choosing correct layout.
Last question: what happens if some redirect occurs without our flash message? Well, it still will be working, with that double load and full-page visit, but still working, so I think it's a good fallback for unexpected cases.
I was able to override the frame-target using the meta tag. It's explained here in the docs: https://turbo.hotwired.dev/handbook/frames#%E2%80%9Cbreaking-out%E2%80%9D-from-a-frame
In certain, specific cases, you might want the response to a
request to be treated as a new, full-page navigation instead, effectively “breaking out” of the frame. The classic example of this is when a lost or expired session causes an application to redirect to a login page. In this case, it’s better for Turbo to display that login page rather than treat it as an error. The simplest way to achieve this is to specify that the login page requires a full-page reload, by including the turbo-visit-control meta tag: ...
@krschacht It will make a full-page reload. Meaning 2 requests for the page, and reloading/reevaluating of JS/CSS.
One relatively common subcase of this problem is when you want to break out of the frame and redirect the full page back to itself to refresh it. Since the introduction of morphing we also have the refresh stream action which will always apply itself to the full page.
So, that case can be solved relatively simply by having the controller emit the refresh action when the submission is successful:
render turbo_stream: turbo_stream.action(:refresh, "")
(Not yet a built in action, pending https://github.com/hotwired/turbo-rails/pull/595).
I agree with the sentiments some people expressed here that it doesn't feel right to use stream actions as response to a form submission but at the moment that is probably the simplest, most maintainable, solution.
Currently if the response from turbo requests returns a redirect response whilst inside a frame, there is no way for the server side to conditionally control what turbo to do with the response, eg. force a full page refresh with target="_top"
There are use cases especailly for modal windows where upon succesful save we would want to perform a redirect back to the index page, where as 422 response would do the normal turbo frame replacement within current frame.
It would be nice to support something like this, which will allow us to control targets from serverside to frame targets by setting frame target via the http response headers.
What everyones thoughts are on this? or is there something I am missing to make this easier.