├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE.md ├── README.md ├── art └── turbo-laravel-logo.svg ├── composer.json ├── config └── turbo-laravel.php ├── docs ├── broadcasting.md ├── conventions.md ├── csrf.md ├── helpers.md ├── hotwire-native.md ├── installation.md ├── known-issues.md ├── overview.md ├── testing.md ├── turbo-frames.md ├── turbo-streams.md ├── upgrade.md └── validation-response-redirects.md ├── rector.php ├── resources └── views │ ├── components │ ├── exempts-page-from-cache.blade.php │ ├── exempts-page-from-preview.blade.php │ ├── frame.blade.php │ ├── page-requires-reload.blade.php │ ├── page-view-transition.blade.php │ ├── refresh-method.blade.php │ ├── refresh-scroll.blade.php │ ├── refreshes-with.blade.php │ ├── stream-from.blade.php │ └── stream.blade.php │ └── turbo-stream.blade.php ├── routes └── turbo.php ├── src ├── Broadcasters │ ├── Broadcaster.php │ └── LaravelBroadcaster.php ├── Broadcasting │ ├── Factory.php │ ├── Limiter.php │ ├── PendingBroadcast.php │ └── Rendering.php ├── Commands │ ├── Tasks │ │ └── EnsureCsrfTokenMetaTagExists.php │ └── TurboInstallCommand.php ├── Events │ └── TurboStreamBroadcast.php ├── Exceptions │ ├── PageRefreshStrategyException.php │ └── TurboStreamTargetException.php ├── Facades │ ├── Limiter.php │ ├── Turbo.php │ └── TurboStream.php ├── Features.php ├── Http │ ├── Controllers │ │ ├── Concerns │ │ │ ├── InteractsWithHotwireNativeNavigation.php │ │ │ └── InteractsWithTurboNativeNavigation.php │ │ └── HotwireNativeNavigationController.php │ ├── HotwireNativeRedirectResponse.php │ ├── Middleware │ │ └── TurboMiddleware.php │ ├── MultiplePendingTurboStreamResponse.php │ ├── PendingTurboStreamResponse.php │ ├── TurboNativeRedirectResponse.php │ ├── TurboResponseFactory.php │ └── TurboStreamResponseFailedException.php ├── Jobs │ └── BroadcastAction.php ├── Models │ ├── Broadcasts.php │ ├── ModelObserver.php │ └── Naming │ │ └── Name.php ├── NamesResolver.php ├── Testing │ ├── AssertableTurboStream.php │ ├── ConvertTestResponseToTurboStreamCollection.php │ ├── InteractsWithTurbo.php │ └── TurboStreamMatcher.php ├── Turbo.php ├── TurboServiceProvider.php ├── Views │ ├── Components │ │ └── RefreshesWith.php │ ├── RecordIdentifier.php │ └── UnidentifiableRecordException.php ├── globals.php └── helpers.php └── stubs └── resources └── js ├── elements └── turbo-echo-stream-tag.js └── libs └── turbo.js /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | notPath('bootstrap/*') 5 | ->notPath('storage/*') 6 | ->notPath('resources/view/mail/*') 7 | ->in([ 8 | __DIR__ . '/src', 9 | __DIR__ . '/tests', 10 | ]) 11 | ->name('*.php') 12 | ->notName('*.blade.php') 13 | ->ignoreDotFiles(true) 14 | ->ignoreVCS(true); 15 | 16 | $config = new PhpCsFixer\Config(); 17 | 18 | $config->setRules([ 19 | '@PSR2' => true, 20 | 'array_syntax' => ['syntax' => 'short'], 21 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 22 | 'no_unused_imports' => true, 23 | 'not_operator_with_successor_space' => true, 24 | 'trailing_comma_in_multiline' => true, 25 | 'phpdoc_scalar' => true, 26 | 'unary_operator_spaces' => true, 27 | 'binary_operator_spaces' => true, 28 | 'blank_line_before_statement' => [ 29 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 30 | ], 31 | 'phpdoc_single_line_var_spacing' => true, 32 | 'phpdoc_var_without_name' => true, 33 | 'class_attributes_separation' => [ 34 | 'elements' => [ 35 | 'method' => 'one', 36 | ], 37 | ], 38 | 'method_argument_space' => [ 39 | 'on_multiline' => 'ensure_fully_multiline', 40 | 'keep_multiple_spaces_after_comma' => true, 41 | ], 42 | 'single_trait_insert_per_statement' => true, 43 | ])->setFinder($finder); 44 | 45 | return $config; 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `turbo-laravel` will be documented in this file. 4 | 5 | ## 0.0.2 6 | 7 | - Fixes the `@domid` generation for new model instances ([8726d37](https://github.com/tonysm/turbo-laravel/commit/8726d370fbd085d004815707592e5af860748f4d)) 8 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | Sent a PR? Add yourself to the list! 4 | 5 | * [Tony Messias](https://github.com/tonysm) 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Tony Messias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Logo Turbo Laravel

2 | 3 |

4 | Latest Workflow Run 5 | Latest Stable Version 6 | License 7 |

