From df58d03a4a9b677c234440769390dfd3636e91c4 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Thu, 22 Aug 2024 19:33:03 -0300 Subject: [PATCH 1/7] Adds morphing to model broadcasts --- src/Broadcasting/PendingBroadcast.php | 16 +++++++ tests/Models/BroadcastsModelTest.php | 63 +++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/Broadcasting/PendingBroadcast.php b/src/Broadcasting/PendingBroadcast.php index 0da7d75..48e9f03 100644 --- a/src/Broadcasting/PendingBroadcast.php +++ b/src/Broadcasting/PendingBroadcast.php @@ -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; diff --git a/tests/Models/BroadcastsModelTest.php b/tests/Models/BroadcastsModelTest.php index 7c8910d..c54a7f4 100644 --- a/tests/Models/BroadcastsModelTest.php +++ b/tests/Models/BroadcastsModelTest.php @@ -767,4 +767,67 @@ public function broadcasts_with_extra_attributes_to_turbo_stream_with_rendering( return true; }); } + + /** @test */ + public function broadcasts_replace_morph() + { + $article = ArticleFactory::new()->create(); + + $article->broadcastReplace()->morph(); + + TurboStream::assertBroadcasted(function (PendingBroadcast $broadcast) use ($article) { + $this->assertCount(1, $broadcast->channels); + $this->assertEquals(sprintf('private-%s', $article->broadcastChannel()), $broadcast->channels[0]->name); + $this->assertEquals("article_{$article->id}", $broadcast->target); + $this->assertEquals('replace', $broadcast->action); + $this->assertEquals('articles._article', $broadcast->partialView); + $this->assertEquals(['article' => $article], $broadcast->partialData); + $this->assertNull($broadcast->targets); + $this->assertEquals(['method' => 'morph'], $broadcast->attributes); + + return true; + }); + } + + /** @test */ + public function broadcasts_update_morph() + { + $article = ArticleFactory::new()->create(); + + $article->broadcastUpdate()->morph(); + + TurboStream::assertBroadcasted(function (PendingBroadcast $broadcast) use ($article) { + $this->assertCount(1, $broadcast->channels); + $this->assertEquals(sprintf('private-%s', $article->broadcastChannel()), $broadcast->channels[0]->name); + $this->assertEquals("article_{$article->id}", $broadcast->target); + $this->assertEquals('update', $broadcast->action); + $this->assertEquals('articles._article', $broadcast->partialView); + $this->assertEquals(['article' => $article], $broadcast->partialData); + $this->assertNull($broadcast->targets); + $this->assertEquals(['method' => 'morph'], $broadcast->attributes); + + return true; + }); + } + + /** @test */ + public function unsets_method_when_overriding_with_null() + { + $article = ArticleFactory::new()->create(); + + $article->broadcastUpdate()->morph()->method(); + + TurboStream::assertBroadcasted(function (PendingBroadcast $broadcast) use ($article) { + $this->assertCount(1, $broadcast->channels); + $this->assertEquals(sprintf('private-%s', $article->broadcastChannel()), $broadcast->channels[0]->name); + $this->assertEquals("article_{$article->id}", $broadcast->target); + $this->assertEquals('update', $broadcast->action); + $this->assertEquals('articles._article', $broadcast->partialView); + $this->assertEquals(['article' => $article], $broadcast->partialData); + $this->assertNull($broadcast->targets); + $this->assertEquals([], $broadcast->attributes); + + return true; + }); + } } From 41cd3522560095b9620e1f002e09dc075346d942 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Thu, 22 Aug 2024 19:43:39 -0300 Subject: [PATCH 2/7] Adds morph method helper to pending turbo stream responses --- src/Http/PendingTurboStreamResponse.php | 28 +++++++++---- tests/FunctionsTest.php | 54 +++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/Http/PendingTurboStreamResponse.php b/src/Http/PendingTurboStreamResponse.php index a51cc1e..5a0555c 100644 --- a/src/Http/PendingTurboStreamResponse.php +++ b/src/Http/PendingTurboStreamResponse.php @@ -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; @@ -37,7 +38,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 @@ -108,6 +109,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( @@ -267,8 +284,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)); @@ -277,8 +293,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); @@ -288,8 +303,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)); diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 2cacbc1..c973ff0 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -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), + trim(turbo_stream($this->article->fresh())->morph()), + ); + + // Unsets method + $expected = trim(view('articles._article', [ + 'article' => $this->article, + ])->render()); + + $this->assertEquals( + trim(<< + + + HTML), + trim(turbo_stream($this->article->fresh())->morph()->method()), + ); } /** @test */ @@ -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), + trim(\turbo_stream($this->article->fresh())->morph()), + ); + + // Unsets method + $expected = trim(view('articles._article', [ + 'article' => $this->article, + ])->render()); + + $this->assertEquals( + trim(<< + + + HTML), + trim(\turbo_stream($this->article->fresh())->morph()->method()), + ); } /** @test */ From c6856807d50ce17dddf0a351e55d0740c74e43f0 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Thu, 22 Aug 2024 19:49:35 -0300 Subject: [PATCH 3/7] Test morphing with turbo stream component --- tests/Views/ComponentsTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Views/ComponentsTest.php b/tests/Views/ComponentsTest.php index 2403bdf..7b3b402 100644 --- a/tests/Views/ComponentsTest.php +++ b/tests/Views/ComponentsTest.php @@ -115,6 +115,14 @@ public function streams() ->assertSee('', false) ->assertSee('', false) ->assertDontSee('target='); + + // Morphs... + $this->blade(<<<'BLADE' + +

