diff --git a/.gitattributes b/.gitattributes index 99a9952e..beca096d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,5 @@ /.php_cs.dist export-ignore /psalm.xml export-ignore /psalm.xml.dist export-ignore +/workbench export-ignore +/testbench.yaml export-ignore diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index a83d7080..7520a186 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -1,21 +1,25 @@ -name: Check & fix styling +name: Fix PHP code style issues -on: [push] +on: + push: + paths: + - '**.php' + +permissions: + contents: write jobs: - php-cs-fixer: + php-code-styling: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: ${{ github.head_ref }} - - name: Run PHP CS Fixer - uses: docker://oskarstark/php-cs-fixer-ga - with: - args: --config=.php-cs-fixer.dist.php --allow-risky=yes + - name: Fix PHP code style issues + uses: aglipanci/laravel-pint-action@2.3.0 - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 28fbd458..40ff4590 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -8,22 +8,13 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, windows-latest] - php: [8.0, 8.1, 8.2] - laravel: [8.*, 9.*, 10.*] + os: [ubuntu-latest, macos-latest] + php: [8.2] + laravel: [10.*] stability: [prefer-lowest, prefer-stable] include: - - laravel: 9.* - testbench: 7.* - laravel: 10.* testbench: 8.* - - laravel: 8.* - testbench: "^6.24" - exclude: - - laravel: 10.* - php: 8.0 - - laravel: 8.* - php: 8.2 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} diff --git a/README.md b/README.md index af68039d..55720772 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,16 @@ Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed re ### Contributing +We're using [Workbench](https://github.com/orchestral/workbench), so we have a companion Laravel app for local development of the package living in the [workbench/](./workbench/) at the root level. + +You may run the Workbench app like this: + +```bash +composer serve +``` + +After that, the app should be available at [http://localhost:8000](http://localhost:8000). + Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. ### Security Vulnerabilities diff --git a/composer.json b/composer.json index 0ad77825..646ccce6 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "hotwired/turbo-laravel", + "name": "hotwired-laravel/turbo-laravel", "description": "Turbo Laravel gives you a set of conventions to make the most out of the Hotwire stack (inspired by turbo-rails gem).", "keywords": [ "hotwired", @@ -7,7 +7,7 @@ "turbo", "turbo-laravel" ], - "homepage": "https://github.com/tonysm/turbo-laravel", + "homepage": "https://github.com/hotwired-laravel/turbo-laravel", "license": "MIT", "authors": [ { @@ -18,28 +18,51 @@ } ], "require": { - "php": "^8.0|^8.1", - "illuminate/support": "^8.47|^9.0|^10.0" + "php": "^8.2", + "illuminate/support": "^10.0" }, "require-dev": { - "orchestra/testbench": "^6.24|^7.0|^8.0", - "phpunit/phpunit": "^9.5" + "laravel/pint": "^1.10", + "orchestra/testbench": "^8.14", + "orchestra/workbench": "^1.0", + "phpunit/phpunit": "^9.6" }, "autoload": { "psr-4": { - "Tonysm\\TurboLaravel\\": "src" + "HotwiredLaravel\\TurboLaravel\\": "src" }, - "files": ["src/helpers.php", "src/globals.php"] + "files": [ + "src/helpers.php", + "src/globals.php" + ] }, "autoload-dev": { "psr-4": { - "Tonysm\\TurboLaravel\\Tests\\": "tests" + "HotwiredLaravel\\TurboLaravel\\Tests\\": "tests", + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" } }, "scripts": { "psalm": "vendor/bin/psalm", "test": "vendor/bin/phpunit --colors=always", - "test-coverage": "vendor/bin/phpunit --coverage-html coverage" + "test-coverage": "vendor/bin/phpunit --coverage-html coverage", + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "build": "@php vendor/bin/testbench workbench:build --ansi", + "serve": [ + "@build", + "Composer\\Config::disableProcessTimeout", + "@php vendor/bin/testbench serve" + ], + "lint": [ + "@php vendor/bin/pint" + ] }, "config": { "sort-packages": true @@ -47,11 +70,11 @@ "extra": { "laravel": { "providers": [ - "\\Tonysm\\TurboLaravel\\TurboServiceProvider" + "\\HotwiredLaravel\\TurboLaravel\\TurboServiceProvider" ], "aliases": { - "Turbo": "\\Tonysm\\TurboLaravel\\Facades\\Turbo", - "TurboStream": "\\Tonysm\\TurboLaravel\\Facades\\TurboStream" + "Turbo": "\\HotwiredLaravel\\TurboLaravel\\Facades\\Turbo", + "TurboStream": "\\HotwiredLaravel\\TurboLaravel\\Facades\\TurboStream" } } }, diff --git a/config/turbo-laravel.php b/config/turbo-laravel.php index 51ce80b7..2eddf56d 100644 --- a/config/turbo-laravel.php +++ b/config/turbo-laravel.php @@ -1,6 +1,6 @@ - - -``` - -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: - -```blade - - - -``` - -Which will generate a `comments_post_123` DOM ID, assuming your Post model has an ID of `123`. - -## Blade Components - -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: - -```blade - - - -``` - -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: - -```blade - - - -``` - -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). - -[Continue to Helper Functions...](/docs/{{version}}/helper-functions) diff --git a/docs/broadcasting.md b/docs/broadcasting.md index c1ab138b..a2234e40 100644 --- a/docs/broadcasting.md +++ b/docs/broadcasting.md @@ -4,19 +4,21 @@ ## Introduction -So far, we have used Turbo Streams over HTTP to handle the case of updating multiple parts of the page for a single user after a form submission. In addition to that, you may want to broadcast model changes over WebSockets to all users that are viewing the same page. Although nice, **you don't have to use WebSockets if you don't have the need for it. You may still benefit from Turbo Streams over HTTP.** +So far, we've seen how to 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, you may also broadcast model changes over WebSockets (or Server-Sent Events) to all users that are viewing the same page. Although nice, **you don't have to use WebSockets if you don't have the need for it. You may still benefit from Turbo Streams over HTTP.** -We can broadcast to all users over WebSockets those exact same Turbo Stream tags we are returning to a user after a form submission. That makes use of Laravel Echo and Laravel's Broadcasting component. +The key here is that we'd broadcast those exact same Turbo Stream tags we've seen before. Remember, "HTML over the wire." Turbo Stream Broadcasts use [Laravel Echo](https://github.com/laravel/echo) and [Laravel's Broadcasting](https://laravel.com/docs/broadcasting) system. -You may still feed the user making the changes with Turbo Streams over HTTP and broadcast the changes to other users over WebSockets. This way, the user making the change will have an instant feedback compared to having to wait for a background worker to pick up the job and send it to them over WebSockets. +Since broadcasts are commonly triggered after a form submission from one user, I'd still recommend feeding that specific user back with Turbo Streams (or a redirect and let Turbo refresh/morph) and only send the Turbo Stream broadcasts to _other_ users most of the time. This way, the user making the change will have an instant feedback compared to having to wait for a background worker to pick up the job and send it to them over WebSockets. ## Configuration -Broadcasting Turbo Streams relies heavily on Laravel's [Broadcasting component](https://laravel.com/docs/broadcasting). This means you need to configure Laravel Echo in the frontend and either use Pusher or any other open-source replacement you want to. If you're not using Pusher, we recommend [Soketi](https://docs.soketi.app/) since it's easy to setup. +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 use [Pusher](https://pusher.com/) or any other open-source Pusher alternatives you may prefer. If you're not using Pusher, we recommend [Soketi](https://docs.soketi.app/) since it's easy to setup. ## Listening to Broadcasts -You may listen to a Turbo Stream broadcasts on your pages by adding the custom HTML tag `` that is published to your application's assets (see [here](https://github.com/tonysm/turbo-laravel/blob/main/stubs/resources/js/elements/turbo-echo-stream-tag.js)). You need to pass the channel you want to listen to broadcasts on using the `channel` attribute of this element, like so. +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)). + +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: ```blade ``` -You may prefer using the convenient `` Blade component, passing the model as the `source` prop to it, something like this: +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). 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): ```blade ``` -By default, it expects a private channel, so the it must be used in a page for already authenticated users. You may control the channel type in the tag with a `type` attribute. +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. ```blade ``` -To register the Broadcast Auth Route you may use Laravel's built-in conventions as well: +Make sure you have the Broadcast Auth Route for your models registered in your `routes/channels.php` file: ```php -// file: routes/channels.php - use App\Models\Post; use App\Models\User; use Illuminate\Support\Facades\Broadcast; @@ -54,10 +54,10 @@ You may want to read the [Laravel Broadcasting](https://laravel.com/docs/broadca ## Broadcasting Model Changes -With Laravel Echo properly configured, you may now broadcast model changes using WebSockets. First thing you need to do is use the `Broadcasts` trait in your model: +To be broadcast model changes for a particular, you must add the `Broadcasts` trait to your models: ```php -use Tonysm\TurboLaravel\Models\Broadcasts; +use HotwiredLaravel\TurboLaravel\Models\Broadcasts; class Comment extends Model { @@ -65,13 +65,13 @@ class Comment extends Model } ``` -This trait will add some methods to your model that you can use to trigger broadcasts. Here's how you can broadcast appending a new comment to all users visiting the post page: +This trait will augment any model with Turbo Stream broadacsting methods that you may use to trigger broadcasts. Here's how you can broadcast an `append` Turbo Stream for a newly created comment to all users visiting the post page: ```php Route::post('posts/{post}/comments', function (Post $post) { $comment = $post->comments()->create(/** params */); - $comment->broadcastAppend()->later(); + $comment->broadcastAppend()->toOthers()->later(); if (request()->wantsTurboStream()) { return turbo_stream($comment); @@ -91,9 +91,10 @@ $comment->broadcastAfter('target_dom_id'); $comment->broadcastReplace(); $comment->broadcastUpdate(); $comment->broadcastRemove(); +$comment->broadcastRefresh(); ``` -These methods will assume you want to broadcast the Turbo Streams to your model's channel. However, you will also find alternative methods where you can specify either a model or the broadcasting channels you want to send the broadcasts to: +These methods will assume you want to broadcast to your model's channel. However, you may want to send these broadcasts to a related model's channel instead: ```php $comment->broadcastAppendTo($post); @@ -103,11 +104,12 @@ $comment->broadcastAfterTo($post, 'target_dom_id'); $comment->broadcastReplaceTo($post); $comment->broadcastUpdateTo($post); $comment->broadcastRemoveTo($post); +$comment->broadcastRefreshTo($post); ``` -These `broadcastXTo()` methods accept either a model, a channel instance or an array containing both of these. When it receives a model, it will guess the channel name using the broadcasting channel convention (see [#conventions](#conventions)). +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. 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). -All of these broadcasting methods return an instance of a `PendingBroadcast` class that will only dispatch the broadcasting job when that pending object is being garbage collected. Which means that you can control a lot of the properties of the broadcast by chaining on that instance before it goes out of scope, like so: +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: ```php $comment->broadcastAppend() @@ -116,11 +118,11 @@ $comment->broadcastAppend() 'comment' => $comment, 'post' => $post, ]) - ->toOthers() // Do not send to the current user. - ->later(); // Dispatch a background job to send. + ->toOthers() // Do not send to the current user... + ->later(); // Don't send it now, dispatch a job to send in background instead... ``` -You may want to hook those methods in the model events of your model to trigger Turbo Stream broadcasts whenever your models are changed in any context, such as: +You may want to hook these broadcasts from your [model's events](https://laravel.com/docs/10.x/eloquent#events) to trigger Turbo Stream broadcasts whenever your models are changed in any context: ```php class Comment extends Model @@ -130,27 +132,21 @@ class Comment extends Model protected static function booted() { static::created(function (Comment $comment) { - $comment->broadcastPrependTo($comment->post) - ->toOthers() - ->later(); + $comment->broadcastPrependTo($comment->post)->later(); }); static::updated(function (Comment $comment) { - $comment->broadcastReplaceTo($comment->post) - ->toOthers() - ->later(); + $comment->broadcastReplaceTo($comment->post)->later(); }); static::deleted(function (Comment $comment) { - $comment->broadcastRemoveTo($comment->post) - ->toOthers() - ->later(); + $comment->broadcastRemoveTo($comment->post)->later(); }); } } ``` -In case you want to broadcast all these changes automatically, instead of specifying them all, you may want to add a `$broadcasts` property to your model, which will instruct the `Broadcasts` trait to trigger the Turbo Stream broadcasts for the created, updated and deleted model events, like so: +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 Tubro Stram broadcasts on the correct events: ```php class Comment extends Model @@ -161,7 +157,20 @@ class Comment extends Model } ``` -This will achieve almost the same thing as the example where we registered the model events manually, with a couple nuanced differences. First, by default, it will broadcast an `append` Turbo Stream to newly created models. You may want to use `prepend` instead. You can do so by using an array with a `insertsBy` key and `prepend` action as value instead of a boolean, like so: +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`: + +```php +class Comment extends Model +{ + use Broadcasts; + + protected $broadcasts = [ + 'insertsBy' => 'prepend', + ]; +} +``` + +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: ```php class Comment extends Model @@ -170,13 +179,14 @@ class Comment extends Model protected $broadcasts = [ 'insertsBy' => 'prepend', + 'stream' => 'my-comments', ]; } ``` -This will also automatically hook into the model events, but instead of broadcasting new instances as `append` it will use `prepend`. +This will send the Turbo Stream broadcast to private channel called `my-comments` when a new comment is created. -Secondly, it will send all changes to this model's broadacsting channel, except for when the model is created (since no one would be listening to its direct channel). In our case, we want to direct the broadcasts to the post linked to this model instead. We can achieve that by adding a `$broadcastsTo` property to the model, like so: +Alternatively, you may also set a `$broadcastsTo` proprety 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: ```php class Comment extends Model @@ -196,9 +206,7 @@ class Comment extends Model } ``` -That property can either be a string that contains the name of a relationship of this model or an array of relationships. - -Alternatively, you may prefer to have more control over where these broadcasts are being sent to by implementing a `broadcastsTo` method in your model instead of using the property. This way, you can return a single model, a broadcasting channel instance or an array containing either of them, like so: +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: ```php use Illuminate\Broadcasting\Channel; @@ -227,45 +235,115 @@ class Comment extends Model } ``` -Newly created models using the auto-broadcasting feature will be broadcasted to a pluralized version of the model's basename. So if you have a `App\Models\PostComment`, you may expect broadcasts of newly created models to be sent to a private channel called `post_comments`. Again, this convention is only valid for newly created models. Updates/Removals will still be sent to the model's own private channel by default using Laravel's convention for channel names. You may want to specify the channel name for newly created models to be broadcasted to with the `stream` key: +Having a `$broadcastsTo` property or implementing the `broadcastsTo()` method in your model will have precedence over the `stream` key of the `$broadcasts` property. + +## Broadcasting Page Refreshes + +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: ```php +use Illuminate\Broadcasting\Channel; + class Comment extends Model { use Broadcasts; - protected $broadcasts = [ - 'stream' => 'some_comments', - ]; + protected $broadcastsRefreshes = true; } ``` -Having a `$broadcastsTo` property or implementing the `broadcastsTo()` method in your model will have precedence over this, so newly created models will be sent to the channels specified on those places instead of using the convention or the `stream` option. +This is the same as doing: -## Broadcasting Turbo Streams to Other Users Only +```php +use Illuminate\Broadcasting\Channel; + +class Comment extends Model +{ + use Broadcasts; + + public static function booted() + { + static::created(function ($comment) { + $comment->broadcastRefreshTo("comments")->later(); + }); + + static::updated(function ($comment) { + $comment->broadcastRefresh()->later(); + }); -As mentioned erlier, you may want to feed the current user with Turbo Streams using HTTP requests and only send the broadcasts to other users. There are a couple ways you can achieve that. + static::deleted(function ($comment) { + $comment->broadcastRefresh(); + }); + } +} +``` -First, you can chain on the broadcasting methods, like so: +You may want to broadcast page refreshes to a related model: ```php -$comment->broadcastAppendTo($post) - ->toOthers(); +use Illuminate\Broadcasting\Channel; + +class Comment extends Model +{ + use Broadcasts; + + protected $broadcastsRefreshes = true; + + protected $broadcastsRefreshesTo = ['post']; + + public function post() + { + return $this->belongsTo(Post::class); + } +} ``` -Second, you can use the Turbo Facade like so: +This will send page refreshes broadcasts to the related `Post` model channel. + +Alternatively, you may specific a `broadcastsRefreshesTo` method instead of a property: ```php -use Tonysm\TurboLaravel\Facades\Turbo; +use Illuminate\Broadcasting\Channel; + +class Comment extends Model +{ + use Broadcasts; + + protected $broadcastsRefreshes = true; + + public function post() + { + return $this->belongsTo(Post::class); + } + + public function broadcastsRefreshesTo() + { + return [$this->post]; + } +} +``` + +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. + +## Broadcasting Turbo Streams to Other Users Only + +As mentioned erlier, 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: + +```php +$comment->broadcastAppendTo($post)->toOthers(); +``` + +Alternatively, you may use the Turbo Facade like so to configure a scope where all brodcasted Turbo Streams will be sent to other users only: + +```php +use HotwiredLaravel\TurboLaravel\Facades\Turbo; Turbo::broadcastToOthers(function () { // ... }); ``` -This way, any broadcast that happens inside the scope of the Closure will only be sent to other users. - -Third, you may use that same method but without the Closure inside a ServiceProvider, for instance, to instruct the package to only send turbo stream broadcasts to other users globally: +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: ```php to('general'); ``` -As for the channel, you may pass a string that will be interpreted as a public channel name, an Eloquent model which will expect a private channel using that model's broadcasting channel convention, or instances of the `Illuminate\Broadcasting\Channel` class. +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. -You may want to specify private or presence string channels, which you may do like so: +You may want to specify private or presence string channels instead of public ones: ```php TurboStream::broadcastAppend('Hello World') @@ -355,7 +436,7 @@ TurboStream::broadcastAppend('Hello World') ->toPresenceChannel('user.123'); ``` -Alternatively to broadcasting any of the 7 default broadcasting actions, you may want to broadcast custom Turbo Stream actions, which you can do by using the `broadcastAction()` method directly (which is the same method used by the other default ones): +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: ```php TurboStream::broadcastAction('scroll_to', target: 'todo_123'); @@ -363,7 +444,7 @@ TurboStream::broadcastAction('scroll_to', target: 'todo_123'); ## Handmade Broadcasting Using The `turbo_stream()` Response Builder -Alternatively to use the `TurboStream` Facade (or Factory type-hint), you may also broadcast directly from the `turbo_stream()` function response builder: +One more alternative to broadcasting Turbo Streams is to call the `broadcastTo()` method on the returned object of the `turbo_stream()` function: ```php turbo_stream() @@ -371,7 +452,7 @@ turbo_stream() ->broadcastTo('general'); ``` -This will tap on the `PendingTurboStreamResponse` and create a `PendingBroadcast` from the Turbo Stream you configured. 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 may pass a `Closure` as the second parameter: +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: ```php turbo_stream() @@ -379,7 +460,7 @@ turbo_stream() ->broadcastTo('general', fn ($broadcast) => $broadcast->toOthers()); ``` -You may pass a string, an Eloquent model, or an instance of the `Illuminate\Broadcasting\Channel` class as the channel: +The first argument must be either a string, an Eloquent model, or an instance of the `Illuminate\Broadcasting\Channel` class as the channel: ```php turbo_stream($comment) @@ -389,15 +470,15 @@ turbo_stream($comment) Similarly to using the Facade, you may also want to broadcast to private or presence string channels like so: ```php -// To private channels... +// Broadcast to private channels... turbo_stream() ->append('notifications', 'Hello World') ->broadcastToPrivateChannel('user.123', fn ($broadcast) => $broadcast->toOthers()) -// To presence channels... +// Broadcast to presence channels... turbo_stream() ->append('notifications', 'Hello World') ->broadcastToPresenceChannel('chat.123', fn ($broadcast) => $broadcast->toOthers()); ``` -[Continue to Livewire Integration...](/docs/{{version}}/livewire) +[Continue to Validation Response Redirects...](/docs/{{version}}/validation-response-redirects) diff --git a/docs/conventions.md b/docs/conventions.md index 974f6943..82d3a903 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -8,11 +8,13 @@ The conventions described below are **NOT mandatory**. Feel free to pick what yo ## Resource Routes -Laravel supports [resource routes](https://laravel.com/docs/controllers#resource-controllers) and that plays really well with Hotwire for most things. This creates routes names such as `posts.index`, `posts.store`, etc. +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. -If you don't want to use resource routes, at least consider using the naming convention: render forms in a route name ending in `.create` or `.edit` and name their handler routes ending with `.store` or `.update`. +If you don't want to use resource routes, at least consider using the naming convention: render forms in a route names ending in `.create`, `.edit`, or `.delete`, and name their handler routes ending with `.store`, `.update`, or `.destroy`, accordingly. -You may want to define exceptions to the route guessing behavior. In that case, you may want to set the `redirect_guessing_exceptions` in the `config/turbo-laravel.php` config file: +Turbo Laravel uses this naming convention so it doesn't redirect after failed valitions 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. + +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: ```php return [ @@ -23,16 +25,18 @@ return [ ]; ``` -When using this config, the redirection behavior will still happen, but the package will not guess routes. See the [Validation Response Redirects](/docs/{{version}}/validation-response-redirects) page to know more about why this happens. +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/{{version}}/validation-response-redirects) page to know more about why this happens. ## Partials -You may want to split up your views in smaller chunks (aka. "partials"), such as `comments/_comment.blade.php` which displays a comment resource, or `comments/_form.blade.php` for the form to either create/update comments. This will allow you to reuse these _partials_ in [Turbo Streams](/docs/{{version}}/turbo-streams). +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/{{version}}/turbo-streams). -The models' partials (such as the `comments/_comment.blade.php` for a `Comment` model) may only rely on having a single `$comment` instance variable passed to them. That's because the package will, by default, 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, that's by default, you can customize most things. Read the [Broadcasting](/docs/{{version}}/broadcasting) section to know more. +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/{{version}}/broadcasting) section to know more. ## Turbo Stream Channel Names +_Note: Turbo Stream Broadcasts are optional._ + 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. -[Continue to Blade Helpers...](/docs/{{version}}/blade-helpers) +[Continue to Helpers...](/docs/{{version}}/helpers) diff --git a/docs/csrf.md b/docs/csrf.md index c3e6b734..09d1afc1 100644 --- a/docs/csrf.md +++ b/docs/csrf.md @@ -1,16 +1,18 @@ # CSRF Protection -Laravel has built-in CSRF protection in place. It essentially prevents our app from processing any non-GET requests that don't have a valid CSRF Token in them. So, to allow a POST form to be processed, we usually need to add a `@csrf` Blade directive to our forms: +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. + +So, to allow a POST form to be processed, we usually need to add a `@csrf` Blade directive to our forms: ```blade
@csrf - +
``` -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. Instead, Turbo.js is smart enough to read your page's meta tags, look for one named `csrf-token` and use its contents. Jetstream and Breeze both ship with such element, but in case you're missing it in your views, it should look like this: +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: ```blade diff --git a/docs/helper-functions.md b/docs/helper-functions.md deleted file mode 100644 index a6abee77..00000000 --- a/docs/helper-functions.md +++ /dev/null @@ -1,71 +0,0 @@ -# Helper Functions - -[TOC] - -## Introduction - -The package ships with a set of helper functions. These functions are all namespaced under `Tonysm\\TurboLaravel\\` but we also add them globally for convenience. - -## The `dom_id()` - -The mentioned namespaced `dom_id()` helper function may also be used from anywhere in your application, like so: - -```php -use function Tonysm\TurboLaravel\dom_id; - -dom_id($comment); -``` - -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. - -These helpers strip out the model's FQCN (see [config/turbo-laravel.php](https://github.com/tonysm/turbo-laravel/blob/main/config/turbo-laravel.php) if you use an unconventional location for your models). - -## The `dom_class()` - -The `dom_class()` helper function may be used from anywhere in your application, like so: - -```php -use function Tonysm\TurboLaravel\dom_class; - -dom_class($comment); -``` - -This function will generate the DOM class named based on your model's classname. If you have an instance of a `App\Models\Comment` model, it will generate a `comment` DOM class. - -Similarly to the `dom_id()` function, you may also pass a context prefix as the second parameter: - -```php -dom_class($comment, 'reactions_list'); -``` - -This will generate a DOM class of `reactions_list_comment`. - -## The `turbo_stream()` - -You may generate Turbo Streams using the `Response::turboStream()` macro, but you may also do so using the `turbo_stream()` helper function: - -```php -use function Tonysm\TurboLaravel\turbo_stream; - -turbo_stream()->append($comment); -``` - -Both the `Response::turboStream()` and the `turbo_stream()` function work the same way. The `turbo_stream()` function may be easier to use. - -## The `turbo_stream_view()` - -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: - -```php -use function Tonysm\TurboLaravel\turbo_stream_view; - -return turbo_stream_view('comments.turbo.created', [ - 'comment' => $comment, -]); -``` - ---- - -All these functions are also registered globally, so you may use it directly without the `use` statements (this is useful in contexts like Blade views, for instance). - -[Continue to Turbo Streams...](/docs/{{version}}/turbo-streams) diff --git a/docs/helpers.md b/docs/helpers.md new file mode 100644 index 00000000..77989aa6 --- /dev/null +++ b/docs/helpers.md @@ -0,0 +1,188 @@ +# Helpers + +[TOC] + +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. + +## Blade Directives + +### The `@domid()` Blade Directive + +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: + +```blade + + + +``` + +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: + +```blade + + + +``` + +Which will generate a `comments_post_123` DOM ID, assuming your Post model has an ID of `123`. + +## Blade Components + +### The `` Blade Component + +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: + +```blade + + + +``` + +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: + +```blade + + + +``` + +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). + +### The `` Blade Component + +If you're rendering a Turbo Stream inside a your Blade files, you may use the `` helper: + +```blade + + @include('posts._post', ['post' => $post]) + +``` + +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. + +### The `` Blade Component + +We can configure which update method Turbo should so to update the document: + +| Method | Description | +|---|---| +| `replace` | Updates the entire body of the document on Turbo Visits | +| `morph` | Uses DOM morphing to update the document instead of replacing everything | + +You can also configure the scroll behavior on Turbo: + +| Behavior | Description | +|---|---| +| `reset` | Resets the scroll position to the top, mimicking for the browser handles new page visits | +| `preserve` | Preserves the current scroll position (usually results in a better UX when used with the `morph` method) | + +You may use the `` component in your main layout's `` tag or on specific pages to configure how Turbo should update the page. Here's an example: + +```blade + +``` + +This will render two HTML `` tags: + +```html + + +``` + +## Helper Functions + +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). + +### The `dom_id()` + +The mentioned namespaced `dom_id()` helper function may also be used from anywhere in your application, like so: + +```php +use function HotwiredLaravel\TurboLaravel\dom_id; + +dom_id($comment); +``` + +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. + +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). + +### The `dom_class()` + +The `dom_class()` helper function may be used from anywhere in your application, like so: + +```php +use function HotwiredLaravel\TurboLaravel\dom_class; + +dom_class($comment); +``` + +This function will generate the DOM class named based on your model's classname. If you have an instance of a `App\Models\Comment` model, it will generate a `comment` DOM class. + +Similarly to the `dom_id()` function, you may also pass a context prefix as the second parameter: + +```php +dom_class($comment, 'reactions_list'); +``` + +This will generate a DOM class of `reactions_list_comment`. + +### The `turbo_stream()` + +You may generate Turbo Streams using the `Response::turboStream()` macro, but you may also do so using the `turbo_stream()` helper function: + +```php +use function HotwiredLaravel\TurboLaravel\turbo_stream; + +turbo_stream()->append($comment); +``` + +Both the `Response::turboStream()` and the `turbo_stream()` function work the same way. The `turbo_stream()` function may be easier to use. + +### The `turbo_stream_view()` + +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: + +```php +use function HotwiredLaravel\TurboLaravel\turbo_stream_view; + +return turbo_stream_view('comments.turbo.created', [ + 'comment' => $comment, +]); +``` + +## Request & Response Macros + +### The `request()->wantsTurboStream()` macro + +The `request()->wantsTurboStream()` macro added to the request class will check if the request accepts Turbo Stream and return `true` or `false` accordingly. + +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. + +### The `request()->wasFromTurboFrame()` macro + +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. + +Aditionally, 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`: + +```php +if (request()->wasFromTurboFrame(dom_id($post, 'create_comment'))) { + // ... +} +``` + +### The `request()->wasFromTurboNative()` macro + +The `request()->wasFromTurboNative()` macro added to the request class will check if the request came from a Turbo Native client and returns `true` or `false` accordingly. + +Turbo Native clients are encouraged to override the `User-Agent` header in the WebViews to mention the words `Turbo Native` on them. This is what this macro uses to detect if it came from a Turbo Native client. + +### The `response()->turboStream()` macro + +The `response()->turboStream()` macro works similarly to the `turbo_stream()` function above. It was only added to the response for convenience. + +### The `response()->turboStreamView()` macro + +The `response()->turboStreamView()` macro works similarly to the `turbo_stream_view()` function above. It was only added to the response for convenience. + +[Continue to Turbo Frames...](/docs/{{version}}/turbo-frames) diff --git a/docs/index.md b/docs/index.md index f4ca3aec..8ec4b7fe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,13 +1,14 @@ +* Prologue + * [Upgrade Guide](/docs/{{version}}/upgrade) * Getting Started * [Installation](/docs/{{version}}/installation) * [Overview](/docs/{{version}}/overview) * [Conventions](/docs/{{version}}/conventions) * The Essentials - * [Blade Helpers](/docs/{{version}}/blade-helpers) - * [Helper Functions](/docs/{{version}}/helper-functions) + * [Helpers](/docs/{{version}}/helpers) + * [Turbo Frames](/docs/{{version}}/turbo-frames) * [Turbo Streams](/docs/{{version}}/turbo-streams) * [Broadcasting](/docs/{{version}}/broadcasting) - * [Livewire Integration](/docs/{{version}}/livewire) * [Validation Response Redirects](/docs/{{version}}/validation-response-redirects) * [CSRF Protection](/docs/{{version}}/csrf) * [Turbo Native](/docs/{{version}}/turbo-native) diff --git a/docs/installation.md b/docs/installation.md index a8c20141..daa82193 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,37 +1,27 @@ # Installation -Turbo Laravel may be installed via Composer: +Turbo Laravel can be installed via [Composer](https://getcomposer.org/): ```bash -composer require hotwired/turbo-laravel +composer require hotwired-laravel/turbo-laravel:2.x-dev ``` -After installing, you may execute the `turbo:install` Artisan command, which will add a couple JS dependencies to your `package.json` file (when you're using Vite and NPM) or to your `routes/importmap.php` file (when you're using [Importmap Laravel](https://github.com/tonysm/importmap-laravel)), publish some JS scripts to your `resources/js` folder that configure Turbo.js for you: +After installing the package, you may run the `turbo:install` Artisan command. This command 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: ```bash php artisan turbo:install ``` -If you are using Jetstream with Livewire, you may add the `--jet` flag to the `turbo:install` Artisan command, which will add a couple more JS dependencies to make sure Alpine.js works nicely with Turbo.js. This will also change the layout that ships with Jetstream files a bit, which will make sure Livewire works nicely as well: +If you're using [Laravel Jetstream](https://jetstream.laravel.com/introduction.html) with [Livewire](https://livewire.laravel.com/), you may add the `--jet` flag to the `turbo:install` Artisan command. This flag will add some required JS dependencies to make sure [Alpine.js](https://alpinejs.dev/) works well with Turbo. It also changes the layout files that ships with Jetstream adding the [Livewire Turbo Plugin](https://github.com/livewire/turbolinks) to make sure Livewire works well with Turbo, too: ```bash php artisan turbo:install --jet ``` -When using Jetstream with Livewire, the [Livewire Turbo Plugin](https://github.com/livewire/turbolinks) is needed so Livewire works nicely with Turbo. This one will be added to your Jetstream layouts as script tags fetching from a CDN (both `app.blade.php` and `guest.blade.php`). - -If you're not using [Importmap Laravel](https://github.com/tonysm/importmap-laravel), the install command will tell you to pull and compile the assets before proceeding: - -```bash -npm install && npm run build -``` - -You may also optionally install [Alpine.js](https://alpinejs.dev/) in a non-Jetstream context (maybe you're more into [Breeze](https://laravel.com/docs/9.x/starter-kits#laravel-breeze)) passing `--alpine` flag to the `turbo:install` Artisan command: +If you're using [Alpine.js](https://alpinejs.dev/) in a non-Jetstream context (maybe you're more into [Breeze](https://laravel.com/docs/starter-kits#laravel-breeze)), you may use the `--alpine` flag in the `turbo:install` Artisan command: ```bash php artisan turbo:install --alpine ``` -_Note: the `--jet` option also adds all the necessary Alpine dependencies since Jetstream depends on Alpine._ - [Continue to Overview...](/docs/{{version}}/overview) diff --git a/docs/known-issues.md b/docs/known-issues.md index 5a993a74..0d14d7fe 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -8,4 +8,4 @@ If you ever encounter an issue with the package, look here first for documented ## 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. +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/hotwired-laravel/turbo-laravel/issues/60#issuecomment-1123142591) where a solution was discussed. diff --git a/docs/livewire.md b/docs/livewire.md deleted file mode 100644 index 4c42ce64..00000000 --- a/docs/livewire.md +++ /dev/null @@ -1,104 +0,0 @@ -# Livewire - -[TOC] - -Hotwire and Livewire can be used together. However, you need to add a JS plugin to make this happen. - -## The Livewire/Turbo Plugin - -Livewire has an [official plugin](https://github.com/livewire/turbolinks) that bridges these worlds. It was made for when Turbo.js was called Turbolinks, but it got updated when Hotwire came alive and Turbolinks was renamed to Turbo.js. - -To use it, all we need to do is add the CDN script after the Livewire Scripts, something like this: - -```blade - - - @livewireScripts - - -``` - -When you install Turbo Laravel using the `--jet` flag, this gets automatically added to your `app` and `guest` layouts, since Jetstream uses Livewire. - -## Deeper Integration - -It's possible to get a deeper integration between Livewire and Hotwire using Turbo Streams. Although, we haven't seen any advancements on that front. There's an example of such integration in the [Turbo Demo App](https://github.com/tonysm/turbo-demo-app). - -In the example, there's a Counter Livewire component that has `increment` and `decrement` methods. When those are triggered, it manipulates the counter and then dispatches a browser event with a rendered Turbo Stream to update a portion of the page that's outside of the scope of the Counter Livewire component: - -```php -counter++; - - $this->dispatchBrowserEvent('turboStreamFromLivewire', [ - 'message' => view('livewire.counter_stream', ['counter' => $this->counter])->render(), - ]); - } - - public function decrement() - { - $this->counter--; - - $this->dispatchBrowserEvent('turboStreamFromLivewire', [ - 'message' => view('livewire.counter_stream', ['counter' => $this->counter])->render(), - ]); - } - - public function render() - { - return view('livewire.counter'); - } -} -``` - -Then, we can create a custom HTML element that listens to the `turboStreamFromLivewire` event in the window that simply applies the Turbo Streams that comes from our Livewire Components, something like: - -```js -import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo" - -class TurboLivewireStreamSourceElement extends HTMLElement { - async connectedCallback() { - connectStreamSource(this) - window.addEventListener('turboStreamFromLivewire', this.dispatchMessageEvent.bind(this)); - } - - disconnectedCallback() { - disconnectStreamSource(this) - window.removeEventListener('turboStreamFromLivewire', this.dispatchMessageEvent.bind(this)); - } - - dispatchMessageEvent(data) { - const event = new MessageEvent("message", { data: data.detail.message }) - return this.dispatchEvent(event) - } -} - -if (customElements.get('turbo-livewire-stream-source') === undefined) { - customElements.define('turbo-livewire-stream-source', TurboLivewireStreamSourceElement) -} -``` - -Now, we can use this element in a page where we want to have the integration between Livewire and Turbo.js (or in a base layout if you want it applied application-wide): - -```blade - - - -``` - -That's it! With that, we got Livewire to generate Turbo Streams, dispatch it as a browser event, which gets intercepted by our custom HTML element and applied to the page! - -This is only an example of what a deeper integration could look like. - -[Continue to Validation Response Redirects...](/docs/{{version}}/validation-response-redirects) diff --git a/docs/overview.md b/docs/overview.md index 7a5a3e13..f9bed08b 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,12 +1,14 @@ # Overview -When Turbo.js is installed, Turbo Drive will be enabled by default. Turbo Drive will turn links and form submissions into fetch requests (AJAX) and will replace the page with the response it gets. +It's recommended to read the entire [Turbo Handbook](https://turbo.hotwired.dev/handbook/introduction) before diving here. But here's a quick intro. -You will also have Turbo-specific custom HTML tags that you may use in your views to enhance the user experience: Turbo Frames and Turbo Streams. This is vanilla Hotwire. It's recommended to read the [Turbo Handbook](https://turbo.hotwired.dev/handbook/introduction). Once you understand how these few pieces work together, the challenge will be in decomposing your UI to work as you want them to. +Turbo offers a set of components that helps us building modern web applications with minimal JavaScript. It relies on sending HTML Over The Wire (that's the the name comes from) instead of JSON, which is how common JavaScript-heavy web applications do. -Turbo also allows you to persist across visits. If you want that to happen, you may annotate these elements with a DOM ID and add the `data-turbo-permanent` custom attribute to them. As long as the response also contains an element with the same DOM ID and `data-turbo-permanent` attribute, Turbo will not touch it. +When Turbo is started in the browser, it will intercet app link clicks and form submissions and convert those into fetch requests (aka. AJAX) instead of letting the browser do a full page refresh. The component in Turbo that handles this is called [Turbo Drive](https://turbo.hotwired.dev/handbook/drive). -Turbo Drive is nice when you want to have a full page visit. That's not what we always want, though. Sometimes we want to only swap a fragment of the page and keep everything else as is. That's what [Turbo Frames](https://turbo.hotwired.dev/handbook/frames) are all about. Links and Form submissions that are trapped inside a Turbo Frame tag (or that point to one using a `data-turbo-frame` attribute) will instruct Turbo Drive to **NOT** replace the entire body of the document, but instead to look for a matching Turbo Frame in the response using its DOM ID and replace that specific portion of the page. +Turbo Drive is nice when you want to have a full page visits. However, there are times where we want to focus the update on specific elements on the page, without affecting the other existing elements. To achieve that, Turbo adds two new custom HTML tags, which we can use to enchance the User Experience (UX): [Turbo Frames](https://turbo.hotwired.dev/handbook/frames) (``) and Turbo Streams (``). This is all available in the browser when we install Turbo, we only have to use these new tags if we want to. + +Turbo Frames allows us to scope a Turbo visit to a specific `` element. When inside of a Turbo Frame tag, link clicks and form submissions will **NOT** replace the entire body of the document, but instead Turbo will look for a matching Turbo Frame tag in the response using its DOM ID and replace that specific portion of the page. Here's how you can use Turbo Frames: @@ -19,17 +21,23 @@ Here's how you can use Turbo Frames: ``` -Turbo Frames also allows you to lazy-load the frame's content. You may do so by adding a `src` attribute to the Turbo Frame tag. The content of a lazy-loading Turbo Frame tag can be used to indicate "loading states": +We may also configure Turbo Frames to lazy-load its contents by adding a `src` attribute to the Turbo Frame tag. The content of a lazy-loading Turbo Frame tag can be used to indicate "loading states": ```blade - +

