When using turbo-frames all page updates by default are limited to the frame which initiated the request. On frontend you can change target frame by setting data-turbo-frame attribute on links (and use _top to update the whole page). But what if you want to make that decision on backend? 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"
you break this feature. Now you always have a full layout event if you don't' need it. How to do it correctly? Pretty simple, just follow same logic:
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?
add_flash_types registers new flash type, so redirect_to can recognise it
we set proper layout. Tiny one for turbo-frame requests, but not when we are trying to break out.
turbo_frame_breakout? checks the value in flash and removes it (otherwise it could be shown with other messages to the user).
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.