diff --git a/README.md b/README.md index fca110ef..a20f778d 100644 --- a/README.md +++ b/README.md @@ -163,12 +163,12 @@ 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 conetnt of a lazy-loading Turbo Frame tag can be used to indicate "loading states", such as: ```blade - +

Loading...

``` -Turbo will automatically fire a GET AJAX request as soon as a lazy-loading Turbo Frame enters the DOM and replace its content with a matching Turbo Frame in the response. +Turbo will automatically dispatch a GET AJAX request as soon as a lazy-loading Turbo Frame enters the DOM and replace its content with a matching Turbo Frame in the response. You may also trigger a Turbo Frame with forms and links that are _outside_ of such frames by pointing to them like so: @@ -185,17 +185,17 @@ You could also "hide" this link and trigger a "click" event with JavaScript prog So far, all vanilla Hotwire and Turbo. -### Blade Directives and Helper Functions +### Blade Components, Directives, and Helper Functions Since Turbo rely 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 `comment_123`. You may also give it a _content_ that will prefix your DOM ID, such as: +This will generate a DOM ID string using your model's basename and its ID, such as `comment_123`. You may also give it a prefix that will added to the DOM ID, such as: ```blade (99) @@ -203,7 +203,17 @@ This will generate a DOM ID string using your model's basename and its ID, such Which will generate a `comments_count_post_123` DOM ID. -The package also ships with a namespaced `dom_id()` helper function so you can use it outside of your own views: +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 +(99) +``` + +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. + +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 you any other attribute will also be forwarded to the `` tag that will be rendered using 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 mentioned namespaced `dom_id()` helper function may also be used from anywhere in your application, like so: ```php use function Tonysm\TurboLaravel\dom_id; @@ -218,13 +228,13 @@ These helpers strip out the model's FQCN (see [config/turbo-laravel.php](config/ ### Turbo Streams -As mentioned earlier, out of everything Turbo provides, it's Turbo Streams that benefit the most from a back-end integration. +As mentioned earlier, out of everything Turbo provides, it's Turbo Streams that benefits the most from a back-end 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 replace the entire page when a form is submitted or a link is clicked. +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 you page at the same time. For instance, after a form submission to create a comment, you may want to append the comment to the comment's list and also update the comment's count in the page. You may achieve that with Turbo Streams. +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 comment's list and also update the comment's count in the page. You may achieve that with Turbo Streams. -Any non-GET form submission will get annotated by Turbo with a `Content-Type: text/vnd.turbo-stream.html` header (besides the other normal Content Types). This will indicate your back-end that you can return a Turbo Stream response for that form submission if you want to. +Form submissions will get annotated by Turbo with a `Content-Type: text/vnd.turbo-stream.html` header (besides the other normal Content Types). This will indicate to your back-end that you can return a Turbo Stream response for that form submission if you want to. Here's an example of a route handler detecting and returning a Turbo Stream response to a form submission: @@ -244,10 +254,12 @@ The `request()->wantsTurboStream()` macro added to the request will check if the Here's what the HTML response will look like: -```blade - +```html + ``` @@ -258,7 +270,7 @@ Most of these things were "guessed" based on the [naming conventions](#conventio return response()->turboStream($comment)->target('post_comments'); ``` -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: +Although it's handy to pass the model instance to the `turboStream()` response macro - 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, which can be achieved like so: ```php return response()->turboStream() @@ -289,7 +301,7 @@ response()->turboStream()->remove($comment); You can read more about Turbo Streams in the [Turbo Handbook](https://turbo.hotwired.dev/handbook/streams). -These shorthand methods return a pending object for the response which you can chain and override everything you want on it: +These shorthand methods return a pending object for the response which you can chain and override everything you want before it's rendered: ```php return response()->turboStream() @@ -312,19 +324,22 @@ You may combine multiple Turbo Stream responses in a single one like so: ```php return response()->turboStream([ - response()->turboStream()->append($commend), - response()->turboStream()->remove($commend)->target('remove-target-id'), + response()->turboStream() + ->append($comment) + ->target(dom_id($comment->post, 'comments')), + response()->turboStream() + ->action('update') + ->target(dom_id($comment->post, 'comments_count')) + ->view('posts._comments_count', ['post' => $comment->post]), ]); ``` -Although this is an 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, use [Custom Turbo Stream Views](#custom-turbo-stream-views). ### Custom Turbo Stream Views -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. - -This is what it looks like: +If you're not using the model partial [convention](#conventions) or if you have some more complex Turbo Stream constructs to build, you may use the `response()->turboStreamView()` version instead and specify your own Blade view where Turbo Streams will be created. This is what that looks like: ```php return response()->turboStreamView('comments.turbo.created_stream', [ @@ -337,7 +352,7 @@ And here's an example of a more complex custom Turbo Stream view: ```blade @include('layouts.turbo.flash_stream') - + @@ -356,6 +371,16 @@ 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 quite a bit. It has the same convention of figureing out the DOM ID of the target when you're passing a model instance or an array as the `` component applied to the `target` attribute here. 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: + +```blade +@include('layouts.turbo.flash_stream') + + + @include('comments._comment', ['comment' => $comment]) + +``` + I hope you can see how powerful this can be to reusing views. @@ -571,33 +596,20 @@ You may listen to a Turbo Stream broadcast message on your pages by adding the c ```blade ``` -By default, it expects a private channel, so the tag must be used in a page for already authenticated users. You can control the channel type in the tag with a `type` attribute. +You may prefer using the convenient `` Blade component, passing the model as the `source` prop to it, something like this: ```blade - + ``` -As this convention is not built into Laravel, you can use the model's `broadcastChannel()` method: +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. ```blade - -``` - -There is also a helper blade directive that you can use to generate the channel name for your models using the same convention if you want to: - -```blade - + ``` To register the Broadcast Auth Route you may use Laravel's built-in conventions as well: diff --git a/resources/views/components/turbo-frame.blade.php b/resources/views/components/turbo-frame.blade.php new file mode 100644 index 00000000..5c876d6e --- /dev/null +++ b/resources/views/components/turbo-frame.blade.php @@ -0,0 +1,7 @@ +{{ $slot }} diff --git a/resources/views/components/turbo-stream-from.blade.php b/resources/views/components/turbo-stream-from.blade.php new file mode 100644 index 00000000..d6d420f8 --- /dev/null +++ b/resources/views/components/turbo-stream-from.blade.php @@ -0,0 +1 @@ + diff --git a/resources/views/components/turbo-stream.blade.php b/resources/views/components/turbo-stream.blade.php new file mode 100644 index 00000000..ea7d29b8 --- /dev/null +++ b/resources/views/components/turbo-stream.blade.php @@ -0,0 +1,4 @@ +@if ($action !== "remove")@endif diff --git a/resources/views/turbo-stream.blade.php b/resources/views/turbo-stream.blade.php index ee643720..10eb7452 100644 --- a/resources/views/turbo-stream.blade.php +++ b/resources/views/turbo-stream.blade.php @@ -1,7 +1,5 @@ - + @if ($partial ?? false) - @endif - + diff --git a/src/TurboServiceProvider.php b/src/TurboServiceProvider.php index 398b8805..60dcbfc0 100644 --- a/src/TurboServiceProvider.php +++ b/src/TurboServiceProvider.php @@ -21,6 +21,7 @@ use Tonysm\TurboLaravel\Http\TurboResponseFactory; use Tonysm\TurboLaravel\Testing\AssertableTurboStream; use Tonysm\TurboLaravel\Testing\ConvertTestResponseToTurboStreamCollection; +use Tonysm\TurboLaravel\Views\Components as TurboComponents; class TurboServiceProvider extends ServiceProvider { @@ -42,6 +43,12 @@ public function boot() $this->loadViewsFrom(__DIR__.'/../resources/views', 'turbo-laravel'); + $this->loadViewComponentsAs('turbo', [ + TurboComponents\StreamFrom::class, + TurboComponents\Stream::class, + TurboComponents\Frame::class, + ]); + $this->bindBladeMacros(); $this->bindRequestAndResponseMacros(); $this->bindTestResponseMacros(); diff --git a/src/Views/Components/Frame.php b/src/Views/Components/Frame.php new file mode 100644 index 00000000..3f0b548a --- /dev/null +++ b/src/Views/Components/Frame.php @@ -0,0 +1,65 @@ +id = $id; + $this->src = $src; + $this->target = $target; + $this->loading = $loading; + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View|\Closure|string + */ + public function render() + { + return view('turbo-laravel::components.turbo-frame', [ + 'domId' => $this->domId(), + ]); + } + + private function domId(): string + { + if (is_string($this->id)) { + return $this->id; + } + + if ($this->id instanceof Model) { + return dom_id($this->id); + } + + return dom_id(...$this->id); + } +} diff --git a/src/Views/Components/Stream.php b/src/Views/Components/Stream.php new file mode 100644 index 00000000..d9da1319 --- /dev/null +++ b/src/Views/Components/Stream.php @@ -0,0 +1,54 @@ +target = $target; + $this->action = $action; + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View|\Closure|string + */ + public function render() + { + return view('turbo-laravel::components.turbo-stream', [ + 'targetValue' => $this->targetValue(), + ]); + } + + private function targetValue(): string + { + if (is_string($this->target)) { + return $this->target; + } + + if ($this->target instanceof Model) { + return dom_id($this->target); + } + + return dom_id(...$this->target); + } +} diff --git a/src/Views/Components/StreamFrom.php b/src/Views/Components/StreamFrom.php new file mode 100644 index 00000000..972fcae8 --- /dev/null +++ b/src/Views/Components/StreamFrom.php @@ -0,0 +1,38 @@ +source = $source; + $this->type = $type; + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View|\Closure|string + */ + public function render() + { + return view('turbo-laravel::components.turbo-stream-from', [ + 'channel' => $this->source instanceof HasBroadcastChannel ? $this->source->broadcastChannel() : $this->source, + ]); + } +}