From 2c340e0e3466987e0a2da2710fc6b916bc2c1b7d Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sat, 18 Jun 2022 19:10:33 -0300 Subject: [PATCH 1/8] Adds Turbo Native Navigation Routes and trait --- phpunit.xml.dist | 3 + routes/turbo.php | 8 ++ src/Facades/Turbo.php | 2 + .../InteractsWithTurboNativeNavigation.php | 49 ++++++++ .../TurboNativeNavigationController.php | 23 ++++ src/Turbo.php | 19 ++++ src/TurboServiceProvider.php | 12 ++ .../TurboNativeNavigationControllerTest.php | 106 ++++++++++++++++++ 8 files changed, 222 insertions(+) create mode 100644 routes/turbo.php create mode 100644 src/Http/Controllers/Concerns/InteractsWithTurboNativeNavigation.php create mode 100644 src/Http/Controllers/TurboNativeNavigationController.php create mode 100644 tests/Http/TurboNativeNavigationControllerTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bafe751..0f0c7f4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -28,4 +28,7 @@ + + + diff --git a/routes/turbo.php b/routes/turbo.php new file mode 100644 index 0000000..a8c07fb --- /dev/null +++ b/routes/turbo.php @@ -0,0 +1,8 @@ +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'); diff --git a/src/Facades/Turbo.php b/src/Facades/Turbo.php index cf4c682..de13b49 100644 --- a/src/Facades/Turbo.php +++ b/src/Facades/Turbo.php @@ -17,6 +17,8 @@ * @method static bool shouldBroadcastToOthers * @method static string domId(Model $model, string $prefix = "") * @method static Broadcaster broadcaster() + * @method static \Tonysm\TurboLaravel\Turbo withoutTurboRoutes() + * @method static bool shouldRegisterRoutes() */ class Turbo extends Facade { diff --git a/src/Http/Controllers/Concerns/InteractsWithTurboNativeNavigation.php b/src/Http/Controllers/Concerns/InteractsWithTurboNativeNavigation.php new file mode 100644 index 0000000..6f0f151 --- /dev/null +++ b/src/Http/Controllers/Concerns/InteractsWithTurboNativeNavigation.php @@ -0,0 +1,49 @@ +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); + } +} diff --git a/src/Http/Controllers/TurboNativeNavigationController.php b/src/Http/Controllers/TurboNativeNavigationController.php new file mode 100644 index 0000000..556021f --- /dev/null +++ b/src/Http/Controllers/TurboNativeNavigationController.php @@ -0,0 +1,23 @@ +visitFromTurboNative; @@ -58,6 +65,18 @@ public function broadcastToOthers($toOthers = true) } } + public function withoutTurboRoutes(): self + { + $this->registersRoutes = true; + + return $this; + } + + public function shouldRegisterRoutes(): bool + { + return $this->registersRoutes; + } + public function shouldBroadcastToOthers(): bool { return $this->broadcastToOthersOnly; diff --git a/src/TurboServiceProvider.php b/src/TurboServiceProvider.php index de6f6b0..6504d7c 100644 --- a/src/TurboServiceProvider.php +++ b/src/TurboServiceProvider.php @@ -28,6 +28,7 @@ class TurboServiceProvider extends ServiceProvider public function boot() { $this->configurePublications(); + $this->configureRoutes(); $this->loadViewsFrom(__DIR__.'/../resources/views', 'turbo-laravel'); @@ -72,11 +73,22 @@ private function configurePublications() __DIR__ . '/../resources/views' => base_path('resources/views/vendor/turbo-laravel'), ], 'views'); + $this->publishes([ + __DIR__.'/../routes/turbo.php' => base_path('routes/turbo.php'), + ], 'turbo-routes'); + $this->commands([ TurboInstallCommand::class, ]); } + private function configureRoutes(): void + { + if (TurboFacade::shouldRegisterRoutes()) { + $this->loadRoutesFrom(__DIR__.'/../routes/turbo.php'); + } + } + private function configureMacros(): void { Blade::if('turbonative', function () { diff --git a/tests/Http/TurboNativeNavigationControllerTest.php b/tests/Http/TurboNativeNavigationControllerTest.php new file mode 100644 index 0000000..5a676ac --- /dev/null +++ b/tests/Http/TurboNativeNavigationControllerTest.php @@ -0,0 +1,106 @@ +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)), + }; + } +} From 211dc3a0b0c505336ed4d0abeeb3875315d05900 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sat, 18 Jun 2022 19:43:32 -0300 Subject: [PATCH 2/8] Docs --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9eadd22..80d97fa 100644 --- a/README.md +++ b/README.md @@ -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) @@ -818,6 +819,45 @@ 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. + +*Note: 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)).* + ### Testing Helpers @@ -952,8 +992,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.* - -### Fixing Laravel's Previous URL Issue + +### 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. From 79adf5b69ae6e5258b1aaaf67ff20554831bb372 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sat, 18 Jun 2022 19:46:49 -0300 Subject: [PATCH 3/8] Generate a new APP_KEY instead of using the one form testbench docs --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0f0c7f4..05ba79f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -29,6 +29,6 @@ - + From 28438bded422a6d3c08778d2c510ae23b5c0a43c Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sat, 18 Jun 2022 20:53:43 -0300 Subject: [PATCH 4/8] Document how to disable the routes --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 80d97fa..818ca7d 100644 --- a/README.md +++ b/README.md @@ -856,7 +856,21 @@ $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. -*Note: 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)).* +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 AppServiceProvider: + +```php +use Tonysm\TurboLaravel\Facades\Turbo; + +class AppServiceProvider extends ServiceProvider +{ + public function boot() + { + Turbo::withoutTurboRoutes(); + } +} +``` ### Testing Helpers From 3bfdb510e7de3ab05eb40a1badd1a782e9dfe54e Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sat, 18 Jun 2022 21:11:44 -0300 Subject: [PATCH 5/8] Change feature toggle to be a static class instead of using Facade (doest work) --- config/turbo-laravel.php | 6 ++++++ src/Facades/Turbo.php | 2 -- src/Features.php | 18 ++++++++++++++++++ src/Turbo.php | 19 ------------------- src/TurboServiceProvider.php | 2 +- 5 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 src/Features.php diff --git a/config/turbo-laravel.php b/config/turbo-laravel.php index dafde22..1a973ef 100644 --- a/config/turbo-laravel.php +++ b/config/turbo-laravel.php @@ -1,5 +1,7 @@ true, + + 'features' => [ + Features::turboNativeRoutes(), + ], ]; diff --git a/src/Facades/Turbo.php b/src/Facades/Turbo.php index de13b49..cf4c682 100644 --- a/src/Facades/Turbo.php +++ b/src/Facades/Turbo.php @@ -17,8 +17,6 @@ * @method static bool shouldBroadcastToOthers * @method static string domId(Model $model, string $prefix = "") * @method static Broadcaster broadcaster() - * @method static \Tonysm\TurboLaravel\Turbo withoutTurboRoutes() - * @method static bool shouldRegisterRoutes() */ class Turbo extends Facade { diff --git a/src/Features.php b/src/Features.php new file mode 100644 index 0000000..6f5f8f3 --- /dev/null +++ b/src/Features.php @@ -0,0 +1,18 @@ +visitFromTurboNative; @@ -65,18 +58,6 @@ public function broadcastToOthers($toOthers = true) } } - public function withoutTurboRoutes(): self - { - $this->registersRoutes = true; - - return $this; - } - - public function shouldRegisterRoutes(): bool - { - return $this->registersRoutes; - } - public function shouldBroadcastToOthers(): bool { return $this->broadcastToOthersOnly; diff --git a/src/TurboServiceProvider.php b/src/TurboServiceProvider.php index 6504d7c..1d75279 100644 --- a/src/TurboServiceProvider.php +++ b/src/TurboServiceProvider.php @@ -84,7 +84,7 @@ private function configurePublications() private function configureRoutes(): void { - if (TurboFacade::shouldRegisterRoutes()) { + if (Features::shouldEnableTurboNativeRoutes()) { $this->loadRoutesFrom(__DIR__.'/../routes/turbo.php'); } } From 8bcd3a140d2c92f9df42a9bfd489ad2f120007ca Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sat, 18 Jun 2022 21:13:33 -0300 Subject: [PATCH 6/8] Change turbo publications to be turbo- prefixed --- src/TurboServiceProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TurboServiceProvider.php b/src/TurboServiceProvider.php index 1d75279..2cf8974 100644 --- a/src/TurboServiceProvider.php +++ b/src/TurboServiceProvider.php @@ -67,11 +67,11 @@ 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'), From 963ab97202ec21d9936a4d1a3e65e22b5cebf9db Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sat, 18 Jun 2022 21:18:50 -0300 Subject: [PATCH 7/8] Tweaks the features implementation --- src/Features.php | 10 ++++------ src/TurboServiceProvider.php | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Features.php b/src/Features.php index 6f5f8f3..588df6a 100644 --- a/src/Features.php +++ b/src/Features.php @@ -4,15 +4,13 @@ class Features { - private static $features = []; - - public static function turboNativeRoutes() + public static function enabled(string $feature) { - static::$features['turbo_routes'] = true; + return in_array($feature, config('turbo-laravel.features', [])); } - public static function shouldEnableTurboNativeRoutes(): bool + public static function turboNativeRoutes(): string { - return static::$features['turbo_routes'] ?? false; + return 'turbo_routes'; } } diff --git a/src/TurboServiceProvider.php b/src/TurboServiceProvider.php index 2cf8974..41541cd 100644 --- a/src/TurboServiceProvider.php +++ b/src/TurboServiceProvider.php @@ -84,7 +84,7 @@ private function configurePublications() private function configureRoutes(): void { - if (Features::shouldEnableTurboNativeRoutes()) { + if (Features::enabled('turbo_routes')) { $this->loadRoutesFrom(__DIR__.'/../routes/turbo.php'); } } From 693a5e0c81932fc09f85f7e2fb6c6987c78fc1f6 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sat, 18 Jun 2022 21:23:42 -0300 Subject: [PATCH 8/8] Tweaks the documentation --- README.md | 16 ++++++---------- config/turbo-laravel.php | 8 ++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 818ca7d..34e788e 100644 --- a/README.md +++ b/README.md @@ -858,18 +858,14 @@ The Turbo Native client should intercept navigations to these special routes and 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 AppServiceProvider: +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 -use Tonysm\TurboLaravel\Facades\Turbo; - -class AppServiceProvider extends ServiceProvider -{ - public function boot() - { - Turbo::withoutTurboRoutes(); - } -} +return [ + 'features' => [ + // Features::turboNativeRoutes(), + ], +]; ``` diff --git a/config/turbo-laravel.php b/config/turbo-laravel.php index 1a973ef..b9bfa39 100644 --- a/config/turbo-laravel.php +++ b/config/turbo-laravel.php @@ -49,6 +49,14 @@ 'automatically_register_middleware' => true, + /* + |-------------------------------------------------------------------------- + | Turbo Laravel Features + |-------------------------------------------------------------------------- + | + | Bellow you can enable/disable some of the features provided by the package. + | + */ 'features' => [ Features::turboNativeRoutes(), ],