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
```
-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
+
+
+
+
Lorem ipsum...
+
+
+
+```
+
+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 `` tag into the target or targets |
+| `prepend` | Prepends the contents of the `` tag to the target or targets |
+| `update` | Updates the target or targets with the contents of the `` tag (keeps the targeted elements around) |
+| `replace` | Replaces the target or targets with the contents of the `` tag (actually removes the targets) |
+| `before` | Inserts the contents of the `` tag _before_ the targeted elements |
+| `after` | Inserts the contents of the `` tag _after_ the targeted elements |
+| `remove` | Removes the targeted elements (doesn't require a `` tag) |
+| `refresh` | Signals to Turbo Drive to do a page refresh (doesn't require a `` tag, nor "target") |
+
+All of the default actions require a the contents of the new or updated element to be wrapped in a `` tag, except `remove` and `refresh`. That's because Turbo Stream tags can be activated by simply adding them to the document. They'll get activate based on the action and then get removed from the DOM. Having the `` ensure the content is not visible in the browser as it gets activated, only after.
+
+I keep saying "default action", well, that's because Turbo allows us to create our own [custom actions](https://turbo.hotwired.dev/handbook/streams#custom-actions):
+
+```js
+import { StreamActions } from "@hotwired/turbo"
+
+StreamActions.log = function () {
+ console.log(this.getAttribute("message"))
+}
+```
+
+In this case, we can use this action like so:
+
+```html
+
+```
+
+This will get "Hello World" printed on the DevTools Console. With custom actions, you can do pretty much anything on the document.
So far, all vanilla Hotwire and Turbo.
diff --git a/docs/testing.md b/docs/testing.md
index 493c7f89..cb1b07c3 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -4,27 +4,141 @@
## Introduction
-There are two aspects of your application using Turbo Laravel that are specific this approach itself:
+Testing a Hotwired app is like testing a regular Laravel app. However, Turbo Laravel comes with a set of helpers that may be used to ease testing some aspects that are specific to Turbo:
-1. **Turbo Stream HTTP responses.** As you return Turbo Stream responses from your route handlers/controllers to be applied by Turbo itself; and
-1. **Turbo Stream broadcasts.** Which is the side-effect of certain model changes, or when you call `$model->broadcastAppend()` on your models, or when you're using Handmade Turbo Stream broadcasts.
+1. **Turbo HTTP Request Helpers**. When you may want to mimic a Turbo visit, or a Turbo Native visit, or a request coming from a Turbo Frame.
+1. **Turbo Streams on HTTP Responses.** When you may want to test the Turbo Streams returned from HTTP requests.
+1. **Turbo Stream Broadcasts.** When you're either using the broadcast methods on your models using the `Broadcasts` trait, or when you're using [Handmade Turbo Stream Broadcasts](https://turbo-laravel.com/docs/2.x/broadcasting#content-handmade-broadcasts).
-We're going to cover both of these scenarios here.
+Let's dig into those aspects and how you may test them.
-## Making Turbo & Turbo Native HTTP requests
+## Turbo HTTP Request Helpers
-To enhance your testing capabilities here, Turbo Laravel adds a couple of macros to the TestResponse that Laravel uses under the hood. The goal is that testing Turbo Stream responses is as convenient as testing regular HTTP responses.
+To enhance your testing capabilities when using Turbo, Turbo Laravel adds a few macros to the `TestResponse` that Laravel uses under the hood. It also ships with a `InteractsWithTurbo` trait that adds Turbo-specific testing helper methods. The goal is to allow mimicking a request and inspecting the response in a very Laravel way.
-To mimic Turbo requests, which means sending a request setting the correct Content-Type in the `Accept:` HTTP header, you need to use the `InteractsWithTurbo` trait to your testcase. Now you can mimic a Turbo HTTP request by using the `$this->turbo()` method before you make the HTTP call itself. You can also mimic Turbo Native specific requests by using the `$this->turboNative()` also before you make the HTTP call. The first method will add the correct Turbo Stream content type to the `Accept:` header, and the second method will add Turbo Native `User-Agent:` value.
+### Acting as Turbo Visits
-These methods are handy when you are conditionally returning Turbo Stream responses based on the `request()->wantsTurboStream()` helper, for instance. Or when using the `@turbonative` or `@unlessturbonative` Blade directives.
+Turbo visits are marked with a `Accept: text/vnd.turbo-stream.html, ...` header, which you may want to respond diferently (maybe returning a Turbo Streams document instead of plain HTML). To be able to make request adding that header, you may add the `InteractsWithTurbo` trait to your current test class (or to the base `TestCase`). Then, you may use the `$this->turbo()` method before issuing a request:
-## Testing Turbo Stream HTTP Responses
+```php
+use HotwiredLaravel\TurboLaravel\Testing\InteractsWithTurbo;
+
+class CreateCommentsTest extends TestCase
+{
+ use InteractsWithTurbo;
+
+ /** @test */
+ public function creates_comments()
+ {
+ $post = Post::factory()->create();
+
+ $this->assertCount(0, $post->comments);
+
+ $this->turbo()->post(route('posts.comments.store', $post), [
+ 'content' => 'Hello World',
+ ])->assertOk();
+
+ $this->assertCount(1, $post->refresh()->comments);
+ $this->assertEquals('Hello World', $post->comments->first()->content);
+ }
+}
+```
+
+When using this method, calls to `request()->wantsTurboStream()` will return `true`.
+
+## Acting as Turbo Frame Requests
+
+You may want to handle requests a bit differently based on whether they came from a request triggered inside a Turbo Frame or not. To mimic a request coming from a Turbo Frame, you may use the `fromTurboFrame()` helper from the `InteractsWithTurbo` trait:
-You can test if you got a Turbo Stream response by using the `assertTurboStream`. Similarly, you can assert that your response is _not_ a Turbo Stream response by using the `assertNotTurboStream()` macro:
+```php
+use HotwiredLaravel\TurboLaravel\Testing\InteractsWithTurbo;
+
+class CreateCommentsTest extends TestCase
+{
+ use InteractsWithTurbo;
+
+ /** @test */
+ public function create_comment()
+ {
+ $article = Article::factory()->create();
+
+ $this->fromTurboFrame(dom_id($article, 'create_comment'))
+ ->post(route('articles.comments.store', $article), [...])
+ ->assertRedirect();
+ }
+}
+```
+
+### Acting as Turbo Native
+
+Additionally, when you're building a Turbo Native mobile app, you may want to issue a request pretending to be sent from a Turbo Native client. That's done by setting the `User-Agent` header to something that mentions the word `Turbo Native`. The `InteractsWithTurbo` trait also has a `$this->turboNative()` method you may use that automatically sets the header correctly:
```php
-use Tonysm\TurboLaravel\Testing\InteractsWithTurbo;
+use HotwiredLaravel\TurboLaravel\Testing\InteractsWithTurbo;
+
+class CreateCommentsTest extends TestCase
+{
+ use InteractsWithTurbo;
+
+ /** @test */
+ public function creating_comments_from_native_recedes()
+ {
+ $post = Post::factory()->create();
+
+ $this->assertCount(0, $post->comments);
+
+ $this->turboNative()->post(route('posts.comments.store', $post), [
+ 'content' => 'Hello World',
+ ])->assertOk();
+
+ $this->assertCount(1, $post->refresh()->comments);
+ $this->assertEquals('Hello World', $post->comments->first()->content);
+ }
+}
+```
+
+When using this method, calls to `request()->wasFromTurboNative()` will return `true`. Additionally, the `@turbonative` and `@unlessturbonative` Blade directives will render as expected.
+
+Additionally, a few macros were added to the `TestResponse` class to make it easier to assert based on the `recede`, `resume`, and `refresh` redirects using the specific assert methods:
+
+| Method | Descrition |
+|---|---|
+| `assertRedirectRecede(array $with = [])` | Asserts that a redirect was returned to the `/recede_historical_location` route. |
+| `assertRedirectResume(array $with = [])` | Asserts that a redirect was returned to the `/resume_historical_location` route. |
+| `assertRedirectRefresh(array $with = [])` | Asserts that a redirect was returned to the `/refresh_historical_location` route. |
+
+The `$with` param will ensure that not only the route is correct, but also any flashed message will be included in the query string:
+
+```php
+use HotwiredLaravel\TurboLaravel\Testing\InteractsWithTurbo;
+
+class CreateCommentsTest extends TestCase
+{
+ use InteractsWithTurbo;
+
+ /** @test */
+ public function creating_comments_from_native_recedes()
+ {
+ $post = Post::factory()->create();
+
+ $this->assertCount(0, $post->comments);
+
+ $this->turboNative()->post(route('posts.comments.store', $post), [
+ 'content' => 'Hello World',
+ ])->assertRedirectRecede(['status' => __('Comment created.')]);
+
+ $this->assertCount(1, $post->refresh()->comments);
+ $this->assertEquals('Hello World', $post->comments->first()->content);
+ }
+}
+```
+
+## Asserting Turbo Stream HTTP Responses
+
+You may test if you got a Turbo Stream response by using the `assertTurboStream()` response helper macro. Similarly, you may assert that your response was _not_ a Turbo Stream response by using the `assertNotTurboStream()` response helper macro:
+
+```php
+use HotwiredLaravel\TurboLaravel\Testing\InteractsWithTurbo;
class CreateTodosTest extends TestCase
{
@@ -33,22 +147,18 @@ class CreateTodosTest extends TestCase
/** @test */
public function creating_todo_from_turbo_request_returns_turbo_stream_response()
{
- $response = $this->turbo()->post(route('todos.store'), [
+ $this->turbo()->post(route('todos.store'), [
'content' => 'Test the app',
- ]);
-
- $response->assertTurboStream();
+ ])->assertTurboStream();
}
/** @test */
public function creating_todo_from_regular_request_does_not_return_turbo_stream_response()
{
// Notice we're not chaining the `$this->turbo()` method here.
- $response = $this->post(route('todos.store'), [
+ $this->post(route('todos.store'), [
'content' => 'Test the app',
- ]);
-
- $response->assertNotTurboStream();
+ ])->assertNotTurboStream();
}
}
```
@@ -65,7 +175,7 @@ class TodosController
]));
if (request()->wantsTurboStream()) {
- return response()->turboStream($todo);
+ return turbo_stream($todo);
}
return redirect()->route('todos.index');
@@ -73,9 +183,9 @@ class TodosController
}
```
-## Fluent Turbo Stream Testing
+## Fluent Turbo Stream Assertions
-You can get specific on your Turbo Stream responses by passing a callback to the `assertTurboStream(fn)` method. This can be used to test that you have a specific Turbo Stream tag being returned, or that you're returning exactly 2 Turbo Stream tags, for instance:
+The `assertTurboStream()` macro accepts a callback which allows you to assert specific details about your returned Turbo Streams. The callback takes an instance of the `AssertableTurboStream` class, which has some matching methods to help you building your specific assertion. In the following example, we're asserting that 2 Turbo Streams were returned, as well as their targets, actions, and even HTML content:
```php
/** @test */
@@ -100,12 +210,12 @@ public function create_todos()
## Testing Turbo Stream Broadcasts
-All broadcasts use the `TurboStream` Facade. You may want to fake it so you can that the broadcasts are being correctly sent:
+You may assert that Turbo Stream broadcasts were sent from any mechanism provided by Turbo Laravel by using the `TurboStream::fake()` abstraction. This allows you to capture any kind of Turbo Stream broadcasting that happens inside your application and assert on them:
```php
use App\Models\Todo;
-use Tonysm\TurboLaravel\Facades\TurboStream;
-use Tonysm\TurboLaravel\Broadcasting\PendingBroadcast;
+use HotwiredLaravel\TurboLaravel\Facades\TurboStream;
+use HotwiredLaravel\TurboLaravel\Broadcasting\PendingBroadcast;
class CreatesCommentsTest extends TestCase
{
diff --git a/docs/turbo-frames.md b/docs/turbo-frames.md
new file mode 100644
index 00000000..79f29d30
--- /dev/null
+++ b/docs/turbo-frames.md
@@ -0,0 +1,55 @@
+# Turbo Frames
+
+[TOC]
+
+## Introduction
+
+The Turbo Stream tag that ships with Turbo can be used on your Blade views just like any other HTML tag:
+
+```blade
+
+
Loading...
+
+```
+
+In this case, the `@domid()` directive is being used to create a dom ID that looks like this `create_comment_post_123`. There's also a Blade Component that ships with Turbo Laravel and can be used like this:
+
+```blade
+
+
Loading...
+
+```
+
+When using the Blade Component, you don't have to worry about using the `@domid()` directive or the `dom_id()` function, as this gets handled automatically by the package. You may also pass a string if you want to enforce your own DOM ID.
+
+Any other attribute passed to the Blade Component will get forwarded to the underlying `` element, so if you want to turn a Turbo Frame into a lazy-loading Turbo Frame using the Blade Component, you can do it like so:
+
+```blade
+
+
Loading...
+
+```
+
+This will work for any other attribute you want to forward to the underlying component.
+
+## The `request()->wasFromTurboFrame()` Macro
+
+You may want to detect if a request came from a Turbo Frame in the backend. You may use the `wasFromTurboFrame()` method for that:
+
+```php
+if ($request->wasFromTurboFrame()) {
+ // ...
+}
+```
+
+When used like this, the macro will return `true` if the `X-Turbo-Frame` custom HTTP header is present in the request (which Turbo adds automatically), or `false` otherwise.
+
+You may also check if the request came from a specific Turbo Frame:
+
+```php
+if ($request->wasFromTurboFrame(dom_id($post, 'create_comment'))) {
+ // ...
+}
+```
+
+[Continue to Turbo Streams...](/docs/{{version}}/turbo-streams)
diff --git a/docs/turbo-native.md b/docs/turbo-native.md
index 1f7c3522..d441aedc 100644
--- a/docs/turbo-native.md
+++ b/docs/turbo-native.md
@@ -4,17 +4,20 @@
## Introduction
-Hotwire also has a [mobile side](https://turbo.hotwired.dev/handbook/native), and the package provides some goodies on this front too.
+Hotwire also has a [mobile side](https://turbo.hotwired.dev/handbook/native) and Turbo Laravel provides some helpers to help integrating with that.
-Turbo Visits made by a Turbo Native client will send a custom `User-Agent` header. So we added another Blade helper you may use to toggle fragments or assets (such as mobile specific stylesheets) on and off depending on whether your page is being rendered for a Native app or a Web app:
+Turbo visits made by a Turbo 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 Turbo Native client instead of a regular web browser.
+
+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 file include, for instance. To do that, you may use the `@turbonative` Blade directive in your Blade views:
```blade
@turbonative
-
Hello, Turbo Native Users!
+
@endturbonative
```
-Alternatively, you can check if it's not a Turbo Native visit using the `@unlessturbonative` Blade helpers:
+Alternatively, you may want to include some elements only if the client requesting it is _NOT_ a Turbo Native client using the `@unlessturbonative` Blade helpers:
```blade
@unlessturbonative
@@ -33,7 +36,7 @@ if (request()->wasFromTurboNative()) {
Or the Turbo Facade directly, like so:
```php
-use Tonysm\TurboLaravel\Facades\Turbo;
+use HotwiredLaravel\TurboLaravel\Facades\Turbo;
if (Turbo::isTurboNativeVisit()) {
// ...
@@ -42,12 +45,24 @@ if (Turbo::isTurboNativeVisit()) {
## Interacting With Turbo Native Navigation
-Turbo is built to work with native navigation principles and present those alongside what's required for the web. When you have Turbo Native clients running (see the Turbo iOS and Turbo Android projects for details), you can respond to native requests with three dedicated responses: `recede`, `resume`, `refresh`.
+Turbo 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.
+
+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 Turbo Native client.
+
+For instance, if a form submission request came from a Turbo 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 Turbo Native client:
-You may want to use the provided `InteractsWithTurboNativeNavigation` trait on your controllers like so:
+| Signal | Route| Description|
+|---|---|---|
+| `recede` | `/recede_historical_location` | Go back to previous screen |
+| `resume` | `/resume_historical_location` | Stay on the current screen as is |
+| `refresh`| `/refresh_historical_location` | Stay on the current screen but refresh |
+
+Sending these signals is a matter of detecting if the request came from a Turbo Native client and, if so, redirect the user to these signal URLs instead. The Turbo Native client should detect the redirect was from one of these special routes and trigger the desired behavior.
+
+You may use the `InteractsWithTurboNativeNavigation` trait on your controllers to achieve this behavior and fallback to a regular redirect if the request wasn't from a Turbo Native client:
```php
-use Tonysm\TurboLaravel\Http\Controllers\Concerns\InteractsWithTurboNativeNavigation;
+use HotwiredLaravel\TurboLaravel\Http\Controllers\Concerns\InteractsWithTurboNativeNavigation;
class TraysController extends Controller
{
@@ -55,14 +70,14 @@ class TraysController extends Controller
public function store()
{
- $tray = /** Create the Tray */;
+ // Tray creation...
return $this->recedeOrRedirectTo(route('trays.show', $tray));
}
}
```
-In this example, when the request to create trays comes from a Turbo Native request, we're going to redirect to the `turbo_recede_historical_location` URL route instead of the `trays.show` route. However, if the request was made from your web app, we're going to redirect the client to the `trays.show` route.
+In this example, when the request to create trays comes from a Turbo Native client, we're going to redirect to the `/turbo_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.
There are a couple of redirect helpers available:
@@ -75,9 +90,36 @@ $this->resumeOrRedirectBack(string $fallbackUrl, array $options = []);
$this->refreshOrRedirectBack(string $fallbackUrl, array $options = []);
```
-The Turbo Native client should intercept navigations to these special routes and handle them separately. For instance, you may want to close a native modal that was showing a form after its submission and _recede_ to the previous screen dismissing the modal, and not by following the redirect as the web does.
+It's common to flash messages using the `->with()` method of the Redirect response in Laravel. However, since a Turbo Native request will never actually redirect somewhere where the flash message will be rendered, the behavior of the `->with()` method was slightly modified too.
+
+If you're setting flash messages like this after a form submission:
+
+```php
+use HotwiredLaravel\TurboLaravel\Http\Controllers\Concerns\InteractsWithTurboNativeNavigation;
+
+class TraysController extends Controller
+{
+ use InteractsWithTurboNativeNavigation;
+
+ public function store()
+ {
+ // Tray creation...
+
+ return $this->recedeOrRedirectTo(route('trays.show', $tray))
+ ->with('status', __('Tray created.'));
+ }
+}
+```
+
+If a request was sent from a Turbo 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:
+
+```
+/recede_historical_location?status=Tray%20created.
+```
+
+In the Turbo 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.
-At the time of this writing, there aren't much information on how the mobile clients should interact with these routes. However, I wanted to be able to experiment with them, so I brought them to the package for parity (see this [comment here](https://github.com/hotwired/turbo-rails/issues/78#issuecomment-815897904)).
+If the request wasn't from a Turbo 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.
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):
diff --git a/docs/turbo-streams.md b/docs/turbo-streams.md
index 8abb9967..f70988ae 100644
--- a/docs/turbo-streams.md
+++ b/docs/turbo-streams.md
@@ -4,13 +4,9 @@
## Introduction
-Out of everything Turbo provides, it's Turbo Streams that benefits the most from a tight back-end integration.
+Out of everything Turbo provides, it's Turbo Streams that benefits the most from a tight backend integration.
-Turbo Drive will get your pages behaving like an SPA and Turbo Frames will allow you to have a finer grained control of chunks of your page instead of replacing the entire page when a form is submitted or a link is clicked.
-
-However, sometimes you want to update _multiple_ parts of your page at the same time. For instance, after a form submission to create a comment, you may want to append the comment to the comments' list and also update the comments' count in the page. You may achieve that with Turbo Streams.
-
-Form submissions will get annotated by Turbo with a `Accept: text/vnd.turbo-stream.html` header (besides the other normal Content Types). This is a signal to the back-end that you can return a Turbo Stream response for that form submission if you want to.
+Turbo Laravel offers helper functions, Blade Components, and [Model traits](/docs/{{version}}/broadcasting) to generate Turbo Streams. Turbo will add a new `Content-Type` to the HTTP Accept header (`Accept: text/vnd.turbo-stream.html, ...`) on Form submissions. This is a signal to the backend that we can return a Turbo Stream response for that form submission instead of an HTML document, if we want to.
Here's an example of a route handler detecting and returning a Turbo Stream response to a form submission:
@@ -19,7 +15,7 @@ Route::post('posts/{post}/comments', function (Post $post) {
$comment = $post->comments()->create(/** params */);
if (request()->wantsTurboStream()) {
- return response()->turboStream($comment);
+ return turbo_stream($comment);
}
return back();
@@ -28,19 +24,7 @@ Route::post('posts/{post}/comments', function (Post $post) {
The `request()->wantsTurboStream()` macro added to the request class will check if the request accepts Turbo Stream and return `true` or `false` accordingly.
-The `response()->turboStream()` macro may be used to generate streams, but you may also use the `turbo_stream()` helper function. From now on, the docs will be using the helper function, but you may use either one of those. Using the function, this example would be:
-
-```php
-Route::post('posts/{post}/comments', function (Post $post) {
- $comment = $post->comments()->create(/** params */);
-
- if (request()->wantsTurboStream()) {
- return turbo_stream($comment);
- }
-
- return back();
-});
-```
+The `turbo_stream()` helper function may be used to generate streams, but you may also use the `response()->turboStream()` macro as well. In the docs, we'll only use the helper function, but you may use either one of those.
Here's what the HTML response will look like:
@@ -54,30 +38,34 @@ Here's what the HTML response will look like:
```
-Most of these things were "guessed" based on the [naming conventions](/docs/{{version}}/conventions) we talked about earlier. But you can override most things, like so:
+Most of these things were "guessed" based on the [conventions](/docs/{{version}}/conventions) we talked about earlier. But you can override most things, like so:
```php
-return turbo_stream($comment)->target('post_comments');
+turbo_stream($comment)->target('post_comments');
+```
+
+This would render the following Turbo Stream:
+
+```html
+
+
+
+
Hello, World
+
+
+
```
-Although it's handy to pass the model instance to the `turbo_stream()` function - which will be used to decide the default values of the Turbo Stream response based on the model's current state, sometimes you may want to build a Turbo Stream response manually:
+Although it's handy to pass a model instance to the `turbo_stream()` function - which will be used to decide the default values of the Turbo Stream response based on the model's current state, sometimes you may want to build a Turbo Stream response manually:
```php
-return turbo_stream()
+turbo_stream()
->target('comments')
->action('append')
->view('comments._comment', ['comment' => $comment]);
```
-There are 7 _actions_ in Turbo Streams. They are:
-
-* `append` & `prepend`: to insert the elements in the target element at the top or at the bottom, respectively
-* `before` & `after`: to insert the elements next to the target element before or after, respectively
-* `replace`: will replace the existing element entirely with the contents of the _template_ tag in the Turbo Stream
-* `update`: will keep the target element and only replace the contents of it with the contents of the _template_ tag in the Turbo Stream
-* `remove`: will remove the element. This one doesn't need a _template_ tag. It accepts either an instance of a Model or the DOM ID of the element to be removed as a string.
-
-You will find shorthand methods for them all:
+There are also shorthand methods which may be used as well:
```php
turbo_stream()->append($comment);
@@ -87,14 +75,17 @@ turbo_stream()->after($comment);
turbo_stream()->replace($comment);
turbo_stream()->update($comment);
turbo_stream()->remove($comment);
+turbo_stream()->refresh();
```
-For these shorthand stream builders, you may pass an instance of an Eloquent model, which will be used to figure out things like `target`, `action`, and the `view` partial as well as the view data passed to them.
+You may pass an instance of an Eloquent model to all these shorthand methods, except the `refresh` one, which will be used to figure things out like `target`, the `view`, and will also pass that model instance to the view.
+
+For a model `App\Models\Comment`, the [convention] says that the view is located at `resources/views/comments/_comment.blade.php`. Based on the model's class basename, it will figure out the name of the variable that the view should depend on, which would be `$comment` in this case, so it would pass the model instance down to the view automatically. For that reason, when using the convention (which is optional), the model view must only depend on the model instance to be available (no globals or other locals with no defaults).
Alternativelly, you may also pass strings to the shorthand stream builders, which will be used as the target, and an optional content string, which will be rendered instead of a partial, for instance:
```php
-turbo_stream()->append('statuses', __('Comment was successfully created!'));
+turbo_stream()->append('statuses', __('Comment created!'));
```
The optional content parameter expects either a string, a view instance, or an instance of Laravel's `Illuminate\Support\HtmlString`, so you could do something like:
@@ -132,7 +123,7 @@ For both the `before` and `after` methods you need additional calls to specify t
turbo_stream()
->before($comment)
->view('comments._flash_message', [
- 'message' => __('Comment was created!'),
+ 'message' => __('Comment created!'),
]);
```
@@ -144,19 +135,9 @@ turbo_stream()->before($comment, __('Oh, hey!'));
You can read more about Turbo Streams in the [Turbo Handbook](https://turbo.hotwired.dev/handbook/streams).
-The shorthand methods also return a pending Turbo Stream builder which you can chain and override everything you want before it's rendered:
-
-```php
-return turbo_stream()
- ->append($comment)
- ->view('comments._comment_card', [
- 'comment' => $comment,
- ]);
-```
-
-As mentioned earlier, passing a model to the `turbo_stream()` helper will pre-fill the pending response object with some defaults based on the model's state.
+As mentioned earlier, passing a model to the `turbo_stream()` helper (or the shorthand Turbo Stream builders) will pre-fill the pending response object with some defaults based on the model's state.
-It will build a `remove` Turbo Stream if the model was deleted (or if it is trashed - in case it's a Soft Deleted model), an `append` if the model was recently created (which you can override the action as the second parameter), a `replace` if the model was just updated (you can also override the action as the second parameter.) Here's how overriding would look like:
+It will build a `remove` Turbo Stream if the model was just deleted (or if it was trashed - in case it's a Soft Deleted model), an `append` if the model was recently created (which you can override the action as the second parameter), a `replace` if the model was just updated (you can also change it to `update` using the second parameter.) Here's how overriding would look like:
```php
return turbo_stream($comment, 'append');
@@ -164,23 +145,24 @@ return turbo_stream($comment, 'append');
## Target Multiple Elements
-You may also [target multiple elements](https://turbo.hotwired.dev/reference/streams#targeting-multiple-elements) using CSS classes with the `xAll` methods:
+Turbo Stream elements can either have a `target` with a DOM ID or a `targets` attribute with a CSS selector to [match multiple elements](https://turbo.hotwired.dev/reference/streams#targeting-multiple-elements). You may use the `xAll` shorthand methods to set the `targets` attribute instead of `target`:
```php
turbo_stream()->appendAll('.comment', 'Some content');
turbo_stream()->prependAll('.comment', 'Some content');
turbo_stream()->updateAll('.comment', 'Some content');
-turbo_stream()->removeAll('.comment');
+turbo_stream()->replaceAll('.comment', 'Some content');
turbo_stream()->beforeAll('.comment', 'Some content');
turbo_stream()->afterAll('.comment', 'Some content');
+turbo_stream()->removeAll('.comment');
```
-With the exception of the `removeAll` method, the `xAll` methods accept as the second parameter a string of inline content, an instance of a View (which may be created using the `view()` function provided by Laravel), or an instance of the `HtmlSafe` class.
+With the exception of the `removeAll` method, the `xAll` methods accept astring of inline content, an instance of a View (which may be created using the `view()` function provided by Laravel), or an instance of the `HtmlSafe` class as the second parameter.
When creating Turbo Streams using the builders, you may also specify the CSS class using the `targets()` (plural) method instead of the `target()` (singular) version:
```php
-return turbo_stream()
+turbo_stream()
->targets('.comment')
->action('append')
->view('comments._comment', ['comment' => $comment]);
@@ -188,7 +170,7 @@ return turbo_stream()
## Turbo Stream Macros
-The `turbo_stream()` function returns an instance of `PendingTurboStreamResponse`, which is _macroable_. This means you can create your custom DSL for streams. Let's say you always return flash messages from your controllers like so:
+The `turbo_stream()` function returns an instance of `PendingTurboStreamResponse`, which is _macroable_. This means you can create your own DSL for your custom Turbo Streams. Let's say you always return flash messages from your controllers like so:
```php
class ChirpsController extends Controller
@@ -265,31 +247,27 @@ return turbo_stream([
->append($comment)
->target(dom_id($comment->post, 'comments')),
turbo_stream()
- ->update(dom_id($comment->post, 'comments_count'), view('posts._comments_count', ['post' => $comment->post])),
+ ->update(dom_id($comment->post, 'comments_count'), view('posts._comments_count', [
+ 'post' => $comment->post,
+ ])),
]);
```
-Although this is a valid option, it might feel like too much work for a controller. If that's the case, use [Custom Turbo Stream Views](#custom-turbo-stream-views).
+Although this is a valid option, it might feel like too much work for a controller. If that's the case, you may use [Custom Turbo Stream Views](#custom-turbo-stream-views).
## Custom Turbo Stream Views
Although combining Turbo Streams in a single response right there in the controller is a valid option, it may feel like too much work for a controller. If that's the case, you may want to extract the Turbo Streams to a Blade view and respond with that instead:
-```php
-return response()->turboStreamView('comments.turbo.created_stream', [
- 'comment' => $comment,
-]);
-```
-
-Similar to the `Response::turboStream()` macro and the `turbo_stream()` helper function, you may prefer using the helper function `turbo_stream_view()`:
-
```php
return turbo_stream_view('comments.turbo.created_stream', [
'comment' => $comment,
]);
```
-And here's an example of a more complex custom Turbo Stream view:
+Similar to the `turbo_stream()` helper function and the `Response::turboStream()` macro, you may prefer using the `Response::turboStreamView()` macro. It works the same way.
+
+Here's an example of a more complex custom Turbo Stream view:
```blade
@include('layouts.turbo.flash_stream')
@@ -313,7 +291,7 @@ Remember, these are Blade views, so you have the full power of Blade at your han
@endif
```
-Similar to the `` Blade component, there's also a `` Blade component that can simplify things a bit. It has the same convention of figureing out the DOM ID when you're passing a model instance or an array as the `` component applied to the `target` attribute. When using the component version, there's also no need to specify the template wrapper for the Turbo Stream tag, as that will be added by the component itself. So, the same example would look something like this:
+Similar to the `` Blade component, there's also a `` Blade component that can simplify things a bit. It has the same convention of figuring out the DOM ID when you're passing a model instance or an array as `target` attribute of the `` component. When using the component version, there's no need to specify the template wrapper for the Turbo Stream tag, as that will be added by the component itself. So, the same example would look something like this:
```blade
@include('layouts.turbo.flash_stream')
@@ -327,22 +305,12 @@ I hope you can see how powerful this can be to reusing views.
## Custom Actions
-When you're using the Blade component, you can use Turbo's custom actions:
+You may also use the `` Blade component for your custom actions as well:
```blade
```
-As you can see, when using custom actions, the `` is also optional. To implement custom actions in the front-end, you'd need something like this:
-
-```js
-import * as Turbo from '@hotwired/turbo';
-
-Turbo.StreamActions.console_log = function () {
- console.log(this.getAttribute("value"))
-}
-```
-
-Custom actions are only supported from Blade views. You cannot return those from controllers using the Pending Streams Builder, for instance.
+Custom actions are only supported from Blade views. You cannot return those from controllers using the Pending Streams Builder.
[Continue to Broadcasting...](/docs/{{version}}/broadcasting)
diff --git a/docs/upgrade.md b/docs/upgrade.md
new file mode 100644
index 00000000..fea166ee
--- /dev/null
+++ b/docs/upgrade.md
@@ -0,0 +1,19 @@
+# Upgrade Guide
+
+## Upgrading from 1.x to 2.x
+
+For version 2.x, we're migrating from `hotwired/turbo-laravel` to `hotwired-laravel/turbo-laravel`. That's just so folks don't get confused thinking this is an official Hotwired project, which it's not. Even if you're on `1.x`, it's recommended to migrate to `hotwired-laravel/turbo-laravel`.
+
+First, update the namespaces from the previous package. You can either do it from your IDE by searching for `Tonysm\TurboLaravel` and replacing it with `HotwiredLaravel\TurboLaravel` on your application (make sure you include all folders), or you can run the following command if you're on a macOS or Linux machine:
+
+```bash
+find app config resources tests -type f -exec sed -i 's/Tonysm\\TurboLaravel/HotwiredLaravel\\TurboLaravel/g' {} +
+```
+
+Next, require the new package and remove the previous one:
+
+```bash
+composer require hotwired-laravel/turbo-laravel:2.0.0-beta1
+
+composer remove hotwired/turbo-laravel
+```
diff --git a/docs/validation-response-redirects.md b/docs/validation-response-redirects.md
index 691e6f22..be2ddce0 100644
--- a/docs/validation-response-redirects.md
+++ b/docs/validation-response-redirects.md
@@ -4,28 +4,27 @@
## Introduction
-By default, Laravel will redirect failed validation exceptions "back" to the page the triggered the request. This is a bit problematic when it comes to Turbo Frames, since a form might be included in a page that don't render the form initially, and after a failed validation exception from a form submission we would want to re-render the form with the invalid messages.
+By default, Laravel redirects failed validation exceptions "back" to the page where the request came from. This isn't usually a problem, in fact it's the expected behavior, since that page usually is the one where the form which triggered the request renders.
-In other words, a Turbo Frame inherits the context of the page where it was inserted in, and a form might not be part of that page itself. We can't redirect "back" to display the form again with the error messages, because the form might not be re-rendered there by default. Instead, we have two options:
+However, this is a bit of a problem when it comes to Turbo Frames, since a form might get injected into a page that doesn't initially render it. The problem is that after a failed validation exception from that form, Laravel would redirect it "back" to the page where the form got injected and since the form is not rendered there initially, the user would see the form disappear.
-1. Render a Blade view with the form as a non-200 HTTP Status Code, then Turbo will look for a matching Turbo Frame inside the response and replace only that portion or page, but it won't update the URL as it would for other Turbo Visits; or
-2. Redirect the request to a page that renders the form directly instead of "back". There you can render the validation messages and all that. Turbo will follow the redirect (303 Status Code) and fetch the Turbo Frame with the form and invalid messages and update the existing one.
+In other words, we can't redirect "back" to display the form again with the error messages, because the form might not be re-rendered there originally. Instead, Turbo expects that we return a non-200 HTTP status code with the form and validation messages right way after a failed validation exception is thrown.
-When using the `TurboMiddleware` that ships with this package, we'll override Laravel's default error handling for validation exceptions. Instead of redirecting "back", we'll guess the form route based on the route resource conventions (if you're using that) and make an internal GET request to that route and return its contents with a 422 status code. So, if you're using the route resource conventions, validation errors will not respond with redirects, but with 422 status codes instead.
+Turbo Laravel automatically prepends a `TurboMiddleware` on the web route group. The middleware will intercept the response when it detects that Laravel is responding after a `ValidationException`. Instead of letting it send the "redirect back" response, it will try to guess where the form for that request usually renders and send an internal request back to the app to render the form, then update the status code so it renders as a 422 instead of 200.
-To guess where the form is located at we rely on the route resource convention. For any route name ending in `.store`, it will guess that the form can be located at the `.create` route for the same resource with all the route params from the previous request. In the same way, for any `.update` routes, it will guess the form is located at the `.edit` route of the same resource.
+To guess where the form is located at we rely on the route resource naming convention. For any route name ending in `.store`, it will guess that the form can be located in a similar route ending with `.create` for the same resource. Similarly, for any route ending with `.update`, it will guess the form is located at a route ending with `.edit`. Addittionaly, for any route ending with `.destroy`, it will guess the form is located at a route ending with `.delete` (this is the only convention that is not there by default in Laravel's conventions.)
-Examples:
+For this internal request, the middleware will pass along any resource the current route has as well as any query string that was passed.
+
+Here are some examples:
- `posts.comments.store` will guess the form is at the `posts.comments.create` route with the `{post}` route param.
- `comments.store` will guess the form is at the `comments.create` route with no route params.
- `comments.update` will guess the form is at the `comments.edit` with the `{comment}` param.
-If a guessed route name doesn't exist (which will always happen if you don't use the route resource convention), the middleware will not change the default handling of validation errors.
-
-When you're not using the [resource route naming convention](/docs/{{version}}/conventions), you can override redirect behavior by catching the `ValidationException` yourself and re-throwing it overriding the redirect with the `redirectTo` method. If the exception has that, the middleware will respect it and make a GET request to that location instead of trying to guess it.
+If a guessed route name doesn't exist (which will always happen if you don't use the route resource convention), the middleware will not change the default handling of validation errors, so the regular "redirect back" behavior will act.
-Here's how you may set the `redirectTo` property:
+When you're not using the [resource route naming convention](/docs/{{version}}/conventions), you may override redirect behavior by catching the `ValidationException` and re-throwing it setting the correct location where the form renders using the `redirectTo` method. If the exception has that, the middleware will respect it and make a GET request to that location instead of trying to guess it:
```php
public function store()
@@ -38,7 +37,7 @@ public function store()
}
```
-You may want to have exceptions to the route guessing behavior, which you can use the `redirect_guessing_exceptions` config in the `config/turbo-laravel.php` config file:
+If you want to register exceptions to this route guessing behavior, add the URIs to the `redirect_guessing_exceptions` key in the `config/turbo-laravel.php` config file:
```php
return [
@@ -49,16 +48,14 @@ return [
];
```
-The internal redirect will still happen, but the resource route convention will not be used.
-
-## Turbo HTTP Middleware
+## The Turbo HTTP Middleware
-The package ships with a middleware which applies some conventions on your redirects, specially around how failed validations are handled automatically by Laravel. Read more about this in the [Conventions](#conventions) section of the documentation.
+Turbo Laravel ships with a middleware which applies some conventions on your redirects, like the one for how failed validations are handled automatically by Laravel as described before. Read more about this in the [Conventions](#conventions) section of the documentation.
-**The middleware is automatically prepended to your web route group middleware stack**. You may want to add the middleware to other groups, when doing so, make sure it's at the top of the middleware stack:
+**The middleware is automatically prepended to your web route group middleware stack**. You may want to add the middleware to other groups. When doing so, make sure it's at the top of the middleware stack:
```php
-\Tonysm\TurboLaravel\Http\Middleware\TurboMiddleware::class,
+\HotwiredLaravel\TurboLaravel\Http\Middleware\TurboMiddleware::class,
```
Like so:
@@ -73,7 +70,7 @@ class Kernel extends HttpKernel
{
protected $middlewareGroups = [
'web' => [
- \Tonysm\TurboLaravel\Http\Middleware\TurboMiddleware::class,
+ \HotwiredLaravel\TurboLaravel\Http\Middleware\TurboMiddleware::class,
// other middlewares...
],
];
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 05ba79f8..9da12479 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -11,7 +11,7 @@
verbose="true"
>
-
+ tests
diff --git a/resources/views/components/refreshes-with.blade.php b/resources/views/components/refreshes-with.blade.php
new file mode 100644
index 00000000..c54f6a59
--- /dev/null
+++ b/resources/views/components/refreshes-with.blade.php
@@ -0,0 +1,2 @@
+
+
diff --git a/routes/turbo.php b/routes/turbo.php
index a8c07fb5..6d8631fa 100644
--- a/routes/turbo.php
+++ b/routes/turbo.php
@@ -1,7 +1,7 @@
name('turbo_recede_historical_location');
Route::get('resume_historical_location', [TurboNativeNavigationController::class, 'resume'])->name('turbo_resume_historical_location');
diff --git a/src/Broadcasters/Broadcaster.php b/src/Broadcasters/Broadcaster.php
index a41ffe74..42c9e537 100644
--- a/src/Broadcasters/Broadcaster.php
+++ b/src/Broadcasters/Broadcaster.php
@@ -1,34 +1,32 @@
isBroadcasting;
+
+ $this->isBroadcasting = false;
+
+ try {
+ return $callback();
+ } finally {
+ $this->isBroadcasting = $original;
+ }
+ }
+
public function fake()
{
$this->recording = true;
@@ -32,42 +52,53 @@ public function fake()
return $this;
}
- public function broadcastAppend($content = null, Model|string|null $target = null, ?string $targets = null, Channel|Model|Collection|array|string|null $channel = null, array $attributes = [])
+ public function broadcastAppend($content = null, Model|string $target = null, string $targets = null, Channel|Model|Collection|array|string $channel = null, array $attributes = [])
{
return $this->broadcastAction('append', $content, $target, $targets, $channel, $attributes);
}
- public function broadcastPrepend($content = null, Model|string|null $target = null, ?string $targets = null, Channel|Model|Collection|array|string|null $channel = null, array $attributes = [])
+ public function broadcastPrepend($content = null, Model|string $target = null, string $targets = null, Channel|Model|Collection|array|string $channel = null, array $attributes = [])
{
return $this->broadcastAction('prepend', $content, $target, $targets, $channel, $attributes);
}
- public function broadcastBefore($content = null, Model|string|null $target = null, ?string $targets = null, Channel|Model|Collection|array|string|null $channel = null, array $attributes = [])
+ public function broadcastBefore($content = null, Model|string $target = null, string $targets = null, Channel|Model|Collection|array|string $channel = null, array $attributes = [])
{
return $this->broadcastAction('before', $content, $target, $targets, $channel, $attributes);
}
- public function broadcastAfter($content = null, Model|string|null $target = null, ?string $targets = null, Channel|Model|Collection|array|string|null $channel = null, array $attributes = [])
+ public function broadcastAfter($content = null, Model|string $target = null, string $targets = null, Channel|Model|Collection|array|string $channel = null, array $attributes = [])
{
return $this->broadcastAction('after', $content, $target, $targets, $channel, $attributes);
}
- public function broadcastUpdate($content = null, Model|string|null $target = null, ?string $targets = null, Channel|Model|Collection|array|string|null $channel = null, array $attributes = [])
+ public function broadcastUpdate($content = null, Model|string $target = null, string $targets = null, Channel|Model|Collection|array|string $channel = null, array $attributes = [])
{
return $this->broadcastAction('update', $content, $target, $targets, $channel, $attributes);
}
- public function broadcastReplace($content = null, Model|string|null $target = null, ?string $targets = null, Channel|Model|Collection|array|string|null $channel = null, array $attributes = [])
+ public function broadcastReplace($content = null, Model|string $target = null, string $targets = null, Channel|Model|Collection|array|string $channel = null, array $attributes = [])
{
return $this->broadcastAction('replace', $content, $target, $targets, $channel, $attributes);
}
- public function broadcastRemove(Model|string|null $target = null, ?string $targets = null, Channel|Model|Collection|array|string|null $channel = null, array $attributes = [])
+ public function broadcastRemove(Model|string $target = null, string $targets = null, Channel|Model|Collection|array|string $channel = null, array $attributes = [])
{
return $this->broadcastAction('remove', null, $target, $targets, $channel, $attributes);
}
- public function broadcastAction(string $action, $content = null, Model|string|null $target = null, ?string $targets = null, Channel|Model|Collection|array|string|null $channel = null, array $attributes = [])
+ public function broadcastRefresh(Channel|Model|Collection|array|string $channel = null)
+ {
+ return $this->broadcastAction(
+ action: 'refresh',
+ channel: $channel,
+ attributes: array_filter(['request-id' => $requestId = Turbo::currentRequestId()]),
+ )->lazyCancelIf(fn (PendingBroadcast $broadcast) => (
+ $this->shouldLimitPageRefreshesOn($broadcast->channels, $requestId)
+ ));
+ }
+
+ public function broadcastAction(string $action, $content = null, Model|string $target = null, string $targets = null, Channel|Model|Collection|array|string $channel = null, array $attributes = [])
{
$broadcast = new PendingBroadcast(
channels: $channel ? $this->resolveChannels($channel) : [],
@@ -82,7 +113,7 @@ public function broadcastAction(string $action, $content = null, Model|string|nu
$broadcast->fake($this);
}
- return $broadcast;
+ return $broadcast->cancelIf(! $this->isBroadcasting);
}
public function record(PendingBroadcast $broadcast)
@@ -92,6 +123,22 @@ public function record(PendingBroadcast $broadcast)
return $this;
}
+ protected function shouldLimitPageRefreshesOn(array $channels, ?string $requestId): bool
+ {
+ return Limiter::shouldLimit($this->pageRefreshLimiterKeyFor($channels, $requestId));
+ }
+
+ protected function pageRefreshLimiterKeyFor(array $channels, ?string $requestId): string
+ {
+ $keys = array_map(fn (Channel $channel) => $channel->name, $channels);
+
+ sort($keys);
+
+ $key = sha1(implode('/', array_values($keys) + array_filter([$requestId])));
+
+ return 'turbo-refreshes-limiter-'.$key;
+ }
+
protected function resolveRendering($content)
{
if ($content instanceof Rendering) {
@@ -130,6 +177,13 @@ protected function getResourceNameFor(Model $model): string
return Name::forModel($model)->plural;
}
+ public function clearRecordedBroadcasts(): self
+ {
+ $this->recordedStreams = [];
+
+ return $this;
+ }
+
public function assertBroadcasted($callback)
{
$result = collect($this->recordedStreams)->filter($callback);
@@ -143,7 +197,13 @@ public function assertBroadcastedTimes($callback, $times = 1, $message = null)
{
$result = collect($this->recordedStreams)->filter($callback);
- Assert::assertCount($times, $result, $message ?: sprintf('Expected to have broadcasted %s, but broadcasted %d instead.', trans_choice('{0} nothing|{1}a Turbo Stream|[2,*]:value Turbo Streams', $result->count()), $result->count()));
+ Assert::assertCount($times, $result, $message ?: sprintf(
+ 'Expected to have broadcasted %d Turbo %s, but broadcasted %d Turbo %s instead.',
+ $times,
+ (string) str('Stream')->plural($times),
+ $result->count(),
+ (string) str('Stream')->plural($result->count()),
+ ));
return $this;
}
diff --git a/src/Broadcasting/Limiter.php b/src/Broadcasting/Limiter.php
new file mode 100644
index 00000000..21899c87
--- /dev/null
+++ b/src/Broadcasting/Limiter.php
@@ -0,0 +1,26 @@
+keys = [];
+ }
+
+ public function shouldLimit(string $key): bool
+ {
+ if (! isset($this->keys[$key]) || $this->keys[$key]->isPast()) {
+ $this->keys[$key] = now()->addMilliseconds($this->delay);
+
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/Broadcasting/PendingBroadcast.php b/src/Broadcasting/PendingBroadcast.php
index 251903d8..53196026 100644
--- a/src/Broadcasting/PendingBroadcast.php
+++ b/src/Broadcasting/PendingBroadcast.php
@@ -1,7 +1,9 @@
+ */
+ protected array $deferredCancelCallbacks = [];
+
+ public function __construct(array $channels, string $action, Rendering $rendering, string $target = null, string $targets = null, array $attributes = [])
{
$this->action = $action;
$this->target = $target;
@@ -167,7 +175,14 @@ public function cancel()
public function cancelIf($condition)
{
- $this->wasCancelled = boolval(value($condition));
+ $this->wasCancelled = $this->wasCancelled || boolval(value($condition, $this));
+
+ return $this;
+ }
+
+ public function lazyCancelIf(callable $condition)
+ {
+ $this->deferredCancelCallbacks[] = $condition;
return $this;
}
@@ -199,7 +214,7 @@ public function render(): HtmlString
public function __destruct()
{
- if ($this->wasCancelled) {
+ if ($this->shouldBeCancelled()) {
return;
}
@@ -230,6 +245,21 @@ public function __destruct()
);
}
+ protected function shouldBeCancelled(): bool
+ {
+ if ($this->wasCancelled) {
+ return true;
+ }
+
+ foreach ($this->deferredCancelCallbacks as $condition) {
+ if (value($condition, $this)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
protected function normalizeChannels($channel, $channelClass)
{
if ($channel instanceof Channel) {
diff --git a/src/Broadcasting/Rendering.php b/src/Broadcasting/Rendering.php
index 56288f5d..8c489278 100644
--- a/src/Broadcasting/Rendering.php
+++ b/src/Broadcasting/Rendering.php
@@ -1,21 +1,23 @@
partial = $partial;
$this->data = $data;
diff --git a/src/Commands/Tasks/EnsureCsrfTokenMetaTagExists.php b/src/Commands/Tasks/EnsureCsrfTokenMetaTagExists.php
index 5daf2376..bbb8f0e4 100644
--- a/src/Commands/Tasks/EnsureCsrfTokenMetaTagExists.php
+++ b/src/Commands/Tasks/EnsureCsrfTokenMetaTagExists.php
@@ -1,6 +1,6 @@
displayHeader('Installing Turbo Laravel', ' INFO >');
- $this->installJsDependencies();
$this->updateTemplates();
$this->publishJsFiles();
+ $this->installJsDependencies();
$this->displayAfterNotes();
@@ -38,13 +40,13 @@ private function publishJsFiles()
File::ensureDirectoryExists(resource_path('js/elements'));
File::ensureDirectoryExists(resource_path('js/libs'));
- File::copy(__DIR__ . '/../../stubs/resources/js/libs/turbo.js', resource_path('js/libs/turbo.js'));
- File::copy(__DIR__ . '/../../stubs/resources/js/elements/turbo-echo-stream-tag.js', resource_path('js/elements/turbo-echo-stream-tag.js'));
+ File::copy(__DIR__.'/../../stubs/resources/js/libs/turbo.js', resource_path('js/libs/turbo.js'));
+ File::copy(__DIR__.'/../../stubs/resources/js/elements/turbo-echo-stream-tag.js', resource_path('js/elements/turbo-echo-stream-tag.js'));
if ($this->option('jet')) {
- File::copy(__DIR__ . '/../../stubs/resources/js/libs/alpine-jet.js', resource_path('js/libs/alpine.js'));
+ File::copy(__DIR__.'/../../stubs/resources/js/libs/alpine-jet.js', resource_path('js/libs/alpine.js'));
} elseif ($this->option('alpine')) {
- File::copy(__DIR__ . '/../../stubs/resources/js/libs/alpine.js', resource_path('js/libs/alpine.js'));
+ File::copy(__DIR__.'/../../stubs/resources/js/libs/alpine.js', resource_path('js/libs/alpine.js'));
}
File::put(resource_path('js/app.js'), $this->appJsImportLines());
@@ -86,10 +88,11 @@ private function libsIndexJsImportLines()
private function installJsDependencies()
{
- if (! $this->usingImportmaps()) {
- $this->updateNpmDependencies();
- } else {
+ if ($this->usingImportmaps()) {
$this->updateImportmapsDependencies();
+ } else {
+ $this->updateNpmDependencies();
+ $this->runInstallAndBuildCommand();
}
}
@@ -106,19 +109,47 @@ private function updateNpmDependencies(): void
});
}
+ private function runInstallAndBuildCommand(): void
+ {
+ if (file_exists(base_path('pnpm-lock.yaml'))) {
+ $this->runCommands(['pnpm install', 'pnpm run build']);
+ } elseif (file_exists(base_path('yarn.lock'))) {
+ $this->runCommands(['yarn install', 'yarn run build']);
+ } else {
+ $this->runCommands(['npm install', 'npm run build']);
+ }
+ }
+
+ private function runCommands($commands): void
+ {
+ $process = Process::fromShellCommandline(implode(' && ', $commands), null, null, null, null);
+
+ if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) {
+ try {
+ $process->setTty(true);
+ } catch (RuntimeException $e) {
+ $this->output->writeln(' WARN > '.$e->getMessage().PHP_EOL);
+ }
+ }
+
+ $process->run(function ($type, $line) {
+ $this->output->write(' '.$line);
+ });
+ }
+
private function updateImportmapsDependencies(): void
{
$this->displayTask('pinning JS dependencies (Importmap)', function () {
$dependencies = array_keys($this->jsDependencies());
- return Artisan::call('importmap:pin ' . implode(' ', $dependencies));
+ return Artisan::call('importmap:pin '.implode(' ', $dependencies));
});
}
private function jsDependencies(): array
{
return [
- '@hotwired/turbo' => '^7.2.5',
+ '@hotwired/turbo' => '^8.0.0-beta.1',
'laravel-echo' => '^1.15.0',
'pusher-js' => '^8.0.1',
] + $this->alpineDependencies();
@@ -150,9 +181,7 @@ private function updateTemplates(): void
}
/**
- *
- * @param callable $callback
- * @param bool $dev
+ * @param bool $dev
* @return void
*/
protected static function updateNodePackages(callable $callback, $dev = true)
@@ -174,7 +203,7 @@ protected static function updateNodePackages(callable $callback, $dev = true)
File::put(
base_path('package.json'),
- json_encode($packages, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL
+ json_encode($packages, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT).PHP_EOL
);
}
@@ -183,12 +212,12 @@ private function updateLayouts(): void
$this->existingLayoutFiles()
->each(
fn ($file) => (new Pipeline(app()))
- ->send($file)
- ->through(array_filter([
- $this->option('jet') ? Tasks\EnsureLivewireTurboBridgeExists::class : null,
- Tasks\EnsureCsrfTokenMetaTagExists::class,
- ]))
- ->thenReturn()
+ ->send($file)
+ ->through(array_filter([
+ $this->option('jet') ? Tasks\EnsureLivewireTurboBridgeExists::class : null,
+ Tasks\EnsureCsrfTokenMetaTagExists::class,
+ ]))
+ ->thenReturn()
);
if ($this->option('jet')) {
diff --git a/src/Events/TurboStreamBroadcast.php b/src/Events/TurboStreamBroadcast.php
index e77b7454..8da29bd6 100644
--- a/src/Events/TurboStreamBroadcast.php
+++ b/src/Events/TurboStreamBroadcast.php
@@ -1,6 +1,6 @@
channels = $channels;
$this->action = $action;
diff --git a/src/Exceptions/PageRefreshStrategyException.php b/src/Exceptions/PageRefreshStrategyException.php
new file mode 100644
index 00000000..5aea03d8
--- /dev/null
+++ b/src/Exceptions/PageRefreshStrategyException.php
@@ -0,0 +1,19 @@
+wasFromTurboNative()) {
- return redirect(route("turbo_{$action}_historical_location"));
+ return TurboNativeRedirectResponse::createFromFallbackUrl($action, $fallbackUrl);
}
if ($redirectType === 'back') {
diff --git a/src/Http/Controllers/TurboNativeNavigationController.php b/src/Http/Controllers/TurboNativeNavigationController.php
index 556021fa..4882296a 100644
--- a/src/Http/Controllers/TurboNativeNavigationController.php
+++ b/src/Http/Controllers/TurboNativeNavigationController.php
@@ -1,6 +1,6 @@
header('X-Turbo-Request-Id', null)) {
+ TurboFacade::setTurboTrackingRequestId($requestId);
+ }
+
return $this->turboResponse($next($request), $request);
}
/**
- * @param \Illuminate\Http\Request $request
- * @return bool
+ * @param \Illuminate\Http\Request $request
*/
private function turboNativeVisit($request): bool
{
@@ -63,8 +63,7 @@ private function turboNativeVisit($request): bool
}
/**
- * @param mixed $next
- * @param Request $request
+ * @param mixed $next
* @return RedirectResponse|mixed
*/
private function turboResponse($response, Request $request)
@@ -109,9 +108,8 @@ private function kernel(): Kernel
}
/**
- * @param Request $request
- * @param Response $response
- *
+ * @param Request $request
+ * @param Response $response
* @return Response
*/
private function handleRedirectInternally($request, $response)
@@ -142,7 +140,7 @@ private function createRequestFrom(string $url, Request $baseRequest)
}
/**
- * @param \Illuminate\Http\Request $request
+ * @param \Illuminate\Http\Request $request
* @return bool
*/
private function turboVisit($request)
@@ -151,10 +149,9 @@ private function turboVisit($request)
}
/**
- * @param \Illuminate\Http\Request $request
- * @param string|null $defaultRedirectUrl
+ * @param \Illuminate\Http\Request $request
*/
- private function guessFormRedirectUrl($request, ?string $defaultRedirectUrl = null)
+ private function guessFormRedirectUrl($request, string $defaultRedirectUrl = null)
{
if ($this->inExceptArray($request)) {
return $defaultRedirectUrl;
@@ -180,11 +177,11 @@ private function guessFormRedirectUrl($request, ?string $defaultRedirectUrl = nu
protected function guessRouteName(string $routeName): ?string
{
- if (! Str::endsWith($routeName, ['.store', '.update'])) {
+ if (! Str::endsWith($routeName, ['.store', '.update', '.destroy'])) {
return null;
}
- return str_replace(['.store', '.update'], ['.create', '.edit'], $routeName);
+ return str_replace(['.store', '.update', '.destroy'], ['.create', '.edit', '.delete'], $routeName);
}
protected function inExceptArray(Request $request): bool
diff --git a/src/Http/MultiplePendingTurboStreamResponse.php b/src/Http/MultiplePendingTurboStreamResponse.php
index 0c381aa0..cee29593 100644
--- a/src/Http/MultiplePendingTurboStreamResponse.php
+++ b/src/Http/MultiplePendingTurboStreamResponse.php
@@ -1,6 +1,6 @@
useCustomAttributes = $attributes;
+
+ return $this;
+ }
+
public function append(Model|string $target, $content = null): self
{
return $this->buildAction(
@@ -221,28 +236,36 @@ public function removeAll(Model|string $targets): self
);
}
- private function buildAction(string $action, Model|string $target, $content = null, ?Rendering $rendering = null)
+ public function refresh(): self
+ {
+ return $this->buildAction('refresh')
+ ->attributes(array_filter(['request-id' => Turbo::currentRequestId()]));
+ }
+
+ private function buildAction(string $action, Model|string $target = null, $content = null, Rendering $rendering = null, array $attributes = [])
{
$this->useAction = $action;
$this->useTarget = $target instanceof Model ? $this->resolveTargetFor($target) : $target;
$this->partialView = $rendering?->partial;
$this->partialData = $rendering?->data ?? [];
+ $this->useCustomAttributes = $attributes;
$this->inlineContent = $content;
return $this;
}
- private function buildActionAll(string $action, Model|string $targets, $content = null)
+ private function buildActionAll(string $action, Model|string $targets, $content = null, array $attributes = [])
{
$this->useAction = $action;
$this->useTarget = null;
$this->useTargets = $targets instanceof Model ? $this->resolveTargetFor($targets, resource: true) : $targets;
+ $this->useCustomAttributes = $attributes;
$this->inlineContent = $content;
return $this;
}
- public function broadcastTo($channel, ?callable $callback = null)
+ public function broadcastTo($channel, callable $callback = null)
{
$callback = $callback ?? function () {
};
@@ -252,7 +275,7 @@ public function broadcastTo($channel, ?callable $callback = null)
});
}
- public function broadcastToPrivateChannel($channel, ?callable $callback = null)
+ public function broadcastToPrivateChannel($channel, callable $callback = null)
{
$callback = $callback ?? function () {
};
@@ -263,7 +286,7 @@ public function broadcastToPrivateChannel($channel, ?callable $callback = null)
});
}
- public function broadcastToPresenceChannel($channel, ?callable $callback = null)
+ public function broadcastToPresenceChannel($channel, callable $callback = null)
{
$callback = $callback ?? function () {
};
@@ -280,6 +303,7 @@ private function asPendingBroadcast($channel)
target: $this->useTarget,
targets: $this->useTargets,
channel: $channel,
+ attributes: $this->useCustomAttributes,
)->rendering($this->contentAsRendering());
}
@@ -303,7 +327,7 @@ private function contentAsRendering()
*/
public function toResponse($request)
{
- if ($this->useAction !== 'remove' && ! $this->partialView && ! $this->inlineContent) {
+ if (! in_array($this->useAction, ['remove', 'refresh']) && ! $this->partialView && ! $this->inlineContent) {
throw TurboStreamResponseFailedException::missingPartial();
}
@@ -319,6 +343,7 @@ public function render(): string
'partial' => $this->partialView,
'partialData' => $this->partialData,
'content' => $this->renderInlineContent(),
+ 'attrs' => $this->useCustomAttributes,
])->render();
}
diff --git a/src/Http/TurboNativeRedirectResponse.php b/src/Http/TurboNativeRedirectResponse.php
new file mode 100644
index 00000000..0e6eb3f3
--- /dev/null
+++ b/src/Http/TurboNativeRedirectResponse.php
@@ -0,0 +1,68 @@
+withQueryString((new self($fallbackUrl))->getQueryString());
+ }
+
+ /**
+ * Sets the flashed data via query strings when redirecting to Turbo Native routes.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @return self
+ */
+ public function with($key, $value = null)
+ {
+ $params = $this->getQueryString();
+
+ return $this->withoutQueryStrings()
+ ->setTargetUrl($this->getTargetUrl().'?'.http_build_query($params + [$key => urlencode($value)]));
+ }
+
+ /**
+ * Sets multiple query strings at the same time.
+ */
+ protected function withQueryString(array $params): self
+ {
+ foreach ($params as $key => $val) {
+ $this->with($key, $val);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the query string as an array.
+ */
+ protected function getQueryString(): array
+ {
+ parse_str(str_contains($this->getTargetUrl(), '?') ? Str::after($this->getTargetUrl(), '?') : '', $query);
+
+ return $query;
+ }
+
+ /**
+ * Returns the target URL without the query strings.
+ */
+ protected function withoutQueryStrings(): self
+ {
+ $fragment = str_contains($this->getTargetUrl(), '#') ? Str::after($this->getTargetUrl(), '#') : '';
+
+ return $this->withoutFragment()
+ ->setTargetUrl(Str::before($this->getTargetUrl(), '?').($fragment ? "#{$fragment}" : ''));
+ }
+}
diff --git a/src/Http/TurboResponseFactory.php b/src/Http/TurboResponseFactory.php
index e582ac2d..fdec530c 100644
--- a/src/Http/TurboResponseFactory.php
+++ b/src/Http/TurboResponseFactory.php
@@ -1,8 +1,8 @@
channels = $channels;
$this->action = $action;
diff --git a/src/Models/Broadcasts.php b/src/Models/Broadcasts.php
index 46267c1a..3a047720 100644
--- a/src/Models/Broadcasts.php
+++ b/src/Models/Broadcasts.php
@@ -1,45 +1,76 @@
broadcastAppendTo(
- $this->brodcastDefaultStreamables(inserting: true)
+ $this->broadcastDefaultStreamables(inserting: true)
);
}
public function broadcastPrepend(): PendingBroadcast
{
return $this->broadcastPrependTo(
- $this->brodcastDefaultStreamables(inserting: true)
+ $this->broadcastDefaultStreamables(inserting: true)
);
}
public function broadcastBefore(string $target, bool $inserting = true): PendingBroadcast
{
return $this->broadcastBeforeTo(
- $this->brodcastDefaultStreamables($inserting),
+ $this->broadcastDefaultStreamables($inserting),
$target
);
}
@@ -47,7 +78,7 @@ public function broadcastBefore(string $target, bool $inserting = true): Pending
public function broadcastAfter(string $target, bool $inserting = true): PendingBroadcast
{
return $this->broadcastAfterTo(
- $this->brodcastDefaultStreamables($inserting),
+ $this->broadcastDefaultStreamables($inserting),
$target
);
}
@@ -59,7 +90,7 @@ public function broadcastInsert(): PendingBroadcast
: 'append';
return $this->broadcastActionTo(
- $this->brodcastDefaultStreamables(inserting: true),
+ $this->broadcastDefaultStreamables(inserting: true),
$action,
Rendering::forModel($this),
);
@@ -68,21 +99,28 @@ public function broadcastInsert(): PendingBroadcast
public function broadcastReplace(): PendingBroadcast
{
return $this->broadcastReplaceTo(
- $this->brodcastDefaultStreamables()
+ $this->broadcastDefaultStreamables()
);
}
public function broadcastUpdate(): PendingBroadcast
{
return $this->broadcastUpdateTo(
- $this->brodcastDefaultStreamables()
+ $this->broadcastDefaultStreamables()
);
}
public function broadcastRemove(): PendingBroadcast
{
return $this->broadcastRemoveTo(
- $this->brodcastDefaultStreamables()
+ $this->broadcastDefaultStreamables()
+ );
+ }
+
+ public function broadcastRefresh(): PendingBroadcast
+ {
+ return $this->broadcastRefreshTo(
+ $this->broadcastRefreshDefaultStreamables()
);
}
@@ -121,12 +159,18 @@ public function broadcastRemoveTo($streamable): PendingBroadcast
return $this->broadcastActionTo($streamable, 'remove', Rendering::empty());
}
+ public function broadcastRefreshTo($streamable): PendingBroadcast
+ {
+ return TurboStream::broadcastRefresh($this->toChannels(Collection::wrap($streamable)))
+ ->cancelIf(fn () => static::isIgnoringTurboStreamBroadcasts());
+ }
+
public function asTurboStreamBroadcastingChannel()
{
- return $this->toChannels(Collection::wrap($this->brodcastDefaultStreamables($this->wasRecentlyCreated)));
+ return $this->toChannels(Collection::wrap($this->broadcastDefaultStreamables($this->wasRecentlyCreated)));
}
- protected function broadcastActionTo($streamables, string $action, Rendering $rendering, ?string $target = null): PendingBroadcast
+ protected function broadcastActionTo($streamables, string $action, Rendering $rendering, string $target = null): PendingBroadcast
{
return TurboStream::broadcastAction(
action: $action,
@@ -134,26 +178,53 @@ protected function broadcastActionTo($streamables, string $action, Rendering $re
targets: null,
channel: $this->toChannels(Collection::wrap($streamables)),
content: $rendering,
- );
+ )->cancelIf(static::isIgnoringTurboStreamBroadcasts());
}
- protected function brodcastDefaultStreamables(bool $inserting = false)
+ protected function broadcastRefreshDefaultStreamables()
{
- if (property_exists($this, 'broadcastsTo')) {
- return Collection::wrap($this->broadcastsTo)
+ return $this->broadcastDefaultStreamables(inserting: $this->wasRecentlyCreated, broadcastToProperty: 'broadcastsRefreshesTo', broadcastsProperty: 'broadcastsRefreshes');
+ }
+
+ /**
+ * @deprecated There was a typo here. Use `broadcastDefaultStreamables` instead.
+ */
+ protected function brodcastDefaultStreamables(bool $inserting = false, string $broadcastToProperty = 'broadcastsTo', string $broadcastsProperty = 'broadcasts')
+ {
+ return $this->broadcastDefaultStreamables($inserting, $broadcastToProperty, $broadcastsProperty);
+ }
+
+ protected function broadcastDefaultStreamables(bool $inserting = false, string $broadcastToProperty = 'broadcastsTo', string $broadcastsProperty = 'broadcasts')
+ {
+ if (property_exists($this, $broadcastToProperty)) {
+ return Collection::wrap($this->{$broadcastToProperty})
->map(fn ($related) => $this->{$related})
->values()
->all();
}
- if (method_exists($this, 'broadcastsTo')) {
- return $this->broadcastsTo();
+ if (method_exists($this, $broadcastToProperty)) {
+ return $this->{$broadcastToProperty}();
}
- if ($inserting && is_array($this->broadcasts) && isset($this->broadcasts['stream'])) {
- return $this->broadcasts['stream'];
+ if ($inserting && is_array($this->{$broadcastsProperty}) && isset($this->{$broadcastsProperty}['stream'])) {
+ return $this->{$broadcastsProperty}['stream'];
}
+ return $this->broadcastDefaultStreamableForCurrentModel($inserting);
+ }
+
+ protected function broadcastDefaultRefreshStreamables()
+ {
+ if (property_exists($this, 'broadcastsRefreshesTo') && is_array($this->broadcastsRefreshesTo) && isset($this->broadcastsRefreshesTo['stream'])) {
+ return $this->broadcastsRefreshesTo['stream'];
+ }
+
+ return $this->broadcastDefaultStreamableForCurrentModel(inserting: true);
+ }
+
+ protected function broadcastDefaultStreamableForCurrentModel(bool $inserting)
+ {
if ($inserting) {
return Name::forModel($this)->plural;
}
diff --git a/src/Models/ModelObserver.php b/src/Models/ModelObserver.php
index f57f512e..4b1cd0d4 100644
--- a/src/Models/ModelObserver.php
+++ b/src/Models/ModelObserver.php
@@ -1,6 +1,6 @@
shouldBroadcast($model)) {
- return;
+ if ($this->shouldBroadcastRefresh($model)) {
+ $model->broadcastRefresh()->later();
}
- if ($model->wasRecentlyCreated) {
- $model->broadcastInsert()->later();
- } else {
- $model->broadcastReplace()->later();
+ if ($this->shouldBroadcast($model)) {
+ if ($model->wasRecentlyCreated) {
+ $model->broadcastInsert()->later();
+ } else {
+ $model->broadcastReplace()->later();
+ }
}
}
/**
- * @param Model|Broadcasts $model
+ * @param Model|Broadcasts $model
*/
public function deleted(Model $model)
{
- if (! $this->shouldBroadcast($model)) {
- return;
+ if ($this->shouldBroadcastRefresh($model)) {
+ $model->broadcastRefresh()->later();
}
- $model->broadcastRemove()->later();
+ if ($this->shouldBroadcast($model)) {
+ $model->broadcastRemove()->later();
+ }
+ }
+
+ private function shouldBroadcastRefresh(Model $model): bool
+ {
+ if (property_exists($model, 'broadcastsRefreshes')) {
+ return true;
+ }
+
+ if (property_exists($model, 'broadcastsRefreshesTo')) {
+ return true;
+ }
+
+ return false;
}
private function shouldBroadcast(Model $model): bool
diff --git a/src/Models/Naming/Name.php b/src/Models/Naming/Name.php
index 7f4addd9..565f33b0 100644
--- a/src/Models/Naming/Name.php
+++ b/src/Models/Naming/Name.php
@@ -1,6 +1,6 @@
withHeader('User-Agent', 'Turbo Native Android; Mozilla: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.3 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/43.4');
}
+
+ public function fromTurboFrame(string $frame): self
+ {
+ return $this->withHeader('Turbo-Frame', $frame);
+ }
}
diff --git a/src/Testing/TurboStreamMatcher.php b/src/Testing/TurboStreamMatcher.php
index 07f864a0..b425dba6 100644
--- a/src/Testing/TurboStreamMatcher.php
+++ b/src/Testing/TurboStreamMatcher.php
@@ -1,6 +1,6 @@
visitFromTurboNative;
@@ -36,9 +37,20 @@ public function setVisitingFromTurboNative(): self
return $this;
}
+ public function setTurboTrackingRequestId(string $requestId): self
+ {
+ $this->turboRequestId = $requestId;
+
+ return $this;
+ }
+
+ public function currentRequestId(): ?string
+ {
+ return $this->turboRequestId;
+ }
+
/**
- * @param bool|Closure $toOthers
- *
+ * @param bool|Closure $toOthers
* @return \Illuminate\Support\HigherOrderTapProxy|mixed
*/
public function broadcastToOthers($toOthers = true)
diff --git a/src/TurboServiceProvider.php b/src/TurboServiceProvider.php
index 0b393300..94c60d86 100644
--- a/src/TurboServiceProvider.php
+++ b/src/TurboServiceProvider.php
@@ -1,7 +1,18 @@
mergeConfigFrom(__DIR__ . '/../config/turbo-laravel.php', 'turbo-laravel');
+ $this->mergeConfigFrom(__DIR__.'/../config/turbo-laravel.php', 'turbo-laravel');
$this->app->scoped(Turbo::class);
$this->app->bind(Broadcaster::class, LaravelBroadcaster::class);
+ $this->app->scoped(Limiter::class);
}
private function configureComponents()
@@ -53,6 +55,7 @@ private function configureComponents()
ViewComponents\Frame::class,
ViewComponents\Stream::class,
ViewComponents\StreamFrom::class,
+ ViewComponents\RefreshesWith::class,
]);
}
@@ -63,11 +66,11 @@ private function configurePublications()
}
$this->publishes([
- __DIR__ . '/../config/turbo-laravel.php' => config_path('turbo-laravel.php'),
+ __DIR__.'/../config/turbo-laravel.php' => config_path('turbo-laravel.php'),
], 'turbo-config');
$this->publishes([
- __DIR__ . '/../resources/views' => base_path('resources/views/vendor/turbo-laravel'),
+ __DIR__.'/../resources/views' => base_path('resources/views/vendor/turbo-laravel'),
], 'turbo-views');
$this->publishes([
@@ -97,11 +100,11 @@ private function configureMacros(): void
});
Blade::directive('domid', function ($expression) {
- return "";
+ return "";
});
Blade::directive('domclass', function ($expression) {
- return "";
+ return "";
});
Blade::directive('channel', function ($expression) {
@@ -123,9 +126,21 @@ private function configureRequestAndResponseMacros(): void
return Str::contains($this->header('Accept'), Turbo::TURBO_STREAM_FORMAT);
});
+ Request::macro('wantsTurboStreams', function (): bool {
+ return $this->wantsTurboStream();
+ });
+
Request::macro('wasFromTurboNative', function (): bool {
return TurboFacade::isTurboNativeVisit();
});
+
+ Request::macro('wasFromTurboFrame', function (string $frame = null): bool {
+ if (! $frame) {
+ return $this->hasHeader('Turbo-Frame');
+ }
+
+ return $this->header('Turbo-Frame', null) === $frame;
+ });
}
private function configureTestResponseMacros()
@@ -154,6 +169,18 @@ private function configureTestResponseMacros()
$this->headers->get('Content-Type'),
);
});
+
+ TestResponse::macro('assertRedirectRecede', function (array $with = []) {
+ $this->assertRedirectToRoute('turbo_recede_historical_location', $with);
+ });
+
+ TestResponse::macro('assertRedirectResume', function (array $with = []) {
+ $this->assertRedirectToRoute('turbo_resume_historical_location', $with);
+ });
+
+ TestResponse::macro('assertRedirectRefresh', function (array $with = []) {
+ $this->assertRedirectToRoute('turbo_refresh_historical_location', $with);
+ });
}
protected function configureMiddleware(): void
diff --git a/src/Views/Components/Frame.php b/src/Views/Components/Frame.php
index 993a2245..29e9ea7d 100644
--- a/src/Views/Components/Frame.php
+++ b/src/Views/Components/Frame.php
@@ -1,11 +1,11 @@
method = $method;
+ $this->scroll = $scroll;
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ *
+ * @return \Illuminate\Contracts\View\View|\Closure|string
+ */
+ public function render()
+ {
+ return view('turbo-laravel::components.refreshes-with');
+ }
+}
diff --git a/src/Views/Components/Stream.php b/src/Views/Components/Stream.php
index cebdb97b..4e1d3bac 100644
--- a/src/Views/Components/Stream.php
+++ b/src/Views/Components/Stream.php
@@ -1,13 +1,12 @@
record = $record;
}
- public function domId(?string $prefix = null): string
+ public function domId(string $prefix = null): string
{
if ($recordId = $this->record->getKey()) {
return sprintf('%s%s%s', $this->domClass($prefix), self::DELIMITER, $recordId);
@@ -32,7 +33,7 @@ public function domId(?string $prefix = null): string
return $this->domClass($prefix ?: static::NEW_PREFIX);
}
- public function domClass(?string $prefix = null): string
+ public function domClass(string $prefix = null): string
{
$singular = Name::forModel($this->record)->singular;
$delimiter = static::DELIMITER;
diff --git a/src/Views/UnidentifiableRecordException.php b/src/Views/UnidentifiableRecordException.php
index deaa334a..6c647486 100644
--- a/src/Views/UnidentifiableRecordException.php
+++ b/src/Views/UnidentifiableRecordException.php
@@ -1,6 +1,6 @@
domId($prefix);
}
@@ -30,12 +25,8 @@ function dom_id(object $model, string $prefix = ""): string
if (! function_exists('dom_class')) {
/**
* Generates the DOM CSS Class for a specific model.
- *
- * @param object $model
- * @param string $prefix
- * @return string
*/
- function dom_class(object $model, string $prefix = ""): string
+ function dom_class(object $model, string $prefix = ''): string
{
return (new RecordIdentifier($model))->domClass($prefix);
}
@@ -45,8 +36,8 @@ function dom_class(object $model, string $prefix = ""): string
/**
* Builds the Turbo Streams.
*
- * @param Model|Collection|array|string|null $model = null
- * @param string|null $action = null
+ * @param Model|Collection|array|string|null $model = null
+ * @param string|null $action = null
*/
function turbo_stream($model = null, string $action = null): MultiplePendingTurboStreamResponse|PendingTurboStreamResponse
{
@@ -66,9 +57,8 @@ function turbo_stream($model = null, string $action = null): MultiplePendingTurb
/**
* Renders a Turbo Stream view wrapped with the correct Content-Types in the response.
*
- * @param string|\Illuminate\View\View $view
- * @param array $data = [] the binding params to be passed to the view.
- * @return \Illuminate\Http\Response|\Illuminate\Contracts\Routing\ResponseFactory
+ * @param string|\Illuminate\View\View $view
+ * @param array $data = [] the binding params to be passed to the view.
*/
function turbo_stream_view($view, array $data = []): Response|ResponseFactory
{
diff --git a/stubs/resources/js/elements/turbo-echo-stream-tag.js b/stubs/resources/js/elements/turbo-echo-stream-tag.js
index cddf6c79..d3ee66eb 100644
--- a/stubs/resources/js/elements/turbo-echo-stream-tag.js
+++ b/stubs/resources/js/elements/turbo-echo-stream-tag.js
@@ -16,7 +16,7 @@ class TurboEchoStreamSourceElement extends HTMLElement {
async connectedCallback() {
connectStreamSource(this)
this.subscription = subscribeTo(this.type, this.channel)
- .listen('.Tonysm\\TurboLaravel\\Events\\TurboStreamBroadcast', (e) => {
+ .listen('.HotwiredLaravel\\TurboLaravel\\Events\\TurboStreamBroadcast', (e) => {
this.dispatchMessageEvent(e.message)
})
}
diff --git a/testbench.yaml b/testbench.yaml
new file mode 100644
index 00000000..e7983682
--- /dev/null
+++ b/testbench.yaml
@@ -0,0 +1,23 @@
+providers:
+ - HotwiredLaravel\TurboLaravel\TurboServiceProvider
+ - Workbench\App\Providers\WorkbenchAppServiceProvider
+
+migrations:
+ - workbench/database/migrations
+
+components:
+ - workbench/resources/views/components
+
+workbench:
+ start: '/articles'
+ install: true
+ welcome: true
+ discovers:
+ web: true
+ views: true
+ build:
+ - create-sqlite-db
+ - db:wipe
+ - migrate:refresh
+ assets: []
+ sync: []
diff --git a/tests/Broadcasting/LimiterTest.php b/tests/Broadcasting/LimiterTest.php
new file mode 100644
index 00000000..62323e02
--- /dev/null
+++ b/tests/Broadcasting/LimiterTest.php
@@ -0,0 +1,25 @@
+freezeTime();
+
+ $debouncer = new Limiter();
+
+ $this->assertFalse($debouncer->shouldLimit('my-key'));
+ $this->assertTrue($debouncer->shouldLimit('my-key'));
+
+ $this->travel(501)->milliseconds();
+
+ $this->assertFalse($debouncer->shouldLimit('my-key'));
+ $this->assertTrue($debouncer->shouldLimit('my-key'));
+ }
+}
diff --git a/tests/Events/TurboStreamBroadcastTest.php b/tests/Events/TurboStreamBroadcastTest.php
index d853c278..5ef22c59 100644
--- a/tests/Events/TurboStreamBroadcastTest.php
+++ b/tests/Events/TurboStreamBroadcastTest.php
@@ -1,70 +1,63 @@
create()->fresh();
+
$event = new TurboStreamBroadcast(
[],
'replace',
- 'test_target',
+ 'article_'.$article->id,
null,
- 'test_models._test_model',
- ['testModel' => new TestModel(['id' => 1])]
+ 'articles._article',
+ ['article' => $article],
);
$expected = View::make('turbo-laravel::turbo-stream', [
- 'target' => 'test_target',
+ 'target' => 'article_'.$article->id,
'action' => 'replace',
- 'partial' => 'test_models._test_model',
+ 'partial' => 'articles._article',
'partialData' => [
- 'testModel' => new TestModel(['id' => 1]),
+ 'article' => $article,
],
- ]);
-
- $rendered = $event->render();
+ ])->render();
- $this->assertEquals(trim($expected), trim($rendered));
+ $this->assertEquals(trim($expected), trim($event->render()));
}
/** @test */
public function renders_turbo_stream_targets()
{
+ $article = ArticleFactory::new()->create()->fresh();
+
$event = new TurboStreamBroadcast(
[],
'replace',
null,
- '.targets',
- 'test_models._test_model',
- ['testModel' => new TestModel(['id' => 1])],
+ '.articles',
+ 'articles._article',
+ ['article' => $article],
);
$expected = View::make('turbo-laravel::turbo-stream', [
'action' => 'replace',
- 'targets' => '.targets',
- 'partial' => 'test_models._test_model',
+ 'targets' => '.articles',
+ 'partial' => 'articles._article',
'partialData' => [
- 'testModel' => new TestModel(['id' => 1]),
+ 'article' => $article,
],
- ]);
-
- $rendered = $event->render();
+ ])->render();
- $this->assertEquals(trim($expected), trim($rendered));
+ $this->assertEquals(trim($expected), trim($event->render()));
}
}
diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php
index 3f686227..2cacbc16 100644
--- a/tests/FunctionsTest.php
+++ b/tests/FunctionsTest.php
@@ -2,31 +2,34 @@
namespace Tests;
+use HotwiredLaravel\TurboLaravel\Tests\TestCase;
+use HotwiredLaravel\TurboLaravel\Turbo;
use Illuminate\Support\Facades\View;
-use function Tonysm\TurboLaravel\dom_class;
-use function Tonysm\TurboLaravel\dom_id;
+use Workbench\App\Models\Article;
-use Tonysm\TurboLaravel\Tests\Stubs\Models\TestModel;
-use Tonysm\TurboLaravel\Tests\TestCase;
-use Tonysm\TurboLaravel\Turbo;
-
-use function Tonysm\TurboLaravel\turbo_stream;
-use function Tonysm\TurboLaravel\turbo_stream_view;
+use function HotwiredLaravel\TurboLaravel\dom_class;
+use function HotwiredLaravel\TurboLaravel\dom_id;
+use function HotwiredLaravel\TurboLaravel\turbo_stream;
+use function HotwiredLaravel\TurboLaravel\turbo_stream_view;
class FunctionsTest extends TestCase
{
+ private Article $article;
+
public function setUp(): void
{
parent::setUp();
- View::addLocation(__DIR__ . '/Stubs/views');
+ View::addLocation(__DIR__.'/Stubs/views');
+
+ $this->article = Article::create(['title' => 'Hello World']);
}
/** @test */
public function namespaced_turbo_stream_fn()
{
$this->assertEquals(
- trim(<<
Hello World
@@ -35,7 +38,7 @@ public function namespaced_turbo_stream_fn()
);
$this->assertEquals(
- trim(<<
Hello World
@@ -49,18 +52,17 @@ public function namespaced_turbo_stream_fn()
])),
);
- $testModel = TestModel::create(['name' => 'Hello']);
- $expected = trim(view('test_models._test_model', [
- 'testModel' => $testModel,
+ $expected = trim(view('articles._article', [
+ 'article' => $this->article,
])->render());
$this->assertEquals(
trim(<<
+
{$expected}
HTML),
- trim(turbo_stream($testModel)),
+ trim(turbo_stream($this->article)),
);
}
@@ -68,7 +70,7 @@ public function namespaced_turbo_stream_fn()
public function global_turbo_stream_fn()
{
$this->assertEquals(
- trim(<<
Hello World
@@ -77,7 +79,7 @@ public function global_turbo_stream_fn()
);
$this->assertEquals(
- trim(<<
Hello World
@@ -91,31 +93,25 @@ public function global_turbo_stream_fn()
])),
);
- $testModel = TestModel::create(['name' => 'Hello']);
- $expected = trim(view('test_models._test_model', [
- 'testModel' => $testModel,
+ $expected = trim(view('articles._article', [
+ 'article' => $this->article,
])->render());
$this->assertEquals(
trim(<<
+
{$expected}
HTML),
- trim(\turbo_stream($testModel)),
+ trim(\turbo_stream($this->article)),
);
}
/** @test */
public function namespace_turbo_stream_htmlable()
{
- $testModel = TestModel::create(['name' => 'Hello']);
- $expected = trim(view('test_models._test_model', [
- 'testModel' => $testModel,
- ])->render());
-
$this->assertEquals(
- trim(<<
Hello World
@@ -123,17 +119,21 @@ public function namespace_turbo_stream_htmlable()
HTML),
- trim(View::make('turbo_stream_global_htmlable_multiple')->render())
+ trim(View::make('functions.turbo_stream_ns_fn_htmlable_multiple')->render())
);
+ $expected = trim(view('articles._article', [
+ 'article' => $this->article,
+ ])->render());
+
$this->assertEquals(
trim(<<
+
{$expected}
HTML),
- trim(View::make('turbo_stream_global_htmlable_model', [
- 'testModel' => $testModel,
+ trim(View::make('functions.turbo_stream_ns_fn_htmlable_model', [
+ 'model' => $this->article,
])->render())
);
}
@@ -141,13 +141,8 @@ public function namespace_turbo_stream_htmlable()
/** @test */
public function global_turbo_stream_htmlable()
{
- $testModel = TestModel::create(['name' => 'Hello']);
- $expected = trim(view('test_models._test_model', [
- 'testModel' => $testModel,
- ])->render());
-
$this->assertEquals(
- trim(<<
Hello World
@@ -155,17 +150,21 @@ public function global_turbo_stream_htmlable()
HTML),
- trim(View::make('turbo_stream_global_htmlable_multiple')->render())
+ trim(View::make('functions.turbo_stream_global_fn_htmlable_multiple')->render())
);
+ $expected = trim(view('articles._article', [
+ 'article' => $this->article,
+ ])->render());
+
$this->assertEquals(
trim(<<
+
{$expected}
HTML),
- trim(View::make('turbo_stream_global_htmlable_model', [
- 'testModel' => $testModel,
+ trim(View::make('functions.turbo_stream_global_fn_htmlable_model', [
+ 'model' => $this->article,
])->render())
);
}
@@ -173,45 +172,37 @@ public function global_turbo_stream_htmlable()
/** @test */
public function namespaced_dom_id_fn()
{
- $testModel = TestModel::create(['name' => 'Hello']);
-
- $this->assertEquals("test_model_{$testModel->id}", dom_id($testModel));
+ $this->assertEquals("article_{$this->article->id}", dom_id($this->article));
}
/** @test */
public function global_dom_id_fn()
{
- $testModel = TestModel::create(['name' => 'Hello']);
-
- $this->assertEquals("test_model_{$testModel->id}", \dom_id($testModel));
+ $this->assertEquals("article_{$this->article->id}", \dom_id($this->article));
}
/** @test */
public function namespaced_dom_class_fn()
{
- $testModel = TestModel::create(['name' => 'Hello']);
-
- $this->assertEquals("test_model", dom_class($testModel));
+ $this->assertEquals('article', dom_class($this->article));
}
/** @test */
public function global_dom_class_fn()
{
- $testModel = TestModel::create(['name' => 'Hello']);
-
- $this->assertEquals("test_model", \dom_class($testModel));
+ $this->assertEquals('article', \dom_class($this->article));
}
/** @test */
public function namespaced_turbo_stream_view_fn()
{
- $response = turbo_stream_view('turbo_stream_view_namespaced', [
- 'title' => 'Post Namespaced',
+ $response = turbo_stream_view('functions.turbo_stream_view', [
+ 'title' => 'Post Using Namespaced Function',
]);
$this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $response->headers->get('Content-Type'));
$this->assertEquals(
- view('turbo_stream_view_namespaced', ['title' => 'Post Namespaced'])->render(),
+ view('functions.turbo_stream_view', ['title' => 'Post Using Namespaced Function'])->render(),
$response->content(),
);
}
@@ -219,13 +210,13 @@ public function namespaced_turbo_stream_view_fn()
/** @test */
public function global_turbo_stream_view_fn()
{
- $response = \turbo_stream_view('turbo_stream_view_global', [
+ $response = \turbo_stream_view('functions.turbo_stream_view', [
'title' => 'Post Global',
]);
$this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $response->headers->get('Content-Type'));
$this->assertEquals(
- view('turbo_stream_view_global', ['title' => 'Post Global'])->render(),
+ view('functions.turbo_stream_view', ['title' => 'Post Global'])->render(),
$response->content(),
);
}
diff --git a/tests/Http/MacroablePendingStreamTest.php b/tests/Http/MacroablePendingStreamTest.php
index 1276b75c..e6d95a3e 100644
--- a/tests/Http/MacroablePendingStreamTest.php
+++ b/tests/Http/MacroablePendingStreamTest.php
@@ -1,53 +1,36 @@
get('/testing/macroable-streams', function () {
- if (request()->wantsTurboStream()) {
- return turbo_stream()->flash('Hello World');
- }
-
- return 'No Turbo Stream';
- })->name('testing.macroable-streams');
- }
-
/** @test */
public function turbo_stream_can_be_macroable()
{
- PendingTurboStreamResponse::macro('flash', function (string $message) {
- return $this->append('notifications', view('notification', [
- 'message' => $message,
- ]));
- });
+ $article = ArticleFactory::new()->create();
$this->turbo()
- ->get(route('testing.macroable-streams'))
+ ->put(route('articles.update', $article), ['title' => 'Title Updated'])
->assertTurboStream(fn (AssertableTurboStream $streams) => (
$streams->hasTurboStream(fn (TurboStreamMatcher $stream) => (
+ $stream->where('action', 'replace')
+ ->where('target', 'article_'.$article->id)
+ ->see('Title Updated')
+ ))
+ && $streams->hasTurboStream(fn (TurboStreamMatcher $stream) => (
$stream->where('action', 'append')
->where('target', 'notifications')
- ->see('Hello World')
+ ->see('Article updated.')
))
+ && $streams->has(2)
));
}
}
diff --git a/tests/Http/Middleware/TurboMiddlewareTest.php b/tests/Http/Middleware/TurboMiddlewareTest.php
index dc8a84e0..f184a771 100644
--- a/tests/Http/Middleware/TurboMiddlewareTest.php
+++ b/tests/Http/Middleware/TurboMiddlewareTest.php
@@ -1,109 +1,69 @@
has('frame') ? ' (frame=' . request('frame') . ')' : '');
- })->name('test-models.create');
-
- Route::post('/test-models', function () {
- request()->validate(['name' => 'required']);
- })->name('test-models.store')->middleware(TurboMiddleware::class);
-
- Route::get('/test-models/{testModel}/edit', function () {
- return 'show edit form' . (request()->has('frame') ? ' (frame=' . request('frame') . ')' : '');
- })->name('test-models.edit');
-
- Route::put('/test-models/{testModel}', function (TestModel $model) {
- request()->validate(['name' => 'required']);
- })->name('test-models.update')->middleware(TurboMiddleware::class);
-
- Route::get('/test-models-form-request', function () {
- return 'show create form when using form requests';
- })->name('test-models-form-requests.create');
-
- Route::post('/test-models-form-request', function (TestFormRequest $request) {
- })->name('test-models-form-requests.store')->middleware(TurboMiddleware::class);
- }
-
- /**
- * @test
- * @define-route usesTestModelResourceRoutes
- */
+ /** @test */
public function doesnt_change_redirect_response_when_not_turbo_visit()
{
- $response = $this->from('/source')->post('/test-models', []);
-
- $response->assertRedirect('/source');
- $response->assertStatus(302);
+ $this->from('/articles')
+ ->post('/articles', [])
+ ->assertRedirect('/articles')
+ ->assertStatus(302);
}
- /**
- * @test
- * @define-route usesTestModelResourceRoutes
- */
+ /** @test */
public function handles_invalid_forms_with_an_internal_redirect()
{
- $response = $this->from('/source')->post('/test-models', [], [
- 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
- ]);
-
- $response->assertSee('show create form');
- $response->assertStatus(422);
+ $this->from('/articles')
+ ->post('/articles', headers: [
+ 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
+ ])
+ ->assertSee('New Article')
+ ->assertSee('The title field is required.')
+ ->assertStatus(422);
}
- /**
- * @test
- * @define-route usesTestModelResourceRoutes
- */
+ /** @test */
public function handles_invalid_forms_with_an_internal_redirect_when_using_form_requests()
{
- $response = $this->from('/source')->post('/test-models-form-request', [], [
- 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
- ]);
-
- $response->assertSee('show create form when using form requests');
- $response->assertStatus(422);
- }
-
- public function usesTurboNativeRoute()
- {
- Route::get('/test-models', function () {
- if (TurboFacade::isTurboNativeVisit()) {
- return 'hello turbo native';
- }
+ $article = Article::create(['title' => 'Hello World']);
- return 'hello not turbo native';
- })->name('test-models.index')->middleware(TurboMiddleware::class);
+ $this
+ ->from("/articles/{$article->id}")
+ ->post(route('articles.comments.store', $article), headers: [
+ 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
+ ])
+ ->assertSee('New Comment')
+ ->assertSee('The content field is required.')
+ ->assertStatus(422);
}
- /**
- * @test
- * @define-route usesTurboNativeRoute
- */
+ /** @test */
public function can_detect_turbo_native_visits()
{
+ ArticleFactory::new()->times(3)->create();
+
$this->assertFalse(
TurboFacade::isTurboNativeVisit(),
'Expected to not have started saying it is a Turbo Native visit, but it said it is.'
);
- $this->get('/test-models', [
+ $this->get('/articles', [
'User-Agent' => 'Turbo Native Android',
- ])->assertSee('hello turbo native');
+ ])->assertJsonStructure([
+ 'data' => [
+ '*' => ['id', 'title', 'content', 'created_at', 'updated_at'],
+ ],
+ ]);
$this->assertTrue(
TurboFacade::isTurboNativeVisit(),
@@ -111,201 +71,112 @@ public function can_detect_turbo_native_visits()
);
}
- public function usesTestModelRoutesWithCustomRedirect()
- {
- Route::get('/somewhere-else', function () {
- return 'show somewhere else';
- })->name('somewhere-else');
-
- Route::get('/test-models/create', function () {
- return 'show create form' . (request()->has('frame') ? ' (frame=' . request('frame') . ')' : '');
- });
-
- Route::post('/test-models', function (TestFormRequest $request) {
- // Laravel sets the redirectTo when the form request validation fails...
- })->middleware(TurboMiddleware::class);
- }
-
- /**
- * @test
- * @define-route usesTestModelRoutesWithCustomRedirect
- */
+ /** @test */
public function uses_the_redirect_to_when_guessed_route_doesnt_exist()
{
- $response = $this->from('/somewhere-else')->post('/test-models', [], [
- 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
- ]);
+ $comment = CommentFactory::new()->create();
- $response->assertSee('show somewhere else');
- $response->assertStatus(422);
- }
-
- public function usesNamedTestRoutesWithFormRequest()
- {
- Route::get('/somewhere-else', function () {
- return 'show somewhere else';
- })->name('somewhere-else');
+ // There's no route named `comments.edit`, so it redirects "back".
- Route::get('/test-models/create', function () {
- return 'show create form';
- })->name('test-models.create');
-
- Route::post('/test-models', function (TestFormRequest $request) {
- // Laravel sets the redirectTo when the form request validation fails...
- })->name('test-models.store')->middleware(TurboMiddleware::class);
+ $this->from(route('articles.show', $comment->article))
+ ->put(route('comments.update', $comment), headers: [
+ 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
+ ])
+ ->assertSee($comment->article->title, escape: false)
+ ->assertUnprocessable();
}
- /**
- * @test
- * @define-route usesNamedTestRoutesWithFormRequest
- */
+ /** @test */
public function can_prevent_redirect_route()
{
config()->set('turbo-laravel.redirect_guessing_exceptions', [
- '/test-models',
- ]);
-
- $response = $this->from('/somewhere-else')->post('/test-models', [], [
- 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
+ '/articles*',
]);
- $response->assertSee('show somewhere else');
- $response->assertStatus(422);
+ $this->from(route('articles.index'))
+ ->post(route('articles.store'), headers: [
+ 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
+ ])
+ ->assertRedirectToRoute('articles.index');
}
- /**
- * @test
- * @define-route usesTestModelResourceRoutes
- */
+ /** @test */
public function sends_an_internal_redirect_to_resource_create_routes_on_failed_validation_follows_laravel_conventions_and_returns_422_status_code()
{
- $response = $this->from('/source')->post(route('test-models.store'), [], [
- 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
- ]);
-
- $response->assertSee('show create form');
- $response->assertStatus(422);
+ $this
+ ->from(route('articles.index'))
+ ->post(route('articles.store'), headers: [
+ 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
+ ])
+ ->assertSee('New Article')
+ ->assertSee('The title field is required.')
+ ->assertUnprocessable();
}
- /**
- * @test
- * @define-route usesTestModelResourceRoutes
- */
+ /** @test */
public function redirects_back_to_resource_edit_routes_on_failed_validation_follows_laravel_conventions()
{
- $testModel = TestModel::create(['name' => 'Dummy model']);
+ $article = ArticleFactory::new()->create();
- $response = $this->from('/source')->put(route('test-models.update', $testModel), [], [
- 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
- ]);
-
- $response->assertSee('show edit form');
- $response->assertStatus(422);
+ $this->from(route('articles.index'))
+ ->put(route('articles.update', $article), headers: [
+ 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
+ ])
+ ->assertSee('Edit Article')
+ ->assertSee('The title field is required.')
+ ->assertUnprocessable();
}
- /**
- * @test
- * @define-route usesTestModelResourceRoutes
- */
+ /** @test */
public function redirects_include_query_params()
{
- $testModel = TestModel::create(['name' => 'Dummy model']);
-
- $response = $this->from('/source')->put(route('test-models.update', ['testModel' => $testModel, 'frame' => 'lorem']), [], [
- 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
- ]);
-
- $response->assertSee('show edit form (frame=lorem)');
- $response->assertStatus(422);
- }
-
- public function usesTestModelUpdateRouteWithoutEdit()
- {
- Route::put('/test-models/{testModel}', function (TestModel $model) {
- request()->validate(['name' => 'required']);
- })->name('test-models.update')->middleware(TurboMiddleware::class);
- }
-
- /**
- * @test
- * @define-route usesTestModelUpdateRouteWithoutEdit
- */
- public function lets_it_crash_when_redirect_route_does_not_exist()
- {
- $testModel = TestModel::create(['name' => 'Dummy model']);
-
- $response = $this->from('/source')->put(route('test-models.update', $testModel), [], [
- 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
- ]);
-
- $response->assertRedirect('/source');
- $response->assertStatus(303);
- }
-
- public function usesNonResourceRoutes()
- {
- Route::name('app.')->middleware(TurboMiddleware::class)->group(function () {
- Route::get('login', function () {
- return 'login form';
- })->name('login');
+ $article = ArticleFactory::new()->create();
- Route::post('login', function () {
- request()->validate([
- 'email' => 'required',
- 'password' => 'required',
- ]);
- });
- });
+ $this->from(route('articles.index'))
+ ->put(route('articles.update', ['article' => $article, 'frame' => 'lorem']), headers: [
+ 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
+ ])
+ ->assertSee('Edit Article')
+ ->assertSee('The title field is required.')
+ ->assertSee('Showing frame: lorem.')
+ ->assertUnprocessable();
}
- /**
- * @test
- * @define-route usesNonResourceRoutes
- */
+ /** @test */
public function only_guess_route_on_resource_routes()
{
- $this->from(route('app.login'))
- ->withHeaders([
+ $this->from(route('login'))
+ ->post(route('login.store'), headers: [
'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
])
- ->post('/login')
- ->assertRedirect(route('app.login'))
+ ->assertRedirectToRoute('login')
->assertStatus(303);
}
- public function usesRoutesWhichExceptCookies()
+ /** @test */
+ public function passes_the_request_cookies_to_the_internal_request()
{
- Route::get('posts/create', function () {
- $firstRequestCookie = request()->cookie('my-cookie', 'no-cookie');
-
- $responseCookie = request()->cookie('response-cookie', 'no-cookie');
-
- return response(sprintf('Request Cookie: %s; Response Cookie: %s', $firstRequestCookie, $responseCookie));
- })->name('posts.create');
+ $article = ArticleFactory::new()->create();
- Route::post('posts', function () {
- $exception = ValidationException::withMessages([
- 'title' => ['Title cannot be blank.'],
- ]);
-
- $exception->response = redirect()->to('/')->withCookie('response-cookie', 'response-cookie-value');
-
- throw $exception;
- })->name('posts.store')->middleware(TurboMiddleware::class);
+ $this
+ ->withCookie('my-cookie', 'test-value')
+ ->delete(route('articles.destroy', $article), headers: [
+ 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
+ ])
+ ->assertSee('Delete Article')
+ ->assertSee('My cookie: test-value.')
+ ->assertSee('Response cookie: response-cookie-value.')
+ ->assertUnprocessable();
}
- /**
- * @test
- * @define-route usesRoutesWhichExceptCookies
- */
- public function passes_the_request_cookies_to_the_internal_request()
+ /** @test */
+ public function sets_turbo_tracking_request_id()
{
- $this->withHeaders([
- 'Accept' => sprintf('%s, text/html, application/xhtml+xml', Turbo::TURBO_STREAM_FORMAT),
- ])
- ->withUnencryptedCookie('my-cookie', 'test-value')
- ->post(route('posts.store'))
- ->assertSee('Request Cookie: test-value; Response Cookie: response-cookie-value')
- ->assertStatus(422);
+ $this->get('request-id')
+ ->assertJson(['turbo_request_id' => null]);
+
+ $this->withHeader('X-Turbo-Request-Id', '123')
+ ->get('request-id')
+ ->assertJson(['turbo_request_id' => '123']);
}
}
diff --git a/tests/Http/RequestMacrosTest.php b/tests/Http/RequestMacrosTest.php
index 8c9de694..f4d088ac 100644
--- a/tests/Http/RequestMacrosTest.php
+++ b/tests/Http/RequestMacrosTest.php
@@ -1,11 +1,11 @@
assertFalse($request->wantsTurboStream(), 'Expected request to not want a turbo stream response, but it did.');
+ $this->assertFalse($request->wantsTurboStreams(), 'Expected request to not want a turbo stream response, but it did.');
$request = Request::create('/hello');
$request->headers->add([
'Accept' => Turbo::TURBO_STREAM_FORMAT.', text/html, application/xhtml+xml',
]);
$this->assertTrue($request->wantsTurboStream(), 'Expected request to want a turbo stream response, but it did not.');
+ $this->assertTrue($request->wantsTurboStreams(), 'Expected request to want a turbo stream response, but it did not.');
}
/** @test */
@@ -31,4 +33,17 @@ public function was_from_turbo_native()
TurboFacade::setVisitingFromTurboNative();
$this->assertTrue($request->wasFromTurboNative());
}
+
+ /** @test */
+ public function was_from_turbo_frame()
+ {
+ $request = Request::create('/hello', server: [
+ 'HTTP_Turbo-Frame' => 'testing',
+ ]);
+
+ $this->assertTrue($request->wasFromTurboFrame());
+ $this->assertTrue($request->wasFromTurboFrame('testing'));
+ $this->assertFalse($request->wasFromTurboFrame('wrong_frame'));
+ $this->assertFalse(Request::create('/hello')->wasFromTurboFrame());
+ }
}
diff --git a/tests/Http/ResponseMacrosTest.php b/tests/Http/ResponseMacrosTest.php
index 1f4b0f7f..68031b09 100644
--- a/tests/Http/ResponseMacrosTest.php
+++ b/tests/Http/ResponseMacrosTest.php
@@ -1,44 +1,39 @@
'test']);
+ $article = ArticleFactory::new()->create();
$expected = view('turbo-laravel::turbo-stream', [
'action' => 'append',
- 'target' => 'test_models',
- 'partial' => 'test_models._test_model',
- 'partialData' => ['testModel' => $testModel],
+ 'target' => 'articles',
+ 'partial' => 'articles._article',
+ 'partialData' => ['article' => $article],
])->render();
- $resp = response()->turboStream($testModel)->toResponse(new Request);
+ $resp = response()->turboStream($article)->toResponse(new Request);
$this->assertEquals(trim($expected), trim($resp->getContent()));
$this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $resp->headers->get('Content-Type'));
@@ -47,18 +42,18 @@ public function streams_model_on_create()
/** @test */
public function streams_broadcastable_models_for_create()
{
- $testModel = BroadcastTestModel::withoutEvents(function () {
- return BroadcastTestModel::create(['name' => 'test']);
+ $comment = Comment::withoutEvents(function () {
+ return CommentFactory::new()->create();
});
$expected = view('turbo-laravel::turbo-stream', [
'action' => 'append',
- 'target' => 'broadcast_test_models',
- 'partial' => 'broadcast_test_models._broadcast_test_model',
- 'partialData' => ['broadcastTestModel' => $testModel],
+ 'target' => 'comments',
+ 'partial' => 'comments._comment',
+ 'partialData' => ['comment' => $comment],
])->render();
- $resp = response()->turboStream($testModel)->toResponse(new Request);
+ $resp = response()->turboStream($comment)->toResponse(new Request);
$this->assertEquals(trim($expected), trim($resp->getContent()));
$this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $resp->headers->get('Content-Type'));
@@ -67,16 +62,18 @@ public function streams_broadcastable_models_for_create()
/** @test */
public function streams_model_on_update()
{
- $testModel = TestModel::create(['name' => 'test'])->fresh();
+ $userProfile = Profile::withoutEvents(function () {
+ return ProfileFactory::new()->create()->fresh();
+ });
$expected = view('turbo-laravel::turbo-stream', [
'action' => 'replace',
- 'target' => dom_id($testModel),
- 'partial' => 'test_models._test_model',
- 'partialData' => ['testModel' => $testModel],
+ 'target' => dom_id($userProfile),
+ 'partial' => 'user_profiles._profile',
+ 'partialData' => ['userProfile' => $userProfile],
])->render();
- $resp = response()->turboStream($testModel)->toResponse(new Request);
+ $resp = response()->turboStream($userProfile)->toResponse(new Request);
$this->assertEquals(trim($expected), trim($resp->getContent()));
$this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $resp->headers->get('Content-Type'));
@@ -85,18 +82,18 @@ public function streams_model_on_update()
/** @test */
public function streams_broadcastable_models_for_update()
{
- $testModel = BroadcastTestModel::withoutEvents(function () {
- return BroadcastTestModel::create(['name' => 'test'])->fresh();
+ $comment = Comment::withoutEvents(function () {
+ return CommentFactory::new()->create()->fresh();
});
$expected = view('turbo-laravel::turbo-stream', [
'action' => 'replace',
- 'target' => dom_id($testModel),
- 'partial' => 'broadcast_test_models._broadcast_test_model',
- 'partialData' => ['broadcastTestModel' => $testModel],
+ 'target' => dom_id($comment),
+ 'partial' => 'comments._comment',
+ 'partialData' => ['comment' => $comment],
])->render();
- $resp = response()->turboStream($testModel)->toResponse(new Request);
+ $resp = response()->turboStream($comment)->toResponse(new Request);
$this->assertEquals(trim($expected), trim($resp->getContent()));
$this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $resp->headers->get('Content-Type'));
@@ -105,14 +102,16 @@ public function streams_broadcastable_models_for_update()
/** @test */
public function streams_model_on_delete()
{
- $testModel = tap(TestModel::create(['name' => 'test']))->delete();
+ $article = Article::withoutEvents(function () {
+ return tap(ArticleFactory::new()->create()->fresh())->delete();
+ });
$expected = view('turbo-laravel::turbo-stream', [
'action' => 'remove',
- 'target' => dom_id($testModel),
+ 'target' => dom_id($article),
])->render();
- $resp = response()->turboStream($testModel)->toResponse(new Request);
+ $resp = response()->turboStream($article)->toResponse(new Request);
$this->assertEquals(trim($expected), trim($resp->getContent()));
$this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $resp->headers->get('Content-Type'));
@@ -121,14 +120,16 @@ public function streams_model_on_delete()
/** @test */
public function streams_model_on_soft_delete()
{
- $testModelSoftDelete = tap(TestModelSoftDelete::create(['name' => 'test']))->delete();
+ $userProfile = Profile::withoutEvents(function () {
+ return tap(ProfileFactory::new()->create()->fresh())->delete();
+ });
$expected = view('turbo-laravel::turbo-stream', [
'action' => 'remove',
- 'target' => dom_id($testModelSoftDelete),
+ 'target' => dom_id($userProfile),
])->render();
- $resp = response()->turboStream($testModelSoftDelete)->toResponse(new Request);
+ $resp = response()->turboStream($userProfile)->toResponse(new Request);
$this->assertEquals(trim($expected), trim($resp->getContent()));
$this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $resp->headers->get('Content-Type'));
@@ -137,16 +138,16 @@ public function streams_model_on_soft_delete()
/** @test */
public function streams_broadcastable_models_for_deleted()
{
- $testModel = BroadcastTestModel::withoutEvents(function () {
- return tap(BroadcastTestModel::create(['name' => 'test']))->delete();
+ $comment = Comment::withoutEvents(function () {
+ return tap(CommentFactory::new()->create()->fresh())->delete();
});
$expected = view('turbo-laravel::turbo-stream', [
'action' => 'remove',
- 'target' => dom_id($testModel),
+ 'target' => dom_id($comment),
])->render();
- $resp = response()->turboStream($testModel)->toResponse(new Request);
+ $resp = response()->turboStream($comment)->toResponse(new Request);
$this->assertEquals(trim($expected), trim($resp->getContent()));
$this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $resp->headers->get('Content-Type'));
@@ -155,31 +156,14 @@ public function streams_broadcastable_models_for_deleted()
/** @test */
public function streams_custom_view()
{
- $testModel = TestModel::create(['name' => 'test']);
-
- $expected = <<hello
- html;
+ $article = ArticleFactory::new()->create();
- $resp = response()->turboStreamView(View::file(__DIR__ . '/../Stubs/views/test_models/_test_model.blade.php', [
- 'testModel' => $testModel,
- ]));
+ $expected = trim(view('articles.turbo.created', [
+ 'article' => $article,
+ ])->render());
- $this->assertEquals($expected, trim($resp->getContent()));
- $this->assertEquals(Turbo::TURBO_STREAM_FORMAT, $resp->headers->get('Content-Type'));
- }
-
- /** @test */
- public function streams_custom_view_with_alternative_syntax_passing_view_string_and_data()
- {
- $testModel = TestModel::create(['name' => 'test']);
-
- $expected = <<hello
- html;
-
- $resp = response()->turboStreamView('test_models._test_model', [
- 'testModel' => $testModel,
+ $resp = response()->turboStreamView('articles.turbo.created', [
+ 'article' => $article,
]);
$this->assertEquals($expected, trim($resp->getContent()));
@@ -200,10 +184,10 @@ public function can_configure_manually_turbo_stream_rendering()
{
$response = response()
->turboStream()
- ->target($target = 'example_target')
- ->action($action = 'replace')
- ->partial($partial = 'test_model_with_turbo_partials.turbo.created_stream', $partialData = [
- 'exampleModel' => TestModel::create(['name' => 'Test model']),
+ ->target($target = 'articles')
+ ->action($action = 'append')
+ ->partial($partial = 'articles._article', $partialData = [
+ 'article' => ArticleFactory::new()->create(),
])
->toResponse(new Request);
@@ -223,10 +207,10 @@ public function can_use_view_instead_of_partial()
{
$response = response()
->turboStream()
- ->target($target = 'example_target')
- ->action($action = 'replace')
- ->view($partial = 'test_model_with_turbo_partials.turbo.created_stream', $partialData = [
- 'exampleModel' => TestModel::create(['name' => 'Test model']),
+ ->target($target = 'articles')
+ ->action($action = 'append')
+ ->view($partial = 'articles._article', $partialData = [
+ 'article' => ArticleFactory::new()->create(),
])
->toResponse(new Request);
@@ -246,14 +230,14 @@ public function append_shorthand_for_response_builder()
{
$response = response()
->turboStream()
- ->append($testModel = TestModel::create(['name' => 'Test model']))
+ ->append($article = ArticleFactory::new()->create())
->toResponse(new Request);
$expected = view('turbo-laravel::turbo-stream', [
'action' => 'append',
- 'target' => 'test_models',
- 'partial' => 'test_models._test_model',
- 'partialData' => ['testModel' => $testModel],
+ 'target' => 'articles',
+ 'partial' => 'articles._article',
+ 'partialData' => ['article' => $article],
])->render();
$this->assertEquals(trim($expected), trim($response->getContent()));
@@ -268,7 +252,7 @@ public function append_shorthand_passing_string()
->append('some_dom_id', 'Hello World')
->toResponse(new Request());
- $expected = <<
Hello World
@@ -286,7 +270,7 @@ public function append_shorthand_passing_html_string()
->append('some_dom_id', new HtmlString('