This is "new" in Turbo v7.3.0 (turbo-rails v1.4.0), redirect behavior when frame is missing is reverted to the original way of forever staying in a frame.
There is a turbo:frame-missing
event emitted so that you could customize this behavior:
turbo:frame-missing
- fires when the response to a <turbo-frame>
element request does not contain a matching <turbo-frame>
element.
By default, Turbo writes an informational message into the frame and
throws an exception. Cancel this event to override this handling. You
can access the Response
instance with event.detail.response
, and
perform a visit by calling event.detail.visit(...)
https://turbo.hotwired.dev/reference/events
Like this:
document.addEventListener("turbo:frame-missing", (event) => {
const { detail: { response, visit } } = event;
event.preventDefault();
visit(response.url);
});
While this works, it does one more request to the server, which is what used to happen way back.
If you want to just display the redirected response, you can visit
the response
:
document.addEventListener("turbo:frame-missing", (event) => {
const { detail: { response, visit } } = event;
event.preventDefault();
visit(response); // you have to render your "application" layout for this
});
Turbo frame requests used to render without a layout, they now render within a tiny layout. response
has to be a full page response to be visitable, otherwise, turbo will refresh the page, which makes it even worse. This would fix it:
def show
render layout: "application"
end
Custom turbo stream redirect solution:
https://stackoverflow.com/a/75750578/207090
I think it's simpler than the solution below.
Set a custom header
This lets you choose on the front end if and when you want to break out of the frame.
Set a data attribute with a controller action name, like data-missing="controller_action"
(or any other trigger you need, like, controller name as well):
<%= turbo_frame_tag "form_frame", data: { missing: "show" } do %>
# ^
# this is where missing frame is expected, it's not strictly
# necessary, but it's where "application" layout is required
This was more of a "i wonder if that would work" type of a solution, just make sure you need it:
// app/javascript/application.js
addEventListener("turbo:before-fetch-request", (event) => {
const headers = event.detail.fetchOptions.headers;
// find "#form_frame[data-missing]"
const frame = document.querySelector(`#${headers["Turbo-Frame"]}[data-missing]`);
if (frame) {
// if frame is marked with `data-missing` attribute, send it with request
headers["X-Turbo-Frame-Missing"] = frame.dataset.missing;
}
});
addEventListener("turbo:frame-missing", (event) => {
const { detail: { response, visit } } = event;
if (response.headers.get("X-Turbo-Frame-Missing")) {
// if response has "frame missing" header it can be rendered
// because we'll make sure it's rendered with a full layout
event.preventDefault();
visit(response);
}
});
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
layout -> {
if turbo_frame_request?
# check if we are in an action where a missing frame is
# if you're jumping between controllers, you might need to
# have controller name in this header as well
if request.headers["X-Turbo-Frame-Missing"] == action_name
# let `turbo:frame-missing` response handler know it's ok to render it
headers["X-Turbo-Frame-Missing"] = true
# because it's a full page that can be displayed
"application"
else
"turbo_rails/frame"
end
end
}
end
https://turbo.hotwired.dev/handbook/frames#breaking-out-from-a-frame
https://github.com/hotwired/turbo/pull/863