Skip to content

Commit

Permalink
Merge pull request #149 from hotwired-laravel/tm/morph-broadcasts
Browse files Browse the repository at this point in the history
Morph Streams
  • Loading branch information
tonysm authored Sep 1, 2024
2 parents d470a80 + 86c1ee4 commit 7ac6768
Show file tree
Hide file tree
Showing 19 changed files with 243 additions and 32 deletions.
2 changes: 1 addition & 1 deletion docs/helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ If you're rendering a Turbo Stream inside a your Blade files, you may use the `<

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

Expand Down
2 changes: 1 addition & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ class CreatesCommentsTest extends TestCase
TurboStream::assertBroadcasted(function (PendingBroadcast $broadcast) use ($todo) {
return $broadcast->target === 'comments'
&& $broadcast->action === 'append'
&& $broadcast->partialView === 'comments._comment'
&& $broadcast->partialView === 'comments.partials.comment'
&& $broadcast->partialData['comment']->is($todo->comments->first())
&& count($broadcast->channels) === 1
&& $broadcast->channels[0]->name === sprintf('private-%s', $todo->broadcastChannel());
Expand Down
48 changes: 41 additions & 7 deletions docs/turbo-streams.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Although it's handy to pass a model instance to the `turbo_stream()` function -
turbo_stream()
->target('comments')
->action('append')
->view('comments._comment', ['comment' => $comment]);
->view('comments.partials.comment', ['comment' => $comment]);
```

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

## Turbo Stream & Morph

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.

```php
turbo_stream()->replace(dom_id($post, 'comments'), view('comments.partials.comment', [
'comment' => $comment,
]))->morph();
```

This would generate the following Turbo Stream HTML:

```html
<turbo-stream action="replace" target="comments_post_123" method="morph">
<template>...</template>
</turbo-stream>
```

And here's the `update` action version:

```php
turbo_stream()->update(dom_id($post, 'comments'), view('comments.partials.comment', [
'comment' => $comment,
]))->morph();
```

This would generate the following Turbo Stream HTML:

```html
<turbo-stream action="update" target="comments_post_123" method="morph">
<template>...</template>
</turbo-stream>
```

## Target Multiple Elements

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

## Turbo Stream Macros
Expand Down Expand Up @@ -247,7 +281,7 @@ return turbo_stream([
->append($comment)
->target(dom_id($comment->post, 'comments')),
turbo_stream()
->update(dom_id($comment->post, 'comments_count'), view('posts._comments_count', [
->update(dom_id($comment->post, 'comments_count'), view('posts.partials.comments_count', [
'post' => $comment->post,
])),
]);
Expand All @@ -274,7 +308,7 @@ Here's an example of a more complex custom Turbo Stream view:
<turbo-stream target="@domid($comment->post, 'comments')" action="append">
<template>
@include('comments._comment', ['comment' => $comment])
@include('comments.partials.comment', ['comment' => $comment])
</template>
</turbo-stream>
```
Expand All @@ -285,7 +319,7 @@ Remember, these are Blade views, so you have the full power of Blade at your han
@if (session()->has('status'))
<turbo-stream target="notice" action="append">
<template>
@include('layouts._flash')
@include('layouts.partials.flash')
</template>
</turbo-stream>
@endif
Expand All @@ -297,7 +331,7 @@ Similar to the `<x-turbo::frame>` Blade component, there's also a `<x-turbo::str
@include('layouts.turbo.flash_stream')
<x-turbo::stream :target="[$comment->post, 'comments']" action="append">
@include('comments._comment', ['comment' => $comment])
@include('comments.partials.comment', ['comment' => $comment])
</x-turbo::stream>
```

Expand Down
4 changes: 1 addition & 3 deletions src/Broadcasting/Limiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

class Limiter
{
public function __construct(protected array $keys = [], protected int $delay = 500)
{
}
public function __construct(protected array $keys = [], protected int $delay = 500) {}

public function clear(): void
{
Expand Down
16 changes: 16 additions & 0 deletions src/Broadcasting/PendingBroadcast.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,22 @@ public function attributes(array $attributes)
return $this;
}

public function morph(): self
{
return $this->method('morph');
}

public function method(?string $method = null): self
{
if ($method) {
return $this->attributes(array_merge($this->attributes, [
'method' => $method,
]));
}

return $this->attributes(Arr::except($this->attributes, 'method'));
}

public function rendering(Rendering $rendering)
{
$this->partialView = $rendering->partial;
Expand Down
2 changes: 1 addition & 1 deletion src/Broadcasting/Rendering.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static function forContent(View|HtmlString|string $content)

public static function empty(): self
{
return new self();
return new self;
}

public static function forModel(Model $model): self
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/TurboInstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,6 @@ private function existingLayoutFiles()

private function phpBinary()
{
return (new PhpExecutableFinder())->find(false) ?: 'php';
return (new PhpExecutableFinder)->find(false) ?: 'php';
}
}
32 changes: 24 additions & 8 deletions src/Http/PendingTurboStreamResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Traits\Macroable;

Expand All @@ -21,6 +22,8 @@ class PendingTurboStreamResponse implements Htmlable, Renderable, Responsable
{
use Macroable;

private array $defaultActions = ['append', 'prepend', 'update', 'replace', 'before', 'after', 'remove', 'refresh'];

private string $useAction;

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

public static function forModel(Model $model, ?string $action = null): self
{
$builder = new self();
$builder = new self;

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

public function morph(): self
{
return $this->method('morph');
}

public function method(?string $method = null): self
{
if ($method) {
return $this->attributes(array_merge($this->useCustomAttributes, [
'method' => $method,
]));
}

return $this->attributes(Arr::except($this->useCustomAttributes, 'method'));
}

public function append(Model|string $target, $content = null): self
{
return $this->buildAction(
Expand Down Expand Up @@ -267,8 +286,7 @@ private function buildActionAll(string $action, Model|string $targets, $content

public function broadcastTo($channel, ?callable $callback = null)
{
$callback = $callback ?? function () {
};
$callback = $callback ?? function () {};

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

public function broadcastToPrivateChannel($channel, ?callable $callback = null)
{
$callback = $callback ?? function () {
};
$callback = $callback ?? function () {};

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

public function broadcastToPresenceChannel($channel, ?callable $callback = null)
{
$callback = $callback ?? function () {
};
$callback = $callback ?? function () {};

return $this->broadcastTo(null, function (PendingBroadcast $broadcast) use ($channel, $callback) {
$callback($broadcast->toPresenceChannel($channel));
Expand Down Expand Up @@ -327,7 +343,7 @@ private function contentAsRendering()
*/
public function toResponse($request)
{
if (! in_array($this->useAction, ['remove', 'refresh']) && ! $this->partialView && $this->inlineContent === null) {
if (! in_array($this->useAction, ['remove', 'refresh']) && in_array($this->useAction, $this->defaultActions) && ! $this->partialView && $this->inlineContent === null) {
throw TurboStreamResponseFailedException::missingPartial();
}

Expand Down
2 changes: 1 addition & 1 deletion src/Models/Broadcasts.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ trait Broadcasts

public static function bootBroadcasts()
{
static::observe(new ModelObserver());
static::observe(new ModelObserver);
}

public static function withoutTurboStreamBroadcasts(callable $callback)
Expand Down
2 changes: 1 addition & 1 deletion src/Models/Naming/Name.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static function forModel(object $model)

public static function build(string $className)
{
$name = new static();
$name = new static;

$name->className = $className;
$name->classNameWithoutRootNamespace = static::removeRootNamespaces($className);
Expand Down
2 changes: 1 addition & 1 deletion src/Testing/ConvertTestResponseToTurboStreamCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class ConvertTestResponseToTurboStreamCollection
public function __invoke(TestResponse $response): Collection
{
libxml_use_internal_errors(true);
$document = tap(new DOMDocument())->loadHTML($response->content());
$document = tap(new DOMDocument)->loadHTML($response->content());
$elements = $document->getElementsByTagName('turbo-stream');

$streams = collect();
Expand Down
2 changes: 1 addition & 1 deletion src/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function turbo_stream($model = null, ?string $action = null): MultiplePendingTur
}

if ($model === null) {
return new PendingTurboStreamResponse();
return new PendingTurboStreamResponse;
}

return PendingTurboStreamResponse::forModel($model, $action);
Expand Down
2 changes: 1 addition & 1 deletion tests/Broadcasting/LimiterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public function debounces()
{
$this->freezeTime();

$debouncer = new Limiter();
$debouncer = new Limiter;

$this->assertFalse($debouncer->shouldLimit('my-key'));
$this->assertTrue($debouncer->shouldLimit('my-key'));
Expand Down
54 changes: 54 additions & 0 deletions tests/FunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,33 @@ public function namespaced_turbo_stream_fn()
HTML),
trim(turbo_stream($this->article)),
);

$expected = trim(view('articles._article', [
'article' => $this->article,
])->render());

$this->assertEquals(
trim(<<<HTML
<turbo-stream target="article_{$this->article->id}" action="replace" method="morph">
<template>{$expected}</template>
</turbo-stream>
HTML),
trim(turbo_stream($this->article->fresh())->morph()),
);

// Unsets method
$expected = trim(view('articles._article', [
'article' => $this->article,
])->render());

$this->assertEquals(
trim(<<<HTML
<turbo-stream target="article_{$this->article->id}" action="replace">
<template>{$expected}</template>
</turbo-stream>
HTML),
trim(turbo_stream($this->article->fresh())->morph()->method()),
);
}

/** @test */
Expand Down Expand Up @@ -105,6 +132,33 @@ public function global_turbo_stream_fn()
HTML),
trim(\turbo_stream($this->article)),
);

$expected = trim(view('articles._article', [
'article' => $this->article,
])->render());

$this->assertEquals(
trim(<<<HTML
<turbo-stream target="article_{$this->article->id}" action="replace" method="morph">
<template>{$expected}</template>
</turbo-stream>
HTML),
trim(\turbo_stream($this->article->fresh())->morph()),
);

// Unsets method
$expected = trim(view('articles._article', [
'article' => $this->article,
])->render());

$this->assertEquals(
trim(<<<HTML
<turbo-stream target="article_{$this->article->id}" action="replace">
<template>{$expected}</template>
</turbo-stream>
HTML),
trim(\turbo_stream($this->article->fresh())->morph()->method()),
);
}

/** @test */
Expand Down
4 changes: 2 additions & 2 deletions tests/Http/ResponseMacrosTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public function append_shorthand_passing_string()
$response = response()
->turboStream()
->append('some_dom_id', 'Hello World')
->toResponse(new Request());
->toResponse(new Request);

$expected = <<<'HTML'
<turbo-stream target="some_dom_id" action="append">
Expand Down Expand Up @@ -944,7 +944,7 @@ public function builds_multiple_turbo_stream_responses()
response()->turboStream()->append($article)->target('append-target-id'),
response()->turboStream()->prepend($article)->target('prepend-target-id'),
response()->turboStream()->remove($article)->target('remove-target-id'),
])->toResponse(new Request());
])->toResponse(new Request);

$expected = collect([
view('turbo-laravel::turbo-stream', [
Expand Down
Loading

0 comments on commit 7ac6768

Please sign in to comment.