Loading...

``` -Turbo will automatically dispatch a fetch request (AJAX) as soon as a lazy-loading Turbo Frame enters the DOM and replace its content with a matching Turbo Frame in the response. +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. We may defer the request so it's only dispatched when the Turbo Frame element is visible in the viewport with by setting the `loading="lazy"` attribute: + +```blade + +

Loading...

+
+``` -As mentioned earlier, you may also trigger a Turbo Frame with forms and links that are _outside_ of such frames by pointing to them with a `data-turbo-frame` attribute: +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: ```blade
@@ -41,7 +49,52 @@ As mentioned earlier, you may also trigger a Turbo Frame with forms and links th
``` -You could also "hide" this link and trigger a "click" event with JavaScript programmatically to trigger the Turbo Frame to reload, for example. +Turbo Streams, the other custom HTML tag that ships with Turbo, is a bit different. We can use Turbo Stream tags to trigger some actions on the document. For instance, here's Turbo Stream that appends a new comment into the comments list that already lives on the page: + +```html + + + +``` + +In this case, this new comment of DOM ID `#comment_123` will be _appended_ into the list of comments, which has the `#comments` DOM ID. All the default Turbo Stream actions, except the `refresh` one, require a `target` or a `targets` attribute. The difference here is that if you use the `target` attribute, it expects a DOM ID of the target element, and if you use the `targets` attribute, it expects a CSS selector of the target(s) element(s). + +There are 8 default Turbo Stream actions in Turbo: + +| Action | Description | +|---|---| +| `append` | Appends the contents of the `