Hello, World

+
+ BLADE) + ->assertSee('method="morph"', false); } /** @test */ From 8b7f8c73c5758269af0958e2e75936ba4aefc6e4 Mon Sep 17 00:00:00 2001 From: tonysm Date: Sun, 25 Aug 2024 22:24:24 +0000 Subject: [PATCH 4/7] Fix styling --- src/Broadcasting/Limiter.php | 4 +--- src/Broadcasting/Rendering.php | 2 +- src/Commands/TurboInstallCommand.php | 2 +- src/Models/Broadcasts.php | 2 +- src/Models/Naming/Name.php | 2 +- src/Testing/ConvertTestResponseToTurboStreamCollection.php | 2 +- src/helpers.php | 2 +- tests/Broadcasting/LimiterTest.php | 2 +- tests/Http/ResponseMacrosTest.php | 4 ++-- tests/Views/ViewHelpersTest.php | 6 +++--- 10 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Broadcasting/Limiter.php b/src/Broadcasting/Limiter.php index 21899c8..4c9f165 100644 --- a/src/Broadcasting/Limiter.php +++ b/src/Broadcasting/Limiter.php @@ -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 { diff --git a/src/Broadcasting/Rendering.php b/src/Broadcasting/Rendering.php index 691e8ec..c8d948c 100644 --- a/src/Broadcasting/Rendering.php +++ b/src/Broadcasting/Rendering.php @@ -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 diff --git a/src/Commands/TurboInstallCommand.php b/src/Commands/TurboInstallCommand.php index 8b89672..123450e 100644 --- a/src/Commands/TurboInstallCommand.php +++ b/src/Commands/TurboInstallCommand.php @@ -176,6 +176,6 @@ private function existingLayoutFiles() private function phpBinary() { - return (new PhpExecutableFinder())->find(false) ?: 'php'; + return (new PhpExecutableFinder)->find(false) ?: 'php'; } } diff --git a/src/Models/Broadcasts.php b/src/Models/Broadcasts.php index 3c422a7..e523680 100644 --- a/src/Models/Broadcasts.php +++ b/src/Models/Broadcasts.php @@ -21,7 +21,7 @@ trait Broadcasts public static function bootBroadcasts() { - static::observe(new ModelObserver()); + static::observe(new ModelObserver); } public static function withoutTurboStreamBroadcasts(callable $callback) diff --git a/src/Models/Naming/Name.php b/src/Models/Naming/Name.php index 565f33b..ced74a7 100644 --- a/src/Models/Naming/Name.php +++ b/src/Models/Naming/Name.php @@ -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); diff --git a/src/Testing/ConvertTestResponseToTurboStreamCollection.php b/src/Testing/ConvertTestResponseToTurboStreamCollection.php index fd333a8..cd8711c 100644 --- a/src/Testing/ConvertTestResponseToTurboStreamCollection.php +++ b/src/Testing/ConvertTestResponseToTurboStreamCollection.php @@ -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(); diff --git a/src/helpers.php b/src/helpers.php index 2470354..054aef7 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -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); diff --git a/tests/Broadcasting/LimiterTest.php b/tests/Broadcasting/LimiterTest.php index 62323e0..6a67b47 100644 --- a/tests/Broadcasting/LimiterTest.php +++ b/tests/Broadcasting/LimiterTest.php @@ -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')); diff --git a/tests/Http/ResponseMacrosTest.php b/tests/Http/ResponseMacrosTest.php index 7263442..7219b6a 100644 --- a/tests/Http/ResponseMacrosTest.php +++ b/tests/Http/ResponseMacrosTest.php @@ -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' @@ -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', [ diff --git a/tests/Views/ViewHelpersTest.php b/tests/Views/ViewHelpersTest.php index a515f57..d92c8b2 100644 --- a/tests/Views/ViewHelpersTest.php +++ b/tests/Views/ViewHelpersTest.php @@ -56,7 +56,7 @@ public function renders_dom_id() $renderedDomId = Blade::render('
', ['article' => $article]); $renderedDomIdWithPrefix = Blade::render('
', ['article' => $article]); - $rendersDomIdOfNewModel = Blade::render('
', ['article' => new Article()]); + $rendersDomIdOfNewModel = Blade::render('
', ['article' => new Article]); $this->assertEquals('
', trim($renderedDomId)); $this->assertEquals('
', trim($renderedDomIdWithPrefix)); @@ -80,7 +80,7 @@ public function renders_dom_class() $renderedDomClass = Blade::render('
', ['article' => $article]); $renderedDomClassWithPrefix = Blade::render('
', ['article' => $article]); - $rendersDomClassOfNewModel = Blade::render('
', ['article' => new Article()]); + $rendersDomClassOfNewModel = Blade::render('
', ['article' => new Article]); $this->assertEquals('
', trim($renderedDomClass)); $this->assertEquals('
', trim($renderedDomClassWithPrefix)); @@ -113,7 +113,7 @@ public function generates_model_ids_for_models_in_nested_folders() $this->assertEquals('user_profile_'.$profile->id, dom_id($profile)); $this->assertEquals('posts_user_profile_'.$profile->id, dom_id($profile, 'posts')); - $this->assertEquals('create_user_profile', dom_id(new Profile())); + $this->assertEquals('create_user_profile', dom_id(new Profile)); } /** @test */ From 515850384b6bfbeebf973a9091f6ba0c5fd4550b Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sun, 25 Aug 2024 22:21:06 -0300 Subject: [PATCH 5/7] Allow returning custom turbo streams without template on responses --- src/Http/PendingTurboStreamResponse.php | 4 +++- tests/Http/TurboStreamResponseTest.php | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Http/PendingTurboStreamResponse.php b/src/Http/PendingTurboStreamResponse.php index 5a0555c..980d8aa 100644 --- a/src/Http/PendingTurboStreamResponse.php +++ b/src/Http/PendingTurboStreamResponse.php @@ -22,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; @@ -341,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(); } diff --git a/tests/Http/TurboStreamResponseTest.php b/tests/Http/TurboStreamResponseTest.php index 54aadf3..deee5b7 100644 --- a/tests/Http/TurboStreamResponseTest.php +++ b/tests/Http/TurboStreamResponseTest.php @@ -67,4 +67,26 @@ public function turbo_assert_has_turbo_stream() )) )); } + + /** @test */ + public function turbo_allows_custom_actions_with_no_view() + { + $this->assertEquals( + <<<'HTML' + + + + HTML, + (string) turbo_stream()->action('close-drawer')->target('contact-edit-drawer'), + ); + + $this->assertEquals( + <<<'HTML' + + + + HTML, + turbo_stream()->action('close-drawer')->target('contact-edit-drawer')->toResponse(request())->getContent(), + ); + } } From fcaa0e0f1504e89dbd47110fcf768144f78630a4 Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sun, 1 Sep 2024 12:42:04 -0300 Subject: [PATCH 6/7] Add docs --- docs/turbo-streams.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/turbo-streams.md b/docs/turbo-streams.md index c59e23a..bf1e771 100644 --- a/docs/turbo-streams.md +++ b/docs/turbo-streams.md @@ -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 + + + +``` + +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 + + + +``` + ## 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`: From 86c1ee4a048cf7a74103a755ee409cff1b5c82aa Mon Sep 17 00:00:00 2001 From: Tony Messias Date: Sun, 1 Sep 2024 12:47:32 -0300 Subject: [PATCH 7/7] Change examples to use subfolder partials instead of underscore --- docs/helpers.md | 2 +- docs/testing.md | 2 +- docs/turbo-streams.md | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/helpers.md b/docs/helpers.md index 2593e58..1670d55 100644 --- a/docs/helpers.md +++ b/docs/helpers.md @@ -54,7 +54,7 @@ If you're rendering a Turbo Stream inside a your Blade files, you may use the `< ```blade - @include('posts._post', ['post' => $post]) + @include('posts.partials.post', ['post' => $post]) ``` diff --git a/docs/testing.md b/docs/testing.md index cb1b07c..68991a6 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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()); diff --git a/docs/turbo-streams.md b/docs/turbo-streams.md index bf1e771..70c6b1f 100644 --- a/docs/turbo-streams.md +++ b/docs/turbo-streams.md @@ -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: @@ -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!'), ]); ``` @@ -199,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 @@ -281,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, ])), ]); @@ -308,7 +308,7 @@ Here's an example of a more complex custom Turbo Stream view: ``` @@ -319,7 +319,7 @@ Remember, these are Blade views, so you have the full power of Blade at your han @if (session()->has('status')) @endif @@ -331,7 +331,7 @@ Similar to the `` Blade component, there's also a ` - @include('comments._comment', ['comment' => $comment]) + @include('comments.partials.comment', ['comment' => $comment]) ```