Lazy-load url clashes with Laravel back() and previous()
SylarRuby opened this issue ยท 15 comments
Think I've found a bug-ish? Say you have a lazy-loading section:
<turbo-frame id="my_frame" :src="route('my.page')">
<p>Loading...</p>
</turbo-frame>
After page loads, content is then fetched GET
then replace/update my_frame
. Fine. I have a back button defined with
<a href="{{ url()->previous() }}">Back</a>
When I click the back link, it goes to route('my.page')
's url. The behaviour is correct, in some sense. How to avoid this? By not using url()->previous()
?
EDIT:
This seems to relate to this.
Going to close this for now. I've restarted my app using url()->back()
instead of url()->previous()
and all seems well.
Hey, @SylarRuby
I'll test this out during lunch today.
So, I have a root page /
that renders a simple Blade view:
@extends('layouts.main')
@section('content')
<h1>Main Page</h1>
<x-turbo-frame id="my-frame" src="{{ url('/my-page') }}">
<p>Loading...</p>
</x-turbo-frame>
@stop
The /my-page
route also renders a simple Blade view:
@extends('layouts.main')
@section('content')
<h1>My Page</h1>
<x-turbo-frame id="my-frame">
<div>
<p>This content was loaded from /my-page.</p>
<a href="{{ url()->previous() }}">Back</a>
</div>
</x-turbo-frame>
@stop
When the root page loads, the lazy-loading Turbo Frame will make a GET /my-page
request and replace the #my-frame
Turbo Frame's contents with the one in the /my-page
, which contains a link that uses {{ url()->previous() }}
. The link points to /
in this case - which is the page that makes the request to the /my-page
one.
This is the expected behavior, right?
\cc @SylarRuby and @odion-cloud
In my case:
@extends('layouts.main')
@section('content')
<a href="{{ url()->previous() }}">Back</a>
<h1>Main Page</h1>
<x-turbo-frame id="my-frame" src="{{ url('/my-page') }}">
<p>Loading...</p>
</x-turbo-frame>
@stop
After page loads, with updated content, pressing the back button goes to /my-page
instead of /
. What does work:
@extends('layouts.main')
@section('content')
<a href="{{ redirect()->back()->getTargetUrl() }}">Back</a>
<h1>Main Page</h1>
<p>This content was loaded from /my-page.</p>
@stop
That link is outside the Turbo Frame, rendered in the first-page render. There is no "previous" URL if it was the first page render. So it all checks out.
Laravel keeps track of your most recently visited page in session (every time you make a GET request to the app - see source). The url()->previous()
tries to use the referer header, which should be present when you click on a link to the page, for example. Otherwise, it defaults to using the session one via session()->previousUrl()
(see source), which is what is happening in your case.
So that may be where the confusion lies.
It seems like expected behavior, but I'll see what I can do.
I was able to solve this issue at the application level. To do so, you need to create a new StartSession middleware:
php artisan make:middleware StartSession
Next, add the following content to that middleware:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Session\Middleware\StartSession as Middleware;
class StartSession extends Middleware
{
/**
* Store the current URL for the request if necessary.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Session\Session $session
* @return void
*/
protected function storeCurrentUrl(Request $request, $session)
{
if ($request->isMethod('GET') &&
$request->route() instanceof Route &&
! $request->ajax() &&
! $request->prefetch() &&
! $request->isPrecognitive() &&
! $request->headers->has('Turbo-Frame')) {
$session->setPreviousUrl($request->fullUrl());
}
}
}
Note: the key part here is the ! $request->headers->has('Turbo-Frame')
condition added to the check there, which means Turbo Frame requests won't affect the previous URL.
Now, you need to swap the built-in middleware with the new one in the app/Http/Kernel.php
file (should be in the "web" middleware group:
- \Illuminate\Session\Middleware\StartSession::class,
+ \App\Http\Middleware\StartSession::class,
That should do the trick.
Here's a test if you want to add it to your application
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use Illuminate\Support\Facades\Route;
class StartSessionTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Route::middleware('web')->get('_test/session/home', function () {
return url()->previous();
});
Route::middleware('web')->get('_test/session/my-page', function () {
return 'my-page-content';
});
}
/** @test */
public function it_stores_previous_url_in_session()
{
$this->get('_test/session/my-page')
->assertOk()
->assertSee('my-page-content');
$this->get('_test/session/home')
->assertOk()
->assertSee(url('_test/session/my-page'));
}
/** @test */
public function turbo_frames_dont_record_previous_url()
{
$this->withHeaders(['Turbo-Frame' => 'testing-frame'])
->get('_test/session/my-page')
->assertOk()
->assertSee('my-page-content');
$this->get('_test/session/home')
->assertOk()
->assertSee(url(''))
->assertDontSee(url('_test/session/my-page'));
}
}
I'm gonna close the issue for now. Not sure if this should be pushed upstream to Laravel yet.
\cc @SylarRuby and @odion-cloud
Works great. And thanks for the test ๐ ๐
Hi @tonysm, one little question. I'm on a homepage (/
). I change the browser url, without refreshing using js:
// or window.history.pushState(..)
window.history.replaceState(null, null, "/?foo=2");
Once url is changed, while still on homepage, I navigate to /books/1
. On the show
page, I hit the back button:
<a href="{{ redirect()->back() }}">Back</a>
If this is a turbo question, how to go back to /?foo=2
? Pressing the browser back button, only the url changes to /?foo=2
while still on the show
page. I guess two issues? If this is a SO question, please confirm.
Thanks
What's the reason for changing the URL like that instead of making a Turbo visit? I'd say try using url()->previous()
instead of url()->back()
Good question. We have an options tab than when something is selected, turbo stream is used to replace the table contents:
We also want the url to update to match the selected items as url params. I had to save the new url in localStoarge
then use Turbo.visit
to go back:
import * as Turbo from "@hotwired/turbo";
[..]
const prevUrl = localStorage.getItem('prevUrl')
Turbo.visit(prevUrl, {action: "restore"})
Works great. Turbo is the solution here ๐
@SylarRuby btw, that sounds like something Turbo Frames can help. I recorded a screencast the other day of something similar. The idea is that you would wrap the table with a <turbo-frame data-turbo-action="advance" target="_top" id="my-table">
(docs). I guess above the table you have a form, so that would be outside of the frame but pointing to it like: <form data-turbo-frame="my-table">
(docs) which would trigger the frame to update it, then the data-turbo-action="advance"
would update the URL accordingly.
Oh wow! Yes, sure, I'll have a look. Thanks
@tonysm Heads up, the base storeCurrentUrl
method has changed. There's a new ! $request->isPrecognitive()
check added.
Thanks, @KostasKostogloy. I've updated the comment.