Skip to content

Commit bb549c7

Browse files
authored
Merge pull request #19 from tonysm/new-http-turbo-stream-response-api
New Turbo Stream HTTP Response API
2 parents f1c8522 + ddbd60e commit bb549c7

15 files changed

+484
-300
lines changed

.github/workflows/php-cs-fixer.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- name: Run PHP CS Fixer
1616
uses: docker://oskarstark/php-cs-fixer-ga
1717
with:
18-
args: --config=.php_cs.dist --allow-risky=yes
18+
args: --config=.php-cs-fixer.dist.php --allow-risky=yes
1919

2020
- name: Commit changes
2121
uses: stefanzweifel/git-auto-commit-action@v4

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.idea
22
.php_cs
33
.php_cs.cache
4+
.php-cs-fixer.cache
45
.phpunit.result.cache
56
build
67
composer.lock

.php-cs-fixer.dist.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
$finder = Symfony\Component\Finder\Finder::create()
4+
->notPath('bootstrap/*')
5+
->notPath('storage/*')
6+
->notPath('resources/view/mail/*')
7+
->in([
8+
__DIR__ . '/src',
9+
__DIR__ . '/tests',
10+
])
11+
->name('*.php')
12+
->notName('*.blade.php')
13+
->ignoreDotFiles(true)
14+
->ignoreVCS(true);
15+
16+
$config = new PhpCsFixer\Config();
17+
18+
$config->setRules([
19+
'@PSR2' => true,
20+
'array_syntax' => ['syntax' => 'short'],
21+
'ordered_imports' => ['sort_algorithm' => 'alpha'],
22+
'no_unused_imports' => true,
23+
'not_operator_with_successor_space' => true,
24+
'trailing_comma_in_multiline' => true,
25+
'phpdoc_scalar' => true,
26+
'unary_operator_spaces' => true,
27+
'binary_operator_spaces' => true,
28+
'blank_line_before_statement' => [
29+
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
30+
],
31+
'phpdoc_single_line_var_spacing' => true,
32+
'phpdoc_var_without_name' => true,
33+
'class_attributes_separation' => [
34+
'elements' => [
35+
'method' => 'one',
36+
],
37+
],
38+
'method_argument_space' => [
39+
'on_multiline' => 'ensure_fully_multiline',
40+
'keep_multiple_spaces_after_comma' => true,
41+
],
42+
'single_trait_insert_per_statement' => true,
43+
])->setFinder($finder);
44+
45+
return $config;

.php_cs.dist

Lines changed: 0 additions & 43 deletions
This file was deleted.

README.md

Lines changed: 66 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<p align="center"><img src="/art/turbo-laravel-logo.svg" alt="Logo Turbo Laravel" /></p>
1+
<p align="center" style="margin-top: 2rem; margin-bottom: 2rem;"><img src="/art/turbo-laravel-logo.svg" alt="Logo Turbo Laravel" /></p>
22

