Redirecting the Full Page from a Frame
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe just did something pretty custom. Normally, if you submit a form into a frame, if that frame redirects, the new content will be loaded into the frame only. The URL in the address bar won't change and the rest of the page won't be affected. That's usually what you want!
But sometimes, we do want to navigate the entire page, like in a modal. Or, imagine that you have a sidebar with a form. When you submit and fail validation, you do want that to show in the sidebar. But once the form is successful, you want to navigate the entire window to a confirmation page.
So let's make our frame-redirecting system something that we can use anywhere. Here's the plan: if a turbo-frame
- like the turbo-frame
in _modal.html.twig
- has a data-turbo-form-redirect="true"
attribute - which I totally just invented - then we will redirect the whole page if we detect a redirect in that frame.
<div | |
class="modal fade" | |
tabindex="-1" | |
aria-hidden="true" | |
data-modal-form-target="modal" | |
> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
// ... lines 9 - 14 | |
<turbo-frame | |
class="modal-body" | |
src="{{ modalSrc }}" | |
id="{{ id }}" | |
loading="lazy" | |
data-turbo-form-redirect="true" | |
> | |
{{ modalContent|default('Loading...') }} | |
</turbo-frame> | |
</div> | |
</div> | |
</div> |
Moving Code to turbo-helper
Because this new redirect behavior will be something that will work anywhere on our site, we need to move the logic out of our modal-form
controller and into turbo-helper
where the rest of our global Turbo stuff lives.
Copy the beforeFetchResponse()
method and delete it. Then, in turbo-helper
, paste this at the bottom. Cool.
// ... lines 1 - 3 | |
const TurboHelper = class { | |
// ... lines 5 - 103 | |
beforeFetchResponse(event) { | |
if (!this.modal || !this.modal._isShown) { | |
return; | |
} | |
const fetchResponse = event.detail.fetchResponse; | |
if (fetchResponse.succeeded && fetchResponse.redirected) { | |
event.preventDefault(); | |
Turbo.visit(fetchResponse.location); | |
} | |
} | |
} | |
// ... lines 116 - 118 |
Back in modal-form_controller
, we don't need the disconnect()
method anymore. We're going to register this listener just once inside of turbo-helper
. Copy part of connect()
, delete the rest... and we can also remove the Turbo import.
// ... lines 1 - 3 | |
export default class extends Controller { | |
static targets = ['modal']; | |
modal = null; | |
async openModal(event) { | |
this.modal = new Modal(this.modalTarget); | |
this.modal.show(); | |
} | |
} |
Over in turbo-helper
, go up to the constructor - here it is - and paste. To call the method, pass an arrow function with an event argument and call this.beforeFetchResponse(event)
.
// ... lines 1 - 3 | |
const TurboHelper = class { | |
constructor() { | |
// ... lines 6 - 16 | |
document.addEventListener('turbo:before-fetch-response', (event) => { | |
this.beforeFetchResponse(event); | |
}); | |
// ... line 22 | |
} | |
// ... lines 24 - 114 | |
} | |
// ... lines 116 - 118 |
Finding the "Active" Frame, if any, for a Request
Ok - go back down to that method. This is not going to work yet... because it's still coded to work with a modal. To bring this to life, we need determine three things. One: was the Ajax call redirected? Two: did this navigation happen inside of a Turbo frame? And three: does that frame have the data-turbo-form-redirect
attribute?
Tip
Starting in Turbo 7 RC4 (and so also in the stable Turbo 7), the turbo:before-fetch-response
event
is now passed which element the Ajax call was triggered on, as event.target
. You could use this
to find the "current turbo-frame" via event.target.closest('turbo-frame')
.
The trickiest of these three is actually figuring out if this Ajax call is happening inside of a turbo frame. This event doesn't give us any indication of what initiated the Ajax call - like which link was clicked or which form was submitted. But, we can use a trick. Remember: whenever a frame is loading, turbo gives that frame a busy
attribute. We can use that.
Create a new convenience method called getCurrentFrame()
. This is going to return the turbo-frame
Element that is currently loading or null. And it's as simple as return document.querySelector()
looking for turbo-frame[busy]
.
// ... lines 1 - 3 | |
const TurboHelper = class { | |
// ... lines 5 - 115 | |
getCurrentFrame() { | |
return document.querySelector('turbo-frame[busy]'); | |
} | |
} | |
// ... lines 120 - 122 |
It is theoretically possible that two frames could be loading at the same time. But other than on initial page load if you had multiple lazy frames, I think that's pretty unlikely.
Above, let's use this. Remove all of this modal stuff... and then move the event.preventDefault()
and Turbo.visit()
to the end of the method... because we're going to reverse the if
logic to keep things clean. If the fetchResponse
did not succeed or it's not a redirect, then return and do nothing.
But if the response was successful and was a redirect, we need to see if we are inside of a frame and make sure that the frame has our data attribute. If not this.getCurrentFrame()
, then return and do nothing. And if the current frame does not have .dataset.turboFormRedirect
, also do nothing.
// ... lines 1 - 3 | |
const TurboHelper = class { | |
// ... lines 5 - 103 | |
beforeFetchResponse(event) { | |
const fetchResponse = event.detail.fetchResponse; | |
if (!fetchResponse.succeeded || !fetchResponse.redirected) { | |
return; | |
} | |
if (!this.getCurrentFrame() || !this.getCurrentFrame().dataset.turboFormRedirect) { | |
return; | |
} | |
event.preventDefault(); | |
Turbo.visit(fetchResponse.location); | |
} | |
// ... lines 117 - 120 | |
} | |
// ... lines 122 - 124 |
At this point, we know that the Ajax call did happen inside of a frame with our data
attribute and that the Ajax call did redirect to another page. And so, we prevent the frame from rendering and navigate the entire page.
Let's try it! Refresh, open the modal, fill in some info, submit and... got it! I know that worked because the new product showed up thanks to the Turbo visit.
Yay! But... was that too easy? It... kind of was. There are two annoying bugs that are hiding inside of our new system. Let's add one more turbo frame next that will expose both of them. Don't worry, by the end, we're going to have a beautiful bug-free way to force a frame to navigate the whole page.
Hi, I am sorry if I missed something, but I have taken this course to figure out how to make advanced dialogs using Turbo and Stimulus with Symfony. The solution in the tutorial looks fine, but I have noticed two things.
First, after successfully submitting the form, the server will give you a response with the HTML of the page you are supposed to be redirected. This response contains new data which has been submitted in the form.
Second, if you put any flash messages after the form submission, the messages won't show up because the first response from the first reason is ignored. This response just serves to check if the form is submitted successfully or not.
So, I need help with Turbo to figure out if is there any way to avoid using Turbo visit but try to use the HTML response that the server gives you after the form is submitted and successfully validated. This will be much better because the data on the website will be automatically updated, flash messages will be visible, and the front end doesn't need to fetch any extra data because everything is already delivered as a response immediately after submission.