Skip to content

Commit 7ac6768

Browse files
authored
Merge pull request #149 from hotwired-laravel/tm/morph-broadcasts
Morph Streams
2 parents d470a80 + 86c1ee4 commit 7ac6768

19 files changed

+243
-32
lines changed

docs/helpers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ If you're rendering a Turbo Stream inside a your Blade files, you may use the `<
5454

5555
```blade
5656
<x-turbo::stream :target="$post" action="update">
57-
@include('posts._post', ['post' => $post])
57+
@include('posts.partials.post', ['post' => $post])
5858
<x-turbo::stream>
5959
```
6060

docs/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ class CreatesCommentsTest extends TestCase
247247
TurboStream::assertBroadcasted(function (PendingBroadcast $broadcast) use ($todo) {
248248
return $broadcast->target === 'comments'
249249
&& $broadcast->action === 'append'
250-
&& $broadcast->partialView === 'comments._comment'
250+
&& $broadcast->partialView === 'comments.partials.comment'
251251
&& $broadcast->partialData['comment']->is($todo->comments->first())
252252
&& count($broadcast->channels) === 1
253253
&& $broadcast->channels[0]->name === sprintf('private-%s', $todo->broadcastChannel());

docs/turbo-streams.md

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Although it's handy to pass a model instance to the `turbo_stream()` function -
6262
turbo_stream()
6363
->target('comments')
6464
->action('append')
65-
->view('comments._comment', ['comment' => $comment]);
65+
->view('comments.partials.comment', ['comment' => $comment]);
6666
```
6767

6868
There are also shorthand methods which may be used as well:
@@ -122,7 +122,7 @@ For both the `before` and `after` methods you need additional calls to specify t
122122
```php
123123
turbo_stream()
124124
->before($comment)
125-
->view('comments._flash_message', [
125+
->view('comments.partials.flash_message', [
126126
'message' => __('Comment created!'),
127127
]);
128128
```
@@ -143,6 +143,40 @@ It will build a `remove` Turbo Stream if the model was just deleted (or if it wa
143143
return turbo_stream($comment, 'append');
144144
```
145145

146+
## Turbo Stream & Morph
147+
148+
Both the `update` and `replace` Turbo Stream actions can specify a `[method="morph"]`, so the action will use DOM morphing instead of the default renderer.
149+
150+
```php
151+
turbo_stream()->replace(dom_id($post, 'comments'), view('comments.partials.comment', [
152+
'comment' => $comment,
153+
]))->morph();
154+
```
155+
156+
This would generate the following Turbo Stream HTML:
157+
158+
```html
159+
<turbo-stream action="replace" target="comments_post_123" method="morph">
160+
<template>...</template>
161+
</turbo-stream>
162+
```
163+
164+
And here's the `update` action version:
165+
166+
```php
167+
turbo_stream()->update(dom_id($post, 'comments'), view('comments.partials.comment', [
168+
'comment' => $comment,
169+
]))->morph();
170+
```
171+
172+
This would generate the following Turbo Stream HTML:
173+
174+
```html
175+
<turbo-stream action="update" target="comments_post_123" method="morph">
176+
<template>...</template>
177+
</turbo-stream>
178+
```
179+
146180
## Target Multiple Elements
147181

148182
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`:
@@ -165,7 +199,7 @@ When creating Turbo Streams using the builders, you may also specify the CSS cla
165199
turbo_stream()
166200
->targets('.comment')
167201
->action('append')
168-
->view('comments._comment', ['comment' => $comment]);
202+
->view('comments.partials.comment', ['comment' => $comment]);
169203
```
170204

171205
## Turbo Stream Macros
@@ -247,7 +281,7 @@ return turbo_stream([
247281
->append($comment)
248282
->target(dom_id($comment->post, 'comments')),
249283
turbo_stream()
250-
->update(dom_id($comment->post, 'comments_count'), view('posts._comments_count', [
284+
->update(dom_id($comment->post, 'comments_count'), view('posts.partials.comments_count', [
251285
'post' => $comment->post,
252286
])),
253287
]);
@@ -274,7 +308,7 @@ Here's an example of a more complex custom Turbo Stream view:
274308
275309
<turbo-stream target="@domid($comment->post, 'comments')" action="append">
276310
<template>
277-
@include('comments._comment', ['comment' => $comment])
311+
@include('comments.partials.comment', ['comment' => $comment])
278312
</template>
279313
</turbo-stream>
280314
```
@@ -285,7 +319,7 @@ Remember, these are Blade views, so you have the full power of Blade at your han
285319
@if (session()->has('status'))
286320
<turbo-stream target="notice" action="append">
287321
<template>
288-
@include('layouts._flash')
322+
@include('layouts.partials.flash')
289323
</template>
290324
</turbo-stream>
291325
@endif
@@ -297,7 +331,7 @@ Similar to the `<x-turbo::frame>` Blade component, there's also a `<x-turbo::str
297331
@include('layouts.turbo.flash_stream')
298332
299333
<x-turbo::stream :target="[$comment->post, 'comments']" action="append">
300-
@include('comments._comment', ['comment' => $comment])
334+
@include('comments.partials.comment', ['comment' => $comment])
301335
</x-turbo::stream>
302336
```
303337

src/Broadcasting/Limiter.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
class Limiter
66
{
7-
public function __construct(protected array $keys = [], protected int $delay = 500)
8-
{
9-
}
7+
public function __construct(protected array $keys = [], protected int $delay = 500) {}
108

119
public function clear(): void
1210
{

src/Broadcasting/PendingBroadcast.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,22 @@ public function attributes(array $attributes)
154154
return $this;
155155
}
156156

157+
public function morph(): self
158+
{
159+
return $this->method('morph');
160+
}
161+
162+
public function method(?string $method = null): self
163+
{
164+
if ($method) {
165+
return $this->attributes(array_merge($this->attributes, [
166+
'method' => $method,
167+
]));
168+
}
169+
170+
return $this->attributes(Arr::except($this->attributes, 'method'));
171+
}
172+
157173
public function rendering(Rendering $rendering)
158174
{
159175
$this->partialView = $rendering->partial;

src/Broadcasting/Rendering.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public static function forContent(View|HtmlString|string $content)
4040

4141
public static function empty(): self
4242
{
43-
return new self();
43+
return new self;
4444
}
4545

4646
public static function forModel(Model $model): self

src/Commands/TurboInstallCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,6 @@ private function existingLayoutFiles()
176176

177177
private function phpBinary()
178178
{
179-
return (new PhpExecutableFinder())->find(false) ?: 'php';
179+
return (new PhpExecutableFinder)->find(false) ?: 'php';
180180
}
181181
}

src/Http/PendingTurboStreamResponse.php

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Illuminate\Contracts\Support\Responsable;
1313
use Illuminate\Contracts\View\View;
1414
use Illuminate\Database\Eloquent\Model;
15+
use Illuminate\Support\Arr;
1516
use Illuminate\Support\HtmlString;
1617
use Illuminate\Support\Traits\Macroable;
1718

@@ -21,6 +22,8 @@ class PendingTurboStreamResponse implements Htmlable, Renderable, Responsable
2122
{
2223
use Macroable;
2324

25+
private array $defaultActions = ['append', 'prepend', 'update', 'replace', 'before', 'after', 'remove', 'refresh'];
26+
2427
private string $useAction;
2528

2629
private ?string $useTarget = null;
@@ -37,7 +40,7 @@ class PendingTurboStreamResponse implements Htmlable, Renderable, Responsable
3740

3841
public static function forModel(Model $model, ?string $action = null): self
3942
{
40-
$builder = new self();
43+
$builder = new self;
4144

4245
// We're treating soft-deleted models as they were deleted. In other words, we
4346
// will render the remove Turbo Stream. If you need to treat a soft-deleted
@@ -108,6 +111,22 @@ public function attributes(array $attributes): self
108111
return $this;
109112
}
110113

114+
public function morph(): self
115+
{
116+
return $this->method('morph');
117+
}
118+
119+
public function method(?string $method = null): self
120+
{
121+
if ($method) {
122+
return $this->attributes(array_merge($this->useCustomAttributes, [
123+
'method' => $method,
124+
]));
125+
}
126+
127+
return $this->attributes(Arr::except($this->useCustomAttributes, 'method'));
128+
}
129+
111130
public function append(Model|string $target, $content = null): self
112131
{
113132
return $this->buildAction(
@@ -267,8 +286,7 @@ private function buildActionAll(string $action, Model|string $targets, $content
267286

268287
public function broadcastTo($channel, ?callable $callback = null)
269288
{
270-
$callback = $callback ?? function () {
271-
};
289+
$callback = $callback ?? function () {};
272290

273291
return tap($this, function () use ($channel, $callback) {
274292
$callback($this->asPendingBroadcast($channel));
@@ -277,8 +295,7 @@ public function broadcastTo($channel, ?callable $callback = null)
277295

278296
public function broadcastToPrivateChannel($channel, ?callable $callback = null)
279297
{
280-
$callback = $callback ?? function () {
281-
};
298+
$callback = $callback ?? function () {};
282299

283300
return $this->broadcastTo(null, function (PendingBroadcast $broadcast) use ($channel, $callback) {
284301
$broadcast->toPrivateChannel($channel);
@@ -288,8 +305,7 @@ public function broadcastToPrivateChannel($channel, ?callable $callback = null)
288305

289306
public function broadcastToPresenceChannel($channel, ?callable $callback = null)
290307
{
291-
$callback = $callback ?? function () {
292-
};
308+
$callback = $callback ?? function () {};
293309

294310
return $this->broadcastTo(null, function (PendingBroadcast $broadcast) use ($channel, $callback) {
295311
$callback($broadcast->toPresenceChannel($channel));
@@ -327,7 +343,7 @@ private function contentAsRendering()
327343
*/
328344
public function toResponse($request)
329345
{
330-
if (! in_array($this->useAction, ['remove', 'refresh']) && ! $this->partialView && $this->inlineContent === null) {
346+
if (! in_array($this->useAction, ['remove', 'refresh']) && in_array($this->useAction, $this->defaultActions) && ! $this->partialView && $this->inlineContent === null) {
331347
throw TurboStreamResponseFailedException::missingPartial();
332348
}
333349

src/Models/Broadcasts.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ trait Broadcasts
2121

2222
public static function bootBroadcasts()
2323
{
24-
static::observe(new ModelObserver());
24+
static::observe(new ModelObserver);
2525
}
2626

2727
public static function withoutTurboStreamBroadcasts(callable $callback)

src/Models/Naming/Name.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static function forModel(object $model)
5151

5252
public static function build(string $className)
5353
{
54-
$name = new static();
54+
$name = new static;
5555

5656
$name->className = $className;
5757
$name->classNameWithoutRootNamespace = static::removeRootNamespaces($className);

src/Testing/ConvertTestResponseToTurboStreamCollection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class ConvertTestResponseToTurboStreamCollection
1111
public function __invoke(TestResponse $response): Collection
1212
{
1313
libxml_use_internal_errors(true);
14-
$document = tap(new DOMDocument())->loadHTML($response->content());
14+
$document = tap(new DOMDocument)->loadHTML($response->content());
1515
$elements = $document->getElementsByTagName('turbo-stream');
1616

1717
$streams = collect();

src/helpers.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function turbo_stream($model = null, ?string $action = null): MultiplePendingTur
4646
}
4747

4848
if ($model === null) {
49-
return new PendingTurboStreamResponse();
49+
return new PendingTurboStreamResponse;
5050
}
5151

5252
return PendingTurboStreamResponse::forModel($model, $action);

tests/Broadcasting/LimiterTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public function debounces()
1212
{
1313
$this->freezeTime();
1414

15-
$debouncer = new Limiter();
15+
$debouncer = new Limiter;
1616

1717
$this->assertFalse($debouncer->shouldLimit('my-key'));
1818
$this->assertTrue($debouncer->shouldLimit('my-key'));

tests/FunctionsTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,33 @@ public function namespaced_turbo_stream_fn()
6464
HTML),
6565
trim(turbo_stream($this->article)),
6666
);
67+
68+
$expected = trim(view('articles._article', [
69+
'article' => $this->article,
70+
])->render());
71+
72+
$this->assertEquals(
73+
trim(<<<HTML
74+
<turbo-stream target="article_{$this->article->id}" action="replace" method="morph">
75+
<template>{$expected}</template>
76+
</turbo-stream>
77+
HTML),
78+
trim(turbo_stream($this->article->fresh())->morph()),
79+
);
80+
81+
// Unsets method
82+
$expected = trim(view('articles._article', [
83+
'article' => $this->article,
84+
])->render());
85+
86+
$this->assertEquals(
87+
trim(<<<HTML
88+
<turbo-stream target="article_{$this->article->id}" action="replace">
89+
<template>{$expected}</template>
90+
</turbo-stream>
91+
HTML),
92+
trim(turbo_stream($this->article->fresh())->morph()->method()),
93+
);
6794
}
6895

6996
/** @test */
@@ -105,6 +132,33 @@ public function global_turbo_stream_fn()
105132
HTML),
106133
trim(\turbo_stream($this->article)),
107134
);
135+
136+
$expected = trim(view('articles._article', [
137+
'article' => $this->article,
138+
])->render());
139+
140+
$this->assertEquals(
141+
trim(<<<HTML
142+
<turbo-stream target="article_{$this->article->id}" action="replace" method="morph">
143+
<template>{$expected}</template>
144+
</turbo-stream>
145+
HTML),
146+
trim(\turbo_stream($this->article->fresh())->morph()),
147+
);
148+
149+
// Unsets method
150+
$expected = trim(view('articles._article', [
151+
'article' => $this->article,
152+
])->render());
153+
154+
$this->assertEquals(
155+
trim(<<<HTML
156+
<turbo-stream target="article_{$this->article->id}" action="replace">
157+
<template>{$expected}</template>
158+
</turbo-stream>
159+
HTML),
160+
trim(\turbo_stream($this->article->fresh())->morph()->method()),
161+
);
108162
}
109163

110164
/** @test */

tests/Http/ResponseMacrosTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ public function append_shorthand_passing_string()
250250
$response = response()
251251
->turboStream()
252252
->append('some_dom_id', 'Hello World')
253-
->toResponse(new Request());
253+
->toResponse(new Request);
254254

255255
$expected = <<<'HTML'
256256
<turbo-stream target="some_dom_id" action="append">
@@ -944,7 +944,7 @@ public function builds_multiple_turbo_stream_responses()
944944
response()->turboStream()->append($article)->target('append-target-id'),
945945
response()->turboStream()->prepend($article)->target('prepend-target-id'),
946946
response()->turboStream()->remove($article)->target('remove-target-id'),
947-
])->toResponse(new Request());
947+
])->toResponse(new Request);
948948

949949
$expected = collect([
950950
view('turbo-laravel::turbo-stream', [

0 commit comments

Comments
 (0)