33
<p align="center">
44
<a href="https://github.com/tonysm/turbo-laravel/workflows/Tests/badge.svg">
@@ -203,9 +203,7 @@ Route::post('posts/{post}/comments', function (Post $post) {
203203
$comment = $post->comments()->create(/** params */);
204204

205205
if (request()->wantsTurboStream()) {
206-
return response()->turboStreamView('comments.turbo.created_stream', [
207-
'comment' => $comment,
208-
]);
206+
return response()->turboStream()->append($comment);
209207
}
210208

211209
return back();
@@ -214,7 +212,7 @@ Route::post('posts/{post}/comments', function (Post $post) {
214212

215213
The `request()->wantsTurboStream()` macro added to the request will check if the request accepts Turbo Stream and return `true` or `false` accordingly.
216214

217-
Here's what that `comments.turbo.created_stream.blade.php` view could look like:
215+
Here's what the HTML response will look like:
218216

219217
```blade
220218
<turbo-stream action="append" target="comments">
@@ -224,87 +222,94 @@ Here's what that `comments.turbo.created_stream.blade.php` view could look like:
224222
</turbo-stream>
225223
```
226224

225+
Most of these things were "guessed" based on the [naming conventions](#conventions) we talked about earlier. But you can override most things, like so:
226+
227+
```php
228+
return response()->turboStream($comment)->target('post_comments');
229+
```
230+
231+
The model is optional, as it's only used to figure out the defaults based on the model state. You could manually create that same response like so:
232+
233+
```php
234+
return response()->turboStream()
235+
->target('comments')
236+
->action('append')
237+
->view('comments._comment', ['comment' => $comment]);
238+
```
239+
227240
There are 5 _actions_ in Turbo Streams. They are:
228241

229242
* `append` & `prepend`: to add the elements in the target element after the existing contents or before, respectively
230243
* `replace`: will replace the existing element entirely with the contents of the `template` tag in the Turbo Stream
231244
* `update`: will keep the target and only replace the contents of it with the contents of the `template` tag in the Turbo Stream
232-
* `remove`: will remove the element. This one doesn't need a `<template>` tag.
245+
* `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.
246+
247+
Which means you will find shorthand methods for them all, like:
248+
249+
```php
250+
response()->turboStream()->append($comment);
251+
response()->turboStream()->prepend($comment);
252+
response()->turboStream()->replace($comment);
253+
response()->turboStream()->update($comment);
254+
response()->turboStream()->remove($comment);
255+
```
233256

234257
You can read more about Turbo Streams in the [Turbo Handbook](https://turbo.hotwire.dev/handbook/streams).
235258

236-
If you notice, all we're doing in the `comments.turbo.created_stream.blade.php` view is wrapping the comment's partial with a Turbo Stream tag. You can alternatically delegate the generation of the Turbo Stream tag to a `response()->turboStream()` macro, which will essentially do the same thing, but in this case you wouldn't need that `comments.turbo.created_stream.blade.php` anymore:
259+
These shorthand methods return a pending object for the response which you can chain and override everything you want on it:
237260

238261
```php
239-
Route::post('posts/{post}/comments', function (Post $post) {
240-
$comment = $post->comments()->create(/** params */);
262+
return response()->turboStream()
263+
->append($comment)
264+
->view('comments._comment_card', ['comment' => $comment]);
265+
```
241266

242-
if (request()->wantsTurboStream()) {
243-
return response()->turboStream($comment);
244-
}
267+
As mentioned earlier, passing a model to the `response()->turboStream()` macro will pre-fill the pending response object with some defaults based on the model's state.
245268

246-
return back();
247-
});
248-
```
269+
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 of the macro), 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:
249270

250-
Again, this will detect that your model was recently created and generate an `append` Turbo Stream to a `comments` target (using the plural name of your model's basename for that) and render the model's partial inside a `template` tag, similarly to what we were doing manually.
271+
```php
272+
return response()->turboStream($comment, 'append');
273+
```
251274

252-
The `response()->turboStream()` macro will generate a _replace_ Turbo Stream action targeting your model's DOM ID when you are only updating the model. In the same way, if you have deleted the model, it will generate a _remove_ Turbo Stream also using the model's DOM ID as target.
275+
<a name="custom-turbo-stream-views"></a>
276+
### Custom Turbo Stream Views
253277

254-
If you're not using the model partial convention, you may stick with the `response()->turboStreamView()` version instead and specify your own Turbo Stream views. See the [conventions section](#ceventions) to read more about this.
278+
If you're not using the model partial [convention](#conventions) or if you have some more complex Turbo Stream constructs, you may use the `response()->turboStreamView()` version instead and specify your own Turbo Stream views.
255279

256-
You may override the partial name by implementing a `hotwirePartialName` method in your model. You may also have more control over the data that is passed down to the partial by implementing the `hotwirePartialData` method, like so:
280+
This is what it looks like:
257281

258282
```php
259-
class Comment extends Model
260-
{
261-
public function hotwirePartialName()
262-
{
263-
return 'my.non.conventional.partial.name';
264-
}
265-
266-
public function hotwirePartialData()
267-
{
268-
return [
269-
'lorem' => false,
270-
'ipsum' => true,
271-
'comment' => $this,
272-
];
273-
}
274-
}
283+
return response()->turboStreamView('comments.turbo.created_stream', [
284+
'comment' => $comment,
285+
]);
275286
```
276287

277-
You may also override the resource name used as target in the case wher you're generating a Turbo Stream for recently created models, as well as the DOM ID that will be used as targets when your model was either updated or deleted by implementing the following methods:
288+
And here's an example of a more complex custom Turbo Stream view:
278289

279-
```php
280-
class Comment extends Model
281-
{
282-
public function hotwireTargetResourcesName()
283-
{
284-
return 'admin_comments';
285-
}
290+
```blade
291+
@include('layouts.turbo.flash_stream')
286292
287-
public function hotwireTargetDomId()
288-
{
289-
return "admin_comment_{$this->id}";
290-
}
291-
}
293+
<turbo-stream target="comments" action="append">
294+
<template>
295+
@include('comments._comment', ['comment' => $comment])
296+
</template>
297+
</turbo-stream>
292298
```
293299

294-
<a name="custom-turbo-stream-views"></a>
295-
### Custom Turbo Stream Views
296-
297-
Erlier I showed you a custom Turbo Stream view before I showed you how to auto-generate Turbo Stream views for you models. The name and location of that view was no accident. When auto-generating the Turbo Stream views for your model using the `response()->turboStream()` helper function, it will first check if you have a Custom Turbo Stream View in place for this model and, if so, it will use that view instead of generating one from scratch.
298-
299-
This way, you can have more control over your Turbo Stream responses. To use custom Turbo Stream views, you may create a `turbo` folder in the model's resource views folder and name them after the model event you want to override, like so:
300+
Remember, these are Blade views, so you have the full power of Blade at your hands. In this example, we're including a shared Turbo Stream partial which could append any flash messages we may have. That `layouts.turbo.flash_stream` could look like this:
300301

301-
| Model Event | Expected View |
302-
|---|---|
303-
| `created` | `{resource}/turbo/created_stream.blade.php` |
304-
| `updated` | `{resource}/turbo/updated_stream.blade.php` |
305-
| `deleted` | `{resource}/turbo/deleted_stream.blade.php` |
302+
```blade
303+
@if (session()->has('status'))
304+
<turbo-stream target="notice" action="append">
305+
<template>
306+
@include('layouts._flash')
307+
</template>
308+
</turbo-stream>
309+
@endif
310+
```
306311

307-
**Note: these will only be used when you're using the `response()->turboStream()` macro.**
312+
I hope you can see how powerful this can be to reusing views.
308313

309314
<a name="broadcasting"></a>
310315
### Broadcasting Turbo Streams Over WebSockets With Laravel Echo
@@ -640,7 +645,6 @@ public function store()
640645

641646
You may also catch the `ValidationException` and return a non-200 response, if you want to.
642647

643-
644648
<a name="turbo-native"></a>
645649
### Turbo Native
646650

@@ -739,7 +743,7 @@ class CreatesCommentsTest extends TestCase
739743
}
740744
```
741745

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

744748
<a name="closing-notes"></a>
745749
### Closing Notes

0 commit comments

Comments
 (0)