Skip to content

Commit

Permalink
Merge pull request #75 from tonysm/feature/tm-turbo-native-routes
Browse files Browse the repository at this point in the history
Adds Turbo Native Navigation Routes and trait
  • Loading branch information
tonysm authored Jun 19, 2022
2 parents 3937a8c + 693a5e0 commit 41a2e7b
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 4 deletions.
58 changes: 56 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ It's highly recommended reading the [Turbo Handbook](https://turbo.hotwired.dev/
* [Validation Response Redirects](#redirects)
* [Turbo Native](#turbo-native)
* [Testing Helpers](#testing-helpers)
* [Known Issues](#known-issues)
* [Closing Notes](#closing-notes)

<a name="conventions"></a>
Expand Down Expand Up @@ -818,6 +819,55 @@ if (Turbo::isTurboNativeVisit()) {
}
```

#### Interacting With Turbo Native Navigation

Turbo is built to work with native navigation principles and present those alongside what's required for the web. When you have Turbo Native clients running (see the Turbo iOS and Turbo Android projects for details), you can respond to native requests with three dedicated responses: `recede`, `resume`, `refresh`.

You may want to use the provided `InteractsWithTurboNativeNavigation` trait on your controllers like so:

```php
use Tonysm\TurboLaravel\Http\Controllers\Concerns\InteractsWithTurboNativeNavigation;

class TraysController extends Controller
{
use InteractsWithTurboNativeNavigation;

public function store()
{
$tray = /** Create the Tray */;

return $this->recedeOrRedirectTo(route('trays.show', $tray));
}
}
```

In this example, when the request to create trays comes from a Turbo Native request, we're going to redirect to the `turbo_recede_historical_location` URL route instead of the `trays.show` route. However, if the request was made from your web app, we're going to redirect the client to the `trays.show` route.

There are a couple of redirect helpers available:

```php
$this->recedeOrRedirectTo(string $url);
$this->resumeOrRedirectTo(string $url);
$this->refreshOrRedirectTo(string $url);
$this->recedeOrRedirectBack(string $fallbackUrl, array $options = []);
$this->resumeOrRedirectBack(string $fallbackUrl, array $options = []);
$this->refreshOrRedirectBack(string $fallbackUrl, array $options = []);
```

The Turbo Native client should intercept navigations to these special routes and handle them separately. For instance, you may want to close a native modal that was showing a form after its submission and _recede_ to the previous screen dismissing the modal, and not by following the redirect as the web does.

At the time of this writing, there aren't much information on how the mobile clients should interact with these routes. However, I wanted to be able to experiment with them, so I brought them to the package for parity (see this [comment here](https://github.com/hotwired/turbo-rails/issues/78#issuecomment-815897904)).

If you don't want these routes enabled, feel free to disable them in your `config/turbo-laravel.php` file (make sure the Turbo Laravel configs are published):

```php
return [
'features' => [
// Features::turboNativeRoutes(),
],
];
```

<a name="testing-helpers"></a>
### Testing Helpers

Expand Down Expand Up @@ -952,8 +1002,12 @@ class CreatesCommentsTest extends TestCase

*Note: make sure your `turbo-laravel.queue` config key is set to false, otherwise actions may not be dispatched during test because the model observer only fires them after the transaction is commited, which never happens in tests since they run inside a transaction.*

<a name="caveats"></a>
### Fixing Laravel's Previous URL Issue
<a name="known-issues"></a>
### Known Issues

If you ever encounter an issue with the package, look here first for documented solutions.

#### Fixing Laravel's Previous URL Issue

Visits from Turbo Frames will hit your application and Laravel by default keeps track of previously visited URLs to be used with helpers like `url()->previous()`, for instance. This might be confusing because chances are that you wouldn't want to redirect users to the URL of the most recent Turbo Frame that hit your app. So, to avoid storying Turbo Frames visits as Laravel's previous URL, head to the [issue](https://github.com/tonysm/turbo-laravel/issues/60#issuecomment-1123142591) where a solution was discussed.

Expand Down
14 changes: 14 additions & 0 deletions config/turbo-laravel.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

use Tonysm\TurboLaravel\Features;

return [
/*
|--------------------------------------------------------------------------
Expand Down Expand Up @@ -46,4 +48,16 @@
*/

'automatically_register_middleware' => true,

/*
|--------------------------------------------------------------------------
| Turbo Laravel Features
|--------------------------------------------------------------------------
|
| Bellow you can enable/disable some of the features provided by the package.
|
*/
'features' => [
Features::turboNativeRoutes(),
],
];
3 changes: 3 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@
<logging>
<junit outputFile="build/report.junit.xml"/>
</logging>
<php>
<env name="APP_KEY" value="a2ps3dFoNmyehsm7r0VFZ0Iq64hwBpqI"/>
</php>
</phpunit>
8 changes: 8 additions & 0 deletions routes/turbo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

use Illuminate\Support\Facades\Route;
use Tonysm\TurboLaravel\Http\Controllers\TurboNativeNavigationController;

Route::get('recede_historical_location', [TurboNativeNavigationController::class, 'recede'])->name('turbo_recede_historical_location');
Route::get('resume_historical_location', [TurboNativeNavigationController::class, 'resume'])->name('turbo_resume_historical_location');
Route::get('refresh_historical_location', [TurboNativeNavigationController::class, 'refresh'])->name('turbo_refresh_historical_location');
16 changes: 16 additions & 0 deletions src/Features.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Tonysm\TurboLaravel;

class Features
{
public static function enabled(string $feature)
{
return in_array($feature, config('turbo-laravel.features', []));
}

public static function turboNativeRoutes(): string
{
return 'turbo_routes';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Tonysm\TurboLaravel\Http\Controllers\Concerns;

trait InteractsWithTurboNativeNavigation
{
protected function recedeOrRedirectTo(string $url)
{
return $this->redirectToTurboNativeAction('recede', $url);
}

protected function resumeOrRedirectTo(string $url)
{
return $this->redirectToTurboNativeAction('resume', $url);
}

protected function refreshOrRedirectTo(string $url)
{
return $this->redirectToTurboNativeAction('refresh', $url);
}

protected function recedeOrRedirectBack(?string $fallbackUrl, array $options = [])
{
return $this->redirectToTurboNativeAction('recede', $fallbackUrl, 'back', $options);
}

protected function resumeOrRedirectBack(?string $fallbackUrl, array $options = [])
{
return $this->redirectToTurboNativeAction('resume', $fallbackUrl, 'back', $options);
}

protected function refreshOrRedirectBack(?string $fallbackUrl, array $options = [])
{
return $this->redirectToTurboNativeAction('refresh', $fallbackUrl, 'back', $options);
}

protected function redirectToTurboNativeAction(string $action, string $fallbackUrl, string $redirectType = 'to', array $options = [])
{
if (request()->wasFromTurboNative()) {
return redirect(route("turbo_{$action}_historical_location"));
}

if ($redirectType === 'back') {
return redirect()->back($options['status'] ?? 302, $options['headers'] ?? [], $fallbackUrl);
}

return redirect($fallbackUrl);
}
}
23 changes: 23 additions & 0 deletions src/Http/Controllers/TurboNativeNavigationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Tonysm\TurboLaravel\Http\Controllers;

use Illuminate\Routing\Controller;

class TurboNativeNavigationController extends Controller
{
public function recede()
{
return response('Going back...');
}

public function resume()
{
return response('Staying put...');
}

public function refresh()
{
return response('Refreshing...');
}
}
16 changes: 14 additions & 2 deletions src/TurboServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class TurboServiceProvider extends ServiceProvider
public function boot()
{
$this->configurePublications();
$this->configureRoutes();

$this->loadViewsFrom(__DIR__.'/../resources/views', 'turbo-laravel');

Expand Down Expand Up @@ -66,17 +67,28 @@ private function configurePublications()

$this->publishes([
__DIR__ . '/../config/turbo-laravel.php' => config_path('turbo-laravel.php'),
], 'config');
], 'turbo-config');

$this->publishes([
__DIR__ . '/../resources/views' => base_path('resources/views/vendor/turbo-laravel'),
], 'views');
], 'turbo-views');

$this->publishes([
__DIR__.'/../routes/turbo.php' => base_path('routes/turbo.php'),
], 'turbo-routes');

$this->commands([
TurboInstallCommand::class,
]);
}

private function configureRoutes(): void
{
if (Features::enabled('turbo_routes')) {
$this->loadRoutesFrom(__DIR__.'/../routes/turbo.php');
}
}

private function configureMacros(): void
{
Blade::if('turbonative', function () {
Expand Down
106 changes: 106 additions & 0 deletions tests/Http/TurboNativeNavigationControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace Tonysm\TurboLaravel\Tests\Http;

use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Route;
use Tonysm\TurboLaravel\Http\Controllers\Concerns\InteractsWithTurboNativeNavigation;
use Tonysm\TurboLaravel\Http\Middleware\TurboMiddleware;
use Tonysm\TurboLaravel\Testing\InteractsWithTurbo;
use Tonysm\TurboLaravel\Tests\TestCase;

class TurboNativeNavigationControllerTest extends TestCase
{
use InteractsWithTurbo;

public function usesTurboNativeRoutes()
{
Route::middleware(['web', TurboMiddleware::class])->resource('trays', TraysController::class);
}

public function actionsDataProvider()
{
return [
['recede'],
['resume'],
['refresh'],
];
}

/**
* @test
* @dataProvider actionsDataProvider
* @define-route usesTurboNativeRoutes
*/
public function recede_resume_or_refresh_when_native_or_redirect_when_not(string $action)
{
$this->post(route('trays.store'), ['return_to' => "{$action}_or_redirect"])
->assertRedirect(route('trays.show', 1));

$this->turboNative()->post(route('trays.store'), ['return_to' => "{$action}_or_redirect"])
->assertRedirect(route("turbo_{$action}_historical_location"));
}

/**
* @test
* @dataProvider actionsDataProvider
* @define-route usesTurboNativeRoutes
*/
public function recede_resume_or_refresh_when_native_or_redirect_back(string $action)
{
$this->post(route('trays.store'), ['return_to' => "{$action}_or_redirect_back"])
->assertRedirect(route('trays.show', 5));

$this->from(url('/past_place'))->post(route('trays.store'), ['return_to' => "{$action}_or_redirect_back"])
->assertRedirect(url('/past_place'));

$this->turboNative()->from(url('/past_place'))->post(route('trays.store'), ['return_to' => "{$action}_or_redirect_back"])
->assertRedirect(route("turbo_{$action}_historical_location"));
}

/**
* @test
* @define-route usesTurboNativeRoutes
*/
public function historical_location_url_responds_with_html()
{
$this->get(route('turbo_recede_historical_location'))
->assertOk()
->assertSee('Going back...')
->assertHeader('Content-Type', 'text/html; charset=UTF-8');

$this->get(route('turbo_resume_historical_location'))
->assertOk()
->assertSee('Staying put...')
->assertHeader('Content-Type', 'text/html; charset=UTF-8');

$this->get(route('turbo_refresh_historical_location'))
->assertOk()
->assertSee('Refreshing...')
->assertHeader('Content-Type', 'text/html; charset=UTF-8');
}
}

class TraysController extends Controller
{
use InteractsWithTurboNativeNavigation;

public function show($trayId)
{
return [
'tray_id' => $trayId,
];
}

public function store()
{
return match (request('return_to')) {
'recede_or_redirect' => $this->recedeOrRedirectTo(route('trays.show', 1)),
'resume_or_redirect' => $this->resumeOrRedirectTo(route('trays.show', 1)),
'refresh_or_redirect' => $this->refreshOrRedirectTo(route('trays.show', 1)),
'recede_or_redirect_back' => $this->recedeOrRedirectBack(route('trays.show', 5)),
'resume_or_redirect_back' => $this->resumeOrRedirectBack(route('trays.show', 5)),
'refresh_or_redirect_back' => $this->refreshOrRedirectBack(route('trays.show', 5)),
};
}
}

0 comments on commit 41a2e7b

Please sign in to comment.