8 | 9 | ## Introduction 10 | 11 | This package gives you a set of conventions to make the most out of [Hotwire](https://hotwired.dev/) in Laravel. 12 | 13 | #### Inspiration 14 | 15 | This package was inspired by the [Turbo Rails gem](https://github.com/hotwired/turbo-rails). 16 | 17 | #### Bootcamp 18 | 19 | If you want a more hands-on introduction, head out to [Bootcamp](https://turbo-laravel.com/guides). It covers building a multi-platform app in Turbo. 20 | 21 | ## Official Documentation 22 | 23 | Documentation for Turbo Laravel can be found on the [Turbo Laravel website](https://turbo-laravel.com). 24 | 25 | ### Changelog 26 | 27 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 28 | 29 | ### Contributing 30 | 31 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 32 | 33 | ### Security Vulnerabilities 34 | 35 | Drop me an email at [tonysm@hey.com](mailto:tonysm@hey.com?subject=Security%20Vulnerability) if you want to report 36 | security vulnerabilities. 37 | 38 | ### License 39 | 40 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 41 | 42 | ### Credits 43 | 44 | - [Tony Messias](https://github.com/tonysm) 45 | - [All Contributors](./CONTRIBUTORS.md) 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hotwired-laravel/turbo-laravel", 3 | "description": "Turbo Laravel gives you a set of conventions to make the most out of the Hotwire stack (inspired by turbo-rails gem).", 4 | "keywords": [ 5 | "hotwired", 6 | "hotwire", 7 | "turbo", 8 | "turbo-laravel" 9 | ], 10 | "homepage": "https://github.com/hotwired-laravel/turbo-laravel", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Tony Messias", 15 | "email": "tonysm@hey.com", 16 | "homepage": "https://tonysm.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.2", 22 | "illuminate/support": "^11.0|^12.0" 23 | }, 24 | "require-dev": { 25 | "laravel/pint": "^1.10", 26 | "orchestra/testbench": "^9.0|^10.0", 27 | "orchestra/workbench": "^9.0|^10.0", 28 | "phpunit/phpunit": "^10.5|^11.5" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "HotwiredLaravel\\TurboLaravel\\": "src" 33 | }, 34 | "files": [ 35 | "src/helpers.php", 36 | "src/globals.php" 37 | ] 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "HotwiredLaravel\\TurboLaravel\\Tests\\": "tests", 42 | "Workbench\\App\\": "workbench/app/", 43 | "Workbench\\Database\\Factories\\": "workbench/database/factories/", 44 | "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" 45 | } 46 | }, 47 | "scripts": { 48 | "psalm": "vendor/bin/psalm", 49 | "test": "vendor/bin/phpunit --colors=always", 50 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 51 | "post-autoload-dump": [ 52 | "@clear", 53 | "@prepare" 54 | ], 55 | "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", 56 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 57 | "build": "@php vendor/bin/testbench workbench:build --ansi", 58 | "serve": [ 59 | "@build", 60 | "Composer\\Config::disableProcessTimeout", 61 | "@php vendor/bin/testbench serve" 62 | ], 63 | "lint": [ 64 | "@php vendor/bin/pint" 65 | ] 66 | }, 67 | "config": { 68 | "sort-packages": true 69 | }, 70 | "extra": { 71 | "laravel": { 72 | "providers": [ 73 | "\\HotwiredLaravel\\TurboLaravel\\TurboServiceProvider" 74 | ], 75 | "aliases": { 76 | "Turbo": "\\HotwiredLaravel\\TurboLaravel\\Facades\\Turbo", 77 | "TurboStream": "\\HotwiredLaravel\\TurboLaravel\\Facades\\TurboStream" 78 | } 79 | } 80 | }, 81 | "minimum-stability": "dev", 82 | "prefer-stable": true 83 | } 84 | -------------------------------------------------------------------------------- /config/turbo-laravel.php: -------------------------------------------------------------------------------- 1 | env('APP_ENV', 'production') !== 'testing', 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Root Model Namespaces 26 | |-------------------------------------------------------------------------- 27 | | 28 | | When generating DOM IDs for models, we need to strip out the root namespaces from the model's FQCN. Please, 29 | | if you use non-conventional folder structures, make sure you add your custom namespaces to this list. The 30 | | first one that matches a "starts with" check will be used and removed from the model's FQCN for DOM IDs. 31 | | 32 | */ 33 | 34 | 'models_namespace' => [ 35 | 'App\\Models\\', 36 | 'App\\', 37 | ], 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Automatically Register Turbo Middleware 42 | |-------------------------------------------------------------------------- 43 | | 44 | | When set to `true` the TurboMiddleware will be automatically 45 | | *prepended* to the web routes middleware stack. If you want 46 | | to disable this behavior, set this to false. 47 | | 48 | */ 49 | 50 | 'automatically_register_middleware' => true, 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Turbo Laravel Features 55 | |-------------------------------------------------------------------------- 56 | | 57 | | Bellow you can enable/disable some of the features provided by the package. 58 | | 59 | */ 60 | 'features' => [ 61 | Features::hotwireNativeRoutes(), 62 | ], 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Guessed Route Exceptions 67 | |-------------------------------------------------------------------------- 68 | | 69 | | The URIs that should be excluded from the guessing redirect route behavior. 70 | | 71 | */ 72 | 'redirect_guessing_exceptions' => [ 73 | // '/some-page' 74 | ], 75 | ]; 76 | -------------------------------------------------------------------------------- /docs/broadcasting.md: -------------------------------------------------------------------------------- 1 | --- 2 | extends: _layouts.docs 3 | title: Broadcasting 4 | description: Broadcasting Turbo Streams 5 | order: 8 6 | --- 7 | 8 | # Broadcasting Turbo Streams 9 | 10 | So far, we've seen how we may generate Turbo Streams to either add it to our Blade views or return them from controllers after a form submission over HTTP. In addition to that, we may also broadcast model changes over WebSockets (or [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)). 11 | 12 | It's important to mention that this is an optional feature of Turbo and Turbo Laravel. **You don't have to use Turbo Streams Broadcasting if you don't have the need for it** in order to use Turbo. 13 | 14 | We can make our model changes generate Turbo Streams. Yes, the exact same Turbo Streams tags we're used to. Remember, "HTML over the wire." Turbo Streams Broadcasting use [Laravel Echo](https://github.com/laravel/echo) and [Laravel's Broadcasting](https://laravel.com/docs/broadcasting) component. 15 | 16 | Broadcasts are usually triggered after a form submission. You may still return Turbo Streams over HTTP to the user that triggered the form submission, and only send the Turbo Stream Broadcasting to _other_ users. This way, the user making the change will have an instant feedback compared to having to wait for the broadcasting over WebSockets, which may involve queue workers. 17 | 18 | ## Configuration 19 | 20 | Broadcasting Turbo Streams relies heavily on [Laravel's Broadcasting](https://laravel.com/docs/broadcasting) component. This means you need to configure Laravel Echo in the frontend and either [Laravel Reverb](https://reverb.laravel.com/) or a paid service like [Pusher](https://pusher.com/). 21 | 22 | ## Listening to Broadcasts 23 | 24 | Turbo Laravel will publish a custom HTML tag to your application's `resources/js/elements` folder. This tag is called `` (see [here](https://github.com/hotwired-laravel/turbo-laravel/blob/main/stubs/resources/js/elements/turbo-echo-stream-tag.js)). 25 | 26 | You may add this tag to any Blade view passing the channel you want to listen to and users will start receiving Turbo Stream Broadcasts right away: 27 | 28 | ```blade 29 | 32 | ``` 33 | 34 | For convenience, you may prefer using the `` Blade component that ships with Turbo Laravel (it requires that you have a custom element named `` available, since that's the tag this component will render in HTML). You may pass the model as the `source` prop to it, it will figure out the channel name for that specific model using [Laravel's conventions](https://laravel.com/docs/broadcasting#model-broadcasting-conventions): 35 | 36 | ```blade 37 | 38 | ``` 39 | 40 | By default, it expects a private channel, so it must be used in a page where users are already authenticated. You may control the channel type in the tag with a `type` attribute. 41 | 42 | ```blade 43 | 44 | ``` 45 | 46 | Make sure you have the Broadcast Auth Route for your models registered in your `routes/channels.php` file: 47 | 48 | ```php 49 | use App\Models\Post; 50 | use App\Models\User; 51 | use Illuminate\Support\Facades\Broadcast; 52 | 53 | Broadcast::channel(Post::class, function (User $user, Post $post) { 54 | return $user->belongsToTeam($post->team); 55 | }); 56 | ``` 57 | 58 | You may want to read the [Laravel Broadcasting](https://laravel.com/docs/broadcasting) documentation. 59 | 60 | ## Broadcasting Model Changes 61 | 62 | To be broadcast model changes for a particular, you must add the `Broadcasts` trait to your models: 63 | 64 | ```php 65 | use HotwiredLaravel\TurboLaravel\Models\Broadcasts; 66 | 67 | class Comment extends Model 68 | { 69 | use Broadcasts; 70 | } 71 | ``` 72 | 73 | This trait will augment any model with Turbo Stream broadcasting methods that you may use to trigger broadcasts _manually_. Here's how you can broadcast an `append` Turbo Stream for a newly created comment to all users visiting the post page: 74 | 75 | ```php 76 | Route::post('posts/{post}/comments', function (Post $post) { 77 | $comment = $post->comments()->create(/** params */); 78 | 79 | $comment->broadcastAppend()->toOthers()->later(); 80 | 81 | if (request()->wantsTurboStream()) { 82 | return turbo_stream($comment); 83 | } 84 | 85 | return back(); 86 | }); 87 | ``` 88 | 89 | Here are the methods now available to your model: 90 | 91 | ```php 92 | $comment->broadcastAppend(); 93 | $comment->broadcastPrepend(); 94 | $comment->broadcastBefore('target_dom_id'); 95 | $comment->broadcastAfter('target_dom_id'); 96 | $comment->broadcastReplace(); 97 | $comment->broadcastUpdate(); 98 | $comment->broadcastRemove(); 99 | $comment->broadcastRefresh(); 100 | ``` 101 | 102 | These methods will assume you want to broadcast to your model's channel. In this case, it would broadcast the Turbo Streams to a private channel named `App.Models.Comments.{id}`. 103 | 104 | Additionally, you may send these broadcasts to any other model's channel: 105 | 106 | ```php 107 | $comment->broadcastAppendTo($post); 108 | $comment->broadcastPrependTo($post); 109 | $comment->broadcastBeforeTo($post, 'target_dom_id'); 110 | $comment->broadcastAfterTo($post, 'target_dom_id'); 111 | $comment->broadcastReplaceTo($post); 112 | $comment->broadcastUpdateTo($post); 113 | $comment->broadcastRemoveTo($post); 114 | $comment->broadcastRefreshTo($post); 115 | ``` 116 | 117 | These `broadcastXTo()` methods accept either a model, an instance of the [`Channel`](https://github.com/laravel/framework/blob/10.x/src/Illuminate/Broadcasting/Channel.php) class, or an array containing both of these. 118 | 119 | When it receives a model, it will guess the channel name using Laravel's [Broadcasting channel naming convention](https://laravel.com/docs/broadcasting#model-broadcasting-conventions). 120 | 121 | All of these broadcasting methods return an instance of the `PendingBroadcast` class that will only dispatch the broadcasting job when that pending object is being garbage collected. Which means you may make changes to this pending broadcast by chaining on the returned object: 122 | 123 | ```php 124 | $comment->broadcastAppend() 125 | ->to($post) 126 | ->view('comments/_custom_view_partial', [ 127 | 'comment' => $comment, 128 | 'post' => $post, 129 | ]) 130 | ->toOthers() // Do not send to the current user... 131 | ->later(); // Don't send it now, dispatch a job to send in background instead... 132 | ``` 133 | 134 | You may want to hook these broadcasts from your [model's events](https://laravel.com/docs/eloquent#events) to trigger Turbo Stream broadcasts whenever your models are changed in any context: 135 | 136 | ```php 137 | class Comment extends Model 138 | { 139 | use Broadcasts; 140 | 141 | protected static function booted() 142 | { 143 | static::created(function (Comment $comment) { 144 | $comment->broadcastPrependTo($comment->post)->later(); 145 | }); 146 | 147 | static::updated(function (Comment $comment) { 148 | $comment->broadcastReplaceTo($comment->post)->later(); 149 | }); 150 | 151 | static::deleted(function (Comment $comment) { 152 | $comment->broadcastRemoveTo($comment->post)->later(); 153 | }); 154 | } 155 | } 156 | ``` 157 | 158 | For convenience, instead of adding all these lines to achieve this set of broadcasting, you may add a `$broadcasts = true` property to your model class. This property instructs the `Brodcasts` trait to automatically hook the model Turbo Stream broadcasts on the correct events: 159 | 160 | ```php 161 | class Comment extends Model 162 | { 163 | use Broadcasts; 164 | 165 | protected $broadcasts = true; 166 | } 167 | ``` 168 | 169 | This achieves almost the same set of Broadcasts as the previous example, with a few nuanced differences. First, by default, it will broadcast an `append` Turbo Stream on newly created models. You may want to use `prepend` instead. You may do that by changing the `$broadcasts` property to be a configuration array instead of a boolean `true`, then set the `insertsBy` key to `prepend`: 170 | 171 | ```php 172 | class Comment extends Model 173 | { 174 | use Broadcasts; 175 | 176 | protected $broadcasts = [ 177 | 'insertsBy' => 'prepend', 178 | ]; 179 | } 180 | ``` 181 | 182 | When using the `$broadcasts` property, the Turbo Stream broadcasts will be sent to the current model's channel. However, since the channels use the model's ID as per the naming convention, no one will ever be able to listen on that channel before the model is created. For that reason, Turbo Stream broadcasts of newly created models will be sent to a private channel using the model's plural name instead. You may also configure which `stream` name this specific Turbo Stream should be sent to by setting the `stream` key on the `$broadcasts` property: 183 | 184 | ```php 185 | class Comment extends Model 186 | { 187 | use Broadcasts; 188 | 189 | protected $broadcasts = [ 190 | 'insertsBy' => 'prepend', 191 | 'stream' => 'my-comments', 192 | ]; 193 | } 194 | ``` 195 | 196 | This will send the Turbo Stream broadcast to private channel called `my-comments` when a new comment is created. 197 | 198 | Alternatively, you may also set a `$broadcastsTo` property with either a string with the name of the relationship to be used to resolve the channel, or an array of relationships if you want to send the broadcast to multiple related model's channels: 199 | 200 | ```php 201 | class Comment extends Model 202 | { 203 | use Broadcasts; 204 | 205 | protected $broadcasts = [ 206 | 'insertsBy' => 'prepend', 207 | ]; 208 | 209 | protected $broadcastsTo = 'post'; 210 | 211 | public function post() 212 | { 213 | return $this->belongsTo(Post::class); 214 | } 215 | } 216 | ``` 217 | 218 | You may also do that by adding a `broadcastsTo()` method to your model instead of the `$broadcastsTo` property. The method must return either an Eloquent model, a Channel instance, or an array with a mix of those: 219 | 220 | ```php 221 | use Illuminate\Broadcasting\Channel; 222 | 223 | class Comment extends Model 224 | { 225 | use Broadcasts; 226 | 227 | protected $broadcasts = [ 228 | 'insertsBy' => 'prepend', 229 | ]; 230 | 231 | public function post() 232 | { 233 | return $this->belongsTo(Post::class); 234 | } 235 | 236 | public function broadcastsTo() 237 | { 238 | return [ 239 | $this, 240 | $this->post, 241 | new Channel('full-control'), 242 | ]; 243 | } 244 | } 245 | ``` 246 | 247 | Having a `$broadcastsTo` property or implementing the `broadcastsTo()` method in your model will have precedence over the `stream` key of the `$broadcasts` property. 248 | 249 | ## Broadcasting Page Refreshes 250 | 251 | Similar to the `$broadcasts` property, you may want to automatically configure page refresh broadcasts on a modal. You may use the `$broadcastsRefreshes` property for that: 252 | 253 | ```php 254 | use Illuminate\Broadcasting\Channel; 255 | 256 | class Comment extends Model 257 | { 258 | use Broadcasts; 259 | 260 | protected $broadcastsRefreshes = true; 261 | } 262 | ``` 263 | 264 | This is the same as doing: 265 | 266 | ```php 267 | use Illuminate\Broadcasting\Channel; 268 | 269 | class Comment extends Model 270 | { 271 | use Broadcasts; 272 | 273 | public static function booted() 274 | { 275 | static::created(function ($comment) { 276 | $comment->broadcastRefreshTo("comments")->later(); 277 | }); 278 | 279 | static::updated(function ($comment) { 280 | $comment->broadcastRefresh()->later(); 281 | }); 282 | 283 | static::deleted(function ($comment) { 284 | $comment->broadcastRefresh(); 285 | }); 286 | } 287 | } 288 | ``` 289 | 290 | You may want to broadcast page refreshes to a related model: 291 | 292 | ```php 293 | use Illuminate\Broadcasting\Channel; 294 | 295 | class Comment extends Model 296 | { 297 | use Broadcasts; 298 | 299 | protected $broadcastsRefreshes = true; 300 | 301 | protected $broadcastsRefreshesTo = ['post']; 302 | 303 | public function post() 304 | { 305 | return $this->belongsTo(Post::class); 306 | } 307 | } 308 | ``` 309 | 310 | This will send page refreshes broadcasts to the related `Post` model channel. 311 | 312 | Alternatively, you may specific a `broadcastsRefreshesTo` method instead of a property: 313 | 314 | ```php 315 | use Illuminate\Broadcasting\Channel; 316 | 317 | class Comment extends Model 318 | { 319 | use Broadcasts; 320 | 321 | protected $broadcastsRefreshes = true; 322 | 323 | public function post() 324 | { 325 | return $this->belongsTo(Post::class); 326 | } 327 | 328 | public function broadcastsRefreshesTo() 329 | { 330 | return [$this->post]; 331 | } 332 | } 333 | ``` 334 | 335 | From this method, you may return an instance of an Eloquent model, a string representing the channel name, or an instance of a `Channel` class. 336 | 337 | ## Broadcasting Turbo Streams to Other Users Only 338 | 339 | As mentioned earlier, you may want to feed the current user with Turbo Streams using HTTP requests and only send the broadcasts to other users. You may achieve that by chaining on the pending broadcast object that returns from all `broadcastX` methods: 340 | 341 | ```php 342 | $comment->broadcastAppendTo($post)->toOthers(); 343 | ``` 344 | 345 | Alternatively, you may use the Turbo Facade like so to configure a scope where all broadcast Turbo Streams triggered inside of it will be sent to other users only: 346 | 347 | ```php 348 | use HotwiredLaravel\TurboLaravel\Facades\Turbo; 349 | 350 | Turbo::broadcastToOthers(function () { 351 | // ... 352 | }); 353 | ``` 354 | 355 | If you always want to send broadcasts to other users excluding the current user from receiving broadcasts, you may call the `broadcastToOthers` without passing a closure to it somewhere globally like a middleware or the `AppServiceProvider::boot()` method: 356 | 357 | ```php 358 | 'Hello World']), 407 | target: 'notifications', 408 | channel: 'general', 409 | ); 410 | 411 | // Passing an instance of the HtmlString class (won't be escaped by Blade)... 412 | TurboStream::broadcastAppend( 413 | content: new HtmlString('Hello World'), 414 | target: 'notifications', 415 | channel: 'general', 416 | ); 417 | 418 | // Passing a simple string (will be escaped by Blade)... 419 | TurboStream::broadcastAppend( 420 | content: 'Hello World', 421 | target: 'notifications', 422 | channel: 'general', 423 | ); 424 | ``` 425 | 426 | You may also customize the Turbo Stream by chaining on the returned `PendingBroadcast` object: 427 | 428 | ```php 429 | TurboStream::broadcastAppend('Hello World') 430 | ->target('notifications') 431 | ->to('general'); 432 | ``` 433 | 434 | As for the channel, you may pass a string that will be interpreted as a public channel name, an Eloquent model which will resolve to a private channel using that model's broadcasting channel convention, or instances of the `Illuminate\Broadcasting\Channel` class. 435 | 436 | You may want to specify private or presence string channels instead of public ones: 437 | 438 | ```php 439 | TurboStream::broadcastAppend('Hello World') 440 | ->target('notifications') 441 | ->toPrivateChannel('user.123'); 442 | 443 | TurboStream::broadcastAppend('Hello World') 444 | ->target('notifications') 445 | ->toPresenceChannel('user.123'); 446 | ``` 447 | 448 | Using the `broadcastAction()` will allow you to broadcast any custom Turbo Stream action, so you're not limited to the default ones when using this approach: 449 | 450 | ```php 451 | TurboStream::broadcastAction('scroll_to', target: 'todo_123'); 452 | ``` 453 | 454 | ## Handmade Broadcasting Using The `turbo_stream()` Response Builder 455 | 456 | One more alternative to broadcasting Turbo Streams is to call the `broadcastTo()` method on the returned object of the `turbo_stream()` function: 457 | 458 | ```php 459 | turbo_stream() 460 | ->append('notifications', 'Hello World') 461 | ->broadcastTo('general'); 462 | ``` 463 | 464 | This will tap on the `PendingTurboStreamResponse` and create a `PendingBroadcast` from it. It's important to note that this will return the same `PendingTurboStreamResponse`, not the `PendingBroadcast`. If you want to configure the `PendingBroadcast` that will be generated, you must do that before calling the `broadcastTo()` method, but you may also pass a `Closure` as the second parameter: 465 | 466 | ```php 467 | turbo_stream() 468 | ->append('notifications', 'Hello World') 469 | ->broadcastTo('general', fn ($broadcast) => $broadcast->toOthers()); 470 | ``` 471 | 472 | The first argument must be either a string, an Eloquent model, or an instance of the `Illuminate\Broadcasting\Channel` class as the channel: 473 | 474 | ```php 475 | turbo_stream($comment) 476 | ->broadcastTo($comment->post, fn ($broadcast) => $broadcast->toOthers()); 477 | ``` 478 | 479 | Similarly to using the Facade, you may also want to broadcast to private or presence string channels like so: 480 | 481 | ```php 482 | // Broadcast to private channels... 483 | turbo_stream() 484 | ->append('notifications', 'Hello World') 485 | ->broadcastToPrivateChannel('user.123', fn ($broadcast) => $broadcast->toOthers()) 486 | 487 | // Broadcast to presence channels... 488 | turbo_stream() 489 | ->append('notifications', 'Hello World') 490 | ->broadcastToPresenceChannel('chat.123', fn ($broadcast) => $broadcast->toOthers()); 491 | ``` 492 | -------------------------------------------------------------------------------- /docs/conventions.md: -------------------------------------------------------------------------------- 1 | --- 2 | extends: _layouts.docs 3 | title: Conventions 4 | description: All the (optional) conventions and recommendations 5 | order: 4 6 | --- 7 | 8 | # Conventions 9 | 10 | The conventions described below are **NOT mandatory**. Feel free to pick what you like and also come up with your own conventions. With that out of the way, here's a list of conventions you may find helpful. 11 | 12 | ## Resource Routes 13 | 14 | Laravel supports [resource routes](https://laravel.com/docs/controllers#resource-controllers) and that plays really well with Hotwire for most things. This creates route names such as `posts.index`, `posts.store`, etc. 15 | 16 | If you don't want to use resource routes, at least consider using the naming convention: render forms in route names ending in `.create`, `.edit`, or `.delete`, and name their handler routes ending with `.store`, `.update`, or `.destroy`, accordingly. 17 | 18 | Turbo Laravel uses this naming convention so it doesn't redirect after failed validations and, instead, triggers another internal request to the application as well so it can re-render the form returning a 422 response with. The form should re-render with the `old()` input values and any validation messages as well. 19 | 20 | You may want to define exceptions to the route guessing behavior. In that's the case, set them in the `redirect_guessing_exceptions` in the `config/turbo-laravel.php` config file: 21 | 22 | ```php 23 | return [ 24 | // ... 25 | 'redirect_guessing_exceptions' => [ 26 | '/some-page', 27 | ], 28 | ]; 29 | ``` 30 | 31 | When using this config, the redirection behavior will still happen, but the package will not attempt to guess the routes that render the forms on those routes. See the [Validation Response Redirects](/docs/validation-response-redirects) page to know more about why this happens. 32 | 33 | ## Partials 34 | 35 | You may want to split up your views in smaller chunks (aka. "partials"), such as a `comments/_comment.blade.php` to display a comment resource, or `comments/_form.blade.php` to display the form for both creating and updating comments. This allows you to reuse these _partials_ in [Turbo Streams](/docs/turbo-streams). 36 | 37 | Alternatively, you may override the pattern to a `{plural}.partials.{singular}` convention for your partials location by calling the `Turbo::usePartialsSubfolderPattern()` method of the Turbo Facade from your `AppServiceProvider::boot()` method: 38 | 39 | ```php 40 | 'partials.{singular}'); 75 | } 76 | } 77 | ``` 78 | 79 | You may also want to define your own pattern, which you can do by either specifying a string where you have the `{plural}` and `{singular}` placeholders available, but you can also specify a Closure, which will receive the model instance. On that Closure, you must return a string with the view location using the dot convention of Laravel. For instance, the subfolder pattern sets the config value to `{plural}.partials.{singular}` instead of the default, which is `{plural}._{singular}`. These will resolve to `comments.partials.comment` and `comments._comment` views, respectively. 80 | 81 | The models' partials (such as a `comments/_comment.blade.php` for a `Comment` model) may only rely on having a single `$comment` variable passed to them. That's because Turbo Stream Model Broadcasts - which is an _optional_ feature, by the way - relies on these conventions to figure out the partial for a given model when broadcasting and will also pass the model to such partial, using the class basename as the variable instance in _camelCase_. Again, this is optional, you can customize most of these things or create your own model broadcasting convention. Read the [Broadcasting](/docs/broadcasting) section to know more. 82 | 83 | ## Turbo Stream Channel Names 84 | 85 | _Note: Turbo Stream Broadcasts are optional._ 86 | 87 | You may use the model's Fully Qualified Class Name (aka. FQCN) as your Broadcasting Channel authorization routes with a wildcard, such as `App.Models.Comment.{comment}` for a `Comment` model living in `App\\Models\\` - the wildcard's name doesn't matter, as long as there is one. This is the default [broadcasting channel naming convention](https://laravel.com/docs/8.x/broadcasting#model-broadcasting-conventions) in Laravel. 88 | -------------------------------------------------------------------------------- /docs/csrf.md: -------------------------------------------------------------------------------- 1 | --- 2 | extends: _layouts.docs 3 | title: CSRF Protection 4 | description: CSRF Protection 5 | order: 10 6 | --- 7 | 8 | # CSRF Protection 9 | 10 | Laravel has built-in CSRF protection in place. It prevents our app from processing any non-GET requests that doesn't include a valid CSRF Token that was generated in our backend. 11 | 12 | So, to allow a POST form to be processed, we usually need to add a `@csrf` Blade directive to our forms: 13 | 14 | ```blade 15 |
16 | @csrf 17 | 18 |
19 | ``` 20 | 21 | Since Turbo.js intercepts form submissions and converts those to fetch requests (AJAX), we don't actually _need_ the `@csrf` token applied to each form. Turbo is smart enough to read our page's meta tags, look for one named `csrf-token` and use its contents to add the token to all form submissions it intercepts. Jetstream and Breeze both ship with such element in the layout files, but in case you're missing it in your views, it should look like this: 22 | 23 | ```blade 24 | 25 | ``` 26 | 27 | With that being said, you may still want to use the `@csrf` Blade directive if you want to support users with JavaScript disabled, since the forms will still work if they contain the CSRF token. 28 | -------------------------------------------------------------------------------- /docs/helpers.md: -------------------------------------------------------------------------------- 1 | --- 2 | extends: _layouts.docs 3 | title: Helpers 4 | description: All the helpers the package provides 5 | order: 5 6 | --- 7 | 8 | # Helpers 9 | 10 | Turbo Laravel has a set of Blade Directives, Components, helper functions, and request/response macros to help making the most out of Turbo in Laravel. 11 | 12 | ## Blade Directives 13 | 14 | ### The `@domid()` Blade Directive 15 | 16 | Since Turbo relies a lot on DOM IDs, the package offers a helper to generate unique DOM IDs based on your models. You may use the `@domid` Blade Directive in your Blade views like so: 17 | 18 | ```blade 19 | 20 | 21 | 22 | ``` 23 | 24 | This will generate a DOM ID string using your model's basename and its ID, such as `post_123`. You may also give it a prefix that will be added to the DOM ID, such as: 25 | 26 | ```blade 27 | 28 | 29 | 30 | ``` 31 | 32 | Which will generate a `comments_post_123` DOM ID, assuming your Post model has an ID of `123`. 33 | 34 | ## Blade Components 35 | 36 | ### The `` Blade Component 37 | 38 | You may also prefer using the `` Blade component that ships with the package. This way, you don't need to worry about using the `@domid()` helper for your Turbo Frame: 39 | 40 | ```blade 41 | 42 | 43 | 44 | ``` 45 | 46 | To the `:id` prop, you may pass a string, which will be used as-is as the DOM ID, an Eloquent model instance, which will be passed to the `dom_id()` function that ships with the package (the same one as the `@domid()` Blade directive uses behind the scenes), or an array tuple where the first item is an instance of an Eloquent model and the second is the prefix of the DOM ID, something like this: 47 | 48 | ```blade 49 | 50 | 51 | 52 | ``` 53 | 54 | Additionally, you may also pass along any prop that is supported by the Turbo Frame custom Element to the `` Blade component, like `target`, `src`, or `loading`. These are the listed attributes, but any other attribute will also be forwarded to the `` tag that will be rendered by the `` component. For a full list of what's possible to do with Turbo Frames, see the [documentation](https://turbo.hotwired.dev/handbook/frames). 55 | 56 | ### The `` Blade Component 57 | 58 | If you're rendering a Turbo Stream inside a your Blade files, you may use the `` helper: 59 | 60 | ```blade 61 | 62 | @include('posts.partials.post', ['post' => $post]) 63 | 64 | ``` 65 | 66 | Just like in the Turbo Frames' `:id` prop, the `:target` prop of the Turbo Stream component accepts a string, a model instance, or an array to resolve the DOM ID using the `dom_id()` function. 67 | 68 | ### The `` Blade Component 69 | 70 | We can configure which update method Turbo should so to update the document: 71 | 72 | | Method | Description | 73 | |---|---| 74 | | `replace` | Updates the entire body of the document on Turbo Visits | 75 | | `morph` | Uses DOM morphing to update the document instead of replacing everything | 76 | 77 | Here's how you can use it: 78 | 79 | ```blade 80 | 81 | ``` 82 | 83 | The output would be: 84 | 85 | ```blade 86 | 87 | ``` 88 | 89 | ### The `` Blade Component 90 | 91 | You can also configure the scroll behavior on Turbo: 92 | 93 | | Behavior | Description | 94 | |---|---| 95 | | `reset` | Resets the scroll position to the top, mimicking for the browser handles new page visits | 96 | | `preserve` | Preserves the current scroll position (usually results in a better UX when used with the `morph` method) | 97 | 98 | Here's how you can use it: 99 | 100 | ```blade 101 | 102 | ``` 103 | 104 | The output would be: 105 | 106 | ```blade 107 | 108 | ``` 109 | 110 | ### The `` Blade Component 111 | 112 | You may configure both the refresh method and scroll behavior using the `` component in your main layout's `` tag or on specific pages to configure how Turbo should update the page. Here's an example: 113 | 114 | ```blade 115 | 116 | ``` 117 | 118 | This will render two HTML `` tags: 119 | 120 | ```html 121 | 122 | 123 | ``` 124 | 125 | ### The `` Blade Component 126 | 127 | This component may be added to any page you don't want Turbo to keep a cache in the page cache. Example: 128 | 129 | ```blade 130 | 131 | ``` 132 | 133 | It will render the HTML `` tag: 134 | 135 | ```html 136 | 137 | ``` 138 | 139 | ### The `` Blade Component 140 | 141 | This component may be added to any page you don't want Turbo to show as a preview on regular navigation visits. No-preview pages will only be used in restoration visits (when you use the browser's back or forward buttons, or when when moving backward in the navigation stack). Example: 142 | 143 | ```blade 144 | 145 | ``` 146 | 147 | It will render the HTML `` tag: 148 | 149 | ```html 150 | 151 | ``` 152 | 153 | ### The `` Blade Component 154 | 155 | This component may be added to any page you want Turbo to reload. This will break out of Turbo Frame navigations. May be used at a login screen, for instance. Example: 156 | 157 | ```blade 158 | 159 | ``` 160 | 161 | It will render the HTML `` tag: 162 | 163 | ```html 164 | 165 | ``` 166 | 167 | ## Helper Functions 168 | 169 | The package ships with a set of helper functions. These functions are all namespaced under `HotwiredLaravel\\TurboLaravel\\` but we also add them globally for convenience, so you may use them directly without the `use` statements (this is useful in contexts like Blade views, for instance). 170 | 171 | ### The `dom_id()` 172 | 173 | The mentioned namespaced `dom_id()` helper function may also be used from anywhere in your application, like so: 174 | 175 | ```php 176 | use function HotwiredLaravel\TurboLaravel\dom_id; 177 | 178 | dom_id($comment); 179 | ``` 180 | 181 | When a new instance of a model is passed to any of these DOM ID helpers, since it doesn't have an ID, it will prefix the resource name with a `create_` prefix. This way, new instances of an `App\\Models\\Comment` model will generate a `create_comment` DOM ID. 182 | 183 | These helpers strip out the model's FQCN (see [config/turbo-laravel.php](https://github.com/hotwired-laravel/turbo-laravel/blob/main/config/turbo-laravel.php) if you use an unconventional location for your models). 184 | 185 | ### The `dom_class()` 186 | 187 | The `dom_class()` helper function may be used from anywhere in your application, like so: 188 | 189 | ```php 190 | use function HotwiredLaravel\TurboLaravel\dom_class; 191 | 192 | dom_class($comment); 193 | ``` 194 | 195 | This function will generate the DOM class named based on your model's class name. If you have an instance of a `App\Models\Comment` model, it will generate a `comment` DOM class. 196 | 197 | Similarly to the `dom_id()` function, you may also pass a context prefix as the second parameter: 198 | 199 | ```php 200 | dom_class($comment, 'reactions_list'); 201 | ``` 202 | 203 | This will generate a DOM class of `reactions_list_comment`. 204 | 205 | ### The `turbo_stream()` 206 | 207 | You may generate Turbo Streams using the `Response::turboStream()` macro, but you may also do so using the `turbo_stream()` helper function: 208 | 209 | ```php 210 | use function HotwiredLaravel\TurboLaravel\turbo_stream; 211 | 212 | turbo_stream()->append($comment); 213 | ``` 214 | 215 | Both the `Response::turboStream()` and the `turbo_stream()` function work the same way. The `turbo_stream()` function may be easier to use. 216 | 217 | ### The `turbo_stream_view()` 218 | 219 | You may combo Turbo Streams using the `turbo_stream([])` function passing an array, but you may prefer to create a separate Blade view with all the Turbo Streams, this way you may also use template extensions and everything else Blade offers: 220 | 221 | ```php 222 | use function HotwiredLaravel\TurboLaravel\turbo_stream_view; 223 | 224 | return turbo_stream_view('comments.turbo.created', [ 225 | 'comment' => $comment, 226 | ]); 227 | ``` 228 | 229 | ## Request & Response Macros 230 | 231 | ### The `request()->wantsTurboStream()` macro 232 | 233 | The `request()->wantsTurboStream()` macro added to the request class will check if the request accepts Turbo Stream and return `true` or `false` accordingly. 234 | 235 | Turbo will add a `Accept: text/vnd.turbo-stream.html, ...` header to the requests. That's how we can detect if the request came from a client using Turbo. 236 | 237 | ### The `request()->wasFromTurboFrame()` macro 238 | 239 | The `request()->wasFromTurboFrame()` macro added to the request class will check if the request was made from a Turbo Frame. When used with no parameters, it returns `true` if the request has a `Turbo-Frame` header, no matter which specific Turbo Frame. 240 | 241 | Additionally, you may specific the optional `$frame` parameter. When that's passed, it returns `true` if it has a `Turbo-Frame` header where the value matches the specified `$frame`. Otherwise, it will return `false`: 242 | 243 | ```php 244 | if (request()->wasFromTurboFrame(dom_id($post, 'create_comment'))) { 245 | // ... 246 | } 247 | ``` 248 | 249 | ### The `request()->wasFromHotwireNative()` macro 250 | 251 | The `request()->wasFromHotwireNative()` macro added to the request class will check if the request came from a Hotwire Native client and returns `true` or `false` accordingly. 252 | 253 | Hotwire Native clients are encouraged to override the `User-Agent` header in the WebViews to mention the words `Hotwire Native` on them. This is what this macro uses to detect if it came from a Hotwire Native client. 254 | 255 | ### The `response()->turboStream()` macro 256 | 257 | The `response()->turboStream()` macro works similarly to the `turbo_stream()` function above. It was only added to the response for convenience. 258 | 259 | ### The `response()->turboStreamView()` macro 260 | 261 | The `response()->turboStreamView()` macro works similarly to the `turbo_stream_view()` function above. It was only added to the response for convenience. 262 | -------------------------------------------------------------------------------- /docs/hotwire-native.md: -------------------------------------------------------------------------------- 1 | --- 2 | extends: _layouts.docs 3 | title: Hotwire Native 4 | description: Hotwire Native Helpers 5 | order: 11 6 | --- 7 | 8 | # Hotwire Native 9 | 10 | Hotwire also has a [mobile side](https://native.hotwired.dev/) and Turbo Laravel provides some helpers to help integrating with that. 11 | 12 | Turbo visits made by a Hotwire Native client should send a custom `User-Agent` header. Using that header, we can detect in the backend that a request is coming from a Hotwire Native client instead of a regular web browser. 13 | 14 | This is useful if you want to customize the behavior a little bit different based on that information. For instance, you may want to include some elements for mobile users, like a mobile-only CSS stylesheet, for instance. To do so, you may use the `@hotwirenative` Blade directive in your Blade views: 15 | 16 | ```blade 17 | @hotwirenative 18 | 19 | @endhotwirenative 20 | ``` 21 | 22 | Alternatively, you may want to include some elements only if the client requesting it is _NOT_ a Hotwire Native client using the `@unlesshotwirenative` Blade helpers: 23 | 24 | ```blade 25 | @unlesshotwirenative 26 |

Hello, Non-Hotwire Native Users!

27 | @endunlesshotwirenative 28 | ``` 29 | 30 | You may also check if the request was made from a Hotwire Native visit using the request macro: 31 | 32 | ```php 33 | if (request()->wasFromHotwireNative()) { 34 | // ... 35 | } 36 | ``` 37 | 38 | Or the Turbo Facade directly, like so: 39 | 40 | ```php 41 | use HotwiredLaravel\TurboLaravel\Facades\Turbo; 42 | 43 | if (Turbo::isHotwireNativeVisit()) { 44 | // ... 45 | } 46 | ``` 47 | 48 | ## Interacting With Hotwire Native Navigation 49 | 50 | Hotwire Native will hook into Turbo's visits so it displays them on mobile mimicking the mobile way of stacking screens instead of just replace elements on the same screen. This helps the native feel of our hybrid app. 51 | 52 | However, sometimes we may need to customize the behavior of form request handler to avoid a weird screen jumping effect happening on the mobile client. Instead of regular redirects, we can send some signals by redirecting to specific routes that are detected by the Hotwire Native client. 53 | 54 | For instance, if a form submission request came from a Hotwire Native client, the form was probably rendered on a native modal, which is not part of the screen stack, so we can just tell Turbo to `refresh` the current screen it has on stack instead. There are 3 signals we can send to the Hotwire Native client: 55 | 56 | | Signal | Route| Description| 57 | |---|---|---| 58 | | `recede` | `/recede_historical_location` | Go back to previous screen | 59 | | `resume` | `/resume_historical_location` | Stay on the current screen as is | 60 | | `refresh`| `/refresh_historical_location` | Stay on the current screen but refresh | 61 | 62 | Sending these signals is a matter of detecting if the request came from a Hotwire Native client and, if so, redirect the user to these signal URLs instead. The Hotwire Native client should detect the redirect was from one of these special routes and trigger the desired behavior. 63 | 64 | You may use the `InteractsWithHotwireNativeNavigation` trait on your controllers to achieve this behavior and fallback to a regular redirect if the request wasn't from a Hotwire Native client: 65 | 66 | ```php 67 | use HotwiredLaravel\TurboLaravel\Http\Controllers\Concerns\InteractsWithHotwireNativeNavigation; 68 | 69 | class TraysController extends Controller 70 | { 71 | use InteractsWithHotwireNativeNavigation; 72 | 73 | public function store() 74 | { 75 | // Tray creation... 76 | 77 | return $this->recedeOrRedirectTo(route('trays.show', $tray)); 78 | } 79 | } 80 | ``` 81 | 82 | In this example, when the request to create trays comes from a Hotwire Native client, we're going to redirect to the `/recede_historical_location` URL 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. 83 | 84 | There are a couple of redirect helpers available: 85 | 86 | ```php 87 | $this->recedeOrRedirectTo(string $url); 88 | $this->resumeOrRedirectTo(string $url); 89 | $this->refreshOrRedirectTo(string $url); 90 | $this->recedeOrRedirectBack(string $fallbackUrl, array $options = []); 91 | $this->resumeOrRedirectBack(string $fallbackUrl, array $options = []); 92 | $this->refreshOrRedirectBack(string $fallbackUrl, array $options = []); 93 | ``` 94 | 95 | It's common to flash messages using the `->with()` method of the Redirect response in Laravel. However, since a Hotwire Native request will never actually redirect somewhere where the flash message will be rendered, the behavior of the `->with()` method was slightly modified too. 96 | 97 | If you're setting flash messages like this after a form submission: 98 | 99 | ```php 100 | use HotwiredLaravel\TurboLaravel\Http\Controllers\Concerns\InteractsWithHotwireNativeNavigation; 101 | 102 | class TraysController extends Controller 103 | { 104 | use InteractsWithHotwireNativeNavigation; 105 | 106 | public function store() 107 | { 108 | // Tray creation... 109 | 110 | return $this->recedeOrRedirectTo(route('trays.show', $tray)) 111 | ->with('status', __('Tray created.')); 112 | } 113 | } 114 | ``` 115 | 116 | If a request was sent from a Hotwire Native client, the flashed messages will be added to the query string instead of flashed into the session like they'd normally be. In this example, it would redirect like this: 117 | 118 | ``` 119 | /recede_historical_location?status=Tray%20created. 120 | ``` 121 | 122 | In the Hotwire Native client, you should be able to intercept these redirects, retrieve the flash messages from the query string and create native toasts, if you'd like to. 123 | 124 | If the request wasn't from a Hotwire Native client, the message would be flashed into the session as normal, and the client would receive a redirect to the `trays.show` route in this case. 125 | 126 | If you don't want these routes enabled, feel free to disable them by commenting out the feature on your `config/turbo-laravel.php` file (make sure the Turbo Laravel configs are published): 127 | 128 | ```php 129 | return [ 130 | 'features' => [ 131 | // Features::hotwireNativeRoutes(), 132 | ], 133 | ]; 134 | ``` 135 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | extends: _layouts.docs 3 | title: Installation 4 | description: Install Turbo Laravel in your Laravel app 5 | order: 2 6 | --- 7 | 8 | # Installation 9 | 10 | Turbo Laravel can be installed via [Composer](https://getcomposer.org/): 11 | 12 | ```bash 13 | composer require hotwired-laravel/turbo-laravel 14 | ``` 15 | 16 | After installing the package, you may run the `turbo:install` Artisan command: 17 | 18 | ```bash 19 | php artisan turbo:install 20 | ``` 21 | 22 | This will add the Turbo.js dependency to your `package.json` file, when you're using Vite and NPM, or to your `routes/importmap.php` file, when it detects that you're using [Importmap Laravel](https://github.com/tonysm/importmap-laravel). It also publishes some files to your `resources/js` folder, which imports Turbo for you 23 | 24 | Note: Turbo used to work with Livewire, but somewhere around Livewire V3 the bridges stopped working. There's an open issue to investigate Livewire V3 compatibility. If you're into Livewire and would love to use Turbo in a Livewire app (maybe you want to augment your Livewire & Turbo app with Hotwire Native or something like that), you're welcome to check out the issue and try to bring the compatibility back. If you wanted an application scaffolding like Laravel Breeze or Laravel Jetstream, checkout Turbo Breeze, our fork of Breeze that sets up a fresh Laravel app using Stimulus, Importmaps, TailwindCSS (via the CLI), and Turbo. 25 | -------------------------------------------------------------------------------- /docs/known-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | extends: _layouts.docs 3 | title: Known Issues 4 | description: Known Issues 5 | order: 13 6 | --- 7 | 8 | # Known Issues 9 | 10 | If you ever encounter an issue with the package, look here first for documented solutions. 11 | 12 | ## Fixing Laravel's Previous URL Issue 13 | 14 | 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 storing Turbo Frames visits as Laravel's previous URL, head to the [issue](https://github.com/hotwired-laravel/turbo-laravel/issues/60#issuecomment-1123142591) where a solution was discussed. 15 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | extends: _layouts.docs 3 | title: Overview 4 | description: A quick overview of Hotwire 5 | order: 3 6 | --- 7 | 8 | # Overview 9 | 10 | It's highly recommended that you read the [Turbo Handbook](https://turbo.hotwired.dev/handbook/introduction) first before continuing here. However, a quick intro will be provided here and we'll link to the Turbo documentations when relevant. 11 | 12 | Turbo is the heart of Hotwire. In essence, it's a JavaScript library that turns regular web applications (aka. multi-page web applications) into something that _feels_ like a single-page application (SPA). 13 | 14 | It provides a bunch of components that allows us to build modern web applications with minimal JavaScript. It relies on sending **H**TML **O**ver **T**he **Wire** (hence the name), instead of JSON, which is how JavaScript-heavy web applications are built, typically consuming some sort of JSON API. 15 | 16 | When Turbo.js is started in the browser, it intercepts link clicks and form submissions to convert those into fetch requests (aka. AJAX) instead of letting the browser do a full page refresh. The component in Turbo that handles this behavior is called [Turbo Drive](https://turbo.hotwired.dev/handbook/drive). 17 | 18 | Turbo Drive will do the heavy-lifting of the _SPA feel_ in our application. Just by turning it on, the [perceived performance](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Performance/Perceived_performance) should be noticeable. The default behavior of Turbo will be to _replace_ the contents of the `` tag in our page with the one from the response it gets from the link or form submission. 19 | 20 | Additionally, since Turbo 8, we can also instruct Turbo to [_morph_ the page](https://turbo.hotwired.dev/handbook/page_refreshes) instead of just replacing its contents by adding a meta tag on the pages we can it enabled: 21 | 22 | ```html 23 | 24 | 25 | ``` 26 | 27 | Alternatively, Turbo Laravel provides some Blade components to make it easier (and autocomplete friendlier) to interact with these Turbo page configurations: 28 | 29 | ```blade 30 | 31 | ``` 32 | 33 | Turbo Drive does a lot for us, and with _morphing_ it gets even more powerful, but sometimes you can want to [decompose a page into independent sections](https://turbo.hotwired.dev/handbook/frames) (for different reasons, such as having more control over HTTP caching for these sections). For these use cases, Turbo offers _Turbo Frames_. 34 | 35 | Turbo Frames are custom HTML tags that Turbo provides. You can think of those as "modern iframes", if you will. When link clicks or form submissions happen inside of a Turbo Frame, instead of replacing or morphing the entire page, Turbo will only affect that specific Turbo Frame's content. It will do so by extracting a _matching Turbo Frame_ (one that has the same DOM ID) on the response. 36 | 37 | Here's how you can use Turbo Frames: 38 | 39 | ```html 40 | 41 |

Hello, World!

42 | 43 | Click me 44 |
45 | ``` 46 | 47 | Alternatively, you may want a Turbo Frame to immediately fetch its contents instead of waiting for a user interaction. For that, you may add a `[src]` attribute to the Turbo Frame tag with the URL of where Turbo should fetch that content from. This technique is called [Lazy-loading Turbo Frames](https://turbo.hotwired.dev/handbook/frames#lazy-loading-frames): 48 | 49 | ```blade 50 | 51 |

Loading...

52 |
53 | ``` 54 | 55 | A lazy-loaded Turbo Frame will dispatch a fetch request (aka. AJAX) as soon as it enters the DOM, replacing its contents with the contents of a matching Turbo Frame in the response HTML. Optionally, you may add a `[loading=lazy]` attribute to the lazy-loaded Turbo Frame so Turbo will only fetch its content when the Turbo Frame is visible (within the viewport): 56 | 57 | ```blade 58 | 59 |

Loading...

60 |
61 | ``` 62 | 63 | You may also trigger a Turbo Frame with forms and links that are _outside_ of the frame tag by adding a `[data-turbo-frame]` attribute in the link, form, or submit buttons, passing the ID of the Turbo Frame: 64 | 65 | ```blade 66 |
67 | I'm a link 68 | 69 | 70 | ... 71 | 72 |
73 | ``` 74 | 75 | Turbo Drive and Turbo Frames allows us to build A LOT of different sorts of interactions. However, sometimes you may want to update multiple sections of a page after a form submission, for instance. For those use cases, Turbo provides another custom HTML tag called [Turbo Streams](https://turbo.hotwired.dev/handbook/streams). 76 | 77 | All link clicks and form submissions that Turbo intercepts are annotated by Turbo, which tells our back-end application that Turbo is _on_, so we can return a special type of response that only contains Turbo Streams. Turbo.js will do so by adding a custom [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types) of `text/vnd.turbo-stream.html` to the [`Accept` HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept). 78 | 79 | Turbo Streams allows for a more fine-grained control over the page updates. For instance, here's an example of a Turbro Stream that appends a new comment to a comments section: 80 | 81 | ```html 82 | 83 | 86 | 87 | ``` 88 | 89 | The `[action=append]` will add the contents of what's inside the `` tag into the element that has a DOM ID matching the `[target=comments]` attribute, so `#comments` in this case. 90 | 91 | There are 8 _default_ Turbo Stream actions in Turbo: 92 | 93 | | Action | Description | 94 | |---|---| 95 | | `append` | Appends the contents of the `