diff --git a/.ai/hotwire.blade.php b/.ai/hotwire.blade.php new file mode 100644 index 0000000..91d70f2 --- /dev/null +++ b/.ai/hotwire.blade.php @@ -0,0 +1,229 @@ +## Hotwire/Turbo Core Principles +- For standard application development, use Hotwire (Turbo + Stimulus) +- Send HTML over the wire instead of JSON. Keep complexity on the server side. +- Use Turbo Drive for smooth page transitions without full page reloads. +- Decompose pages with Turbo Frames for independent sections that update separately. +- Use Turbo Streams for real-time updates and dynamic content changes. +- Leverage Stimulus for progressive JavaScript enhancement when Turbo isn't sufficient (if Stimulus is available) +- Prefer server-side template rendering and state management over client-side frameworks. +- Enable "morphing" for seamless page updates that preserve scroll position and focus. +- Use data attributes for JavaScript hooks +- For more complex JavaScript dependencies, use Importmap Laravel + +## Turbo Setup & Base Helpers +@verbatim +- Turbo automatically handles page navigation, form submissions, and CSRF protection +- Enable morphing in your layout (preserves DOM state during page updates): `` +- Configure scroll behavior in your layout: `` +- Enable both morphing and scroll preservation with a single component: `` +- Generate unique DOM IDs from models: use function `dom_id($model, 'optional_prefix')` or Blade directive `@domid($model, 'optional_prefix')` +- Generate CSS classes from models: use function `dom_class($model, 'optional_prefix')` or Blade directive `@domclass($model, 'optional_prefix')` +@endverbatim + +## Turbo Frames Best Practices +- Use frames to decompose pages into independent sections that can update without full page reloads: +@verbatim + ```blade + +

{{ $post->title }}

+

{{ $post->content }}

+ Edit +
+ ``` +@endverbatim +- Forms and links inside frames automatically target their containing frame (no configuration needed): +@verbatim + ```blade + +
+ @csrf + + +
+
+ ``` +@endverbatim +- Override default frame targeting with `data-turbo-frame` attribute: + - Use a frame's DOM ID to target a specific frame + - Use `_top` to break out of frames and navigate the full page: +@verbatim + ```blade + View Full Post + ``` +@endverbatim + +## Turbo Streams for Dynamic Updates +- Return Turbo Stream responses from controllers to update specific page elements without full page reload: +@verbatim + + public function store(Request $request) + { + $post = Post::create($request->validated()); + + if ($request->wantsTurboStream()) { + return turbo_stream([ + turbo_stream()->append('posts', view('posts.partials.post', ['post' => $post])), + turbo_stream()->update('create_post', view('posts.partials.form', ['post' => new Post()])), + ]); + } + + return back(); + } + +@endverbatim +- Available Turbo Stream actions for manipulating DOM elements: +@verbatim + + // Append content + turbo_stream()->append($comment, view('comments.partials.comment', [ + 'comment' => $comment, + ])); + + // Prepend content + turbo_stream()->prepend($comment, view('comments.partials.comment', [ + 'comment' => $comment, + ])); + + // Insert before + turbo_stream()->before($comment, view('comments.partials.comment', [ + 'comment' => $comment, + ])); + + // Insert after + turbo_stream()->after($comment, view('comments.partials.comment', [ + 'comment' => $comment, + ])); + + // Replace content (swaps the target element) + turbo_stream()->replace($comment, view('comments.partials.comment', [ + 'comment' => $comment, + ])); + + // Update content (keeps the target element and only updates its contents) + turbo_stream()->update($comment, view('comments.partials.comment', [ + 'comment' => $comment, + ])); + + // Removes content + turbo_stream()->remove($comment); + +@endverbatim +- Broadcast Turbo Streams over WebSockets to push real-time updates to all connected users: +@verbatim + + // Add the trait to the model: + use HotwiredLaravel\TurboLaravel\Models\Broadcasts; + + class Post extends Model + { + use Broadcasts; + } + + // When you want to trigger the broadcasting from anywhere (including model events)... + $post->broadcastAppend()->to('posts'); + $post->broadcastUpdate(); + $post->broadcastRemove(); + +@endverbatim + +## Form Handling & Validation +- Use Laravel's resource route naming conventions for automatic form re-rendering, if the matching route exists: + - `*.store` action redirects to `*.create` route (shows form again with validation errors) + - `*.update` action redirects to `*.edit` route (shows form again with validation errors) + - `*.destroy` action redirects to `*.delete` route +- Validation errors are automatically displayed when using this convention with Turbo + +## Performance & UX Enhancements +- Use `data-turbo-permanent` to preserve specific elements during Turbo navigation (prevents re-rendering): +@verbatim + ```blade +
+ +
+ ``` +@endverbatim +- Preloading is automatically enabled on all links. You may disable it for specific links with the `data-turbo-preload` attribute (if you need to): +@verbatim + ```blade + + {{ $post->title }} + + ``` +@endverbatim + +## Testing Hotwire/Turbo +@verbatim + + public function test_creating_post_returns_turbo_stream() + { + $this->turbo() + ->post(route('posts.store'), ['title' => 'Test Post']) + ->assertTurboStream(fn (AssertableTurboStream $turboStreams) => ( + $turboStreams->has(2) + && $turboStreams->hasTurboStream(fn ($turboStream) => ( + $turboStream->where('target', 'flash_messages') + ->where('action', 'prepend') + ->see('Post was successfully created!') + )) + && $turboStreams->hasTurboStream(fn ($turboStream) => ( + $turboStream->where('target', 'posts') + ->where('action', 'append') + ->see('Test Post') + )) + )); + } + +@endverbatim +@verbatim + + public function test_frame_request_returns_partial_content() + { + $this->fromTurboFrame(dom_id($post)) + ->get(route('posts.update', $post)) + ->assertSee('', false) + ->assertViewIs('posts.edit'); + } + +@endverbatim +@verbatim + + use HotwiredLaravel\TurboLaravel\Facades\TurboStream; + use HotwiredLaravel\TurboLaravel\Broadcasting\PendingBroadcast; + + public function test_post_creation_broadcasts_stream() + { + TurboStream::fake(); + + $post = Post::create(['title' => 'Test Post']); + + TurboStream::assertBroadcasted(function (PendingBroadcast $broadcast) use ($post) { + return $broadcast->target === 'posts' + && $broadcast->action === 'append' + && $broadcast->partialView === 'posts.partials.post' + && $broadcast->partialData['post']->is($post) + && count($broadcast->channels) === 1 + && $broadcast->channels[0]->name === sprintf('private-%s', $post->broadcastChannel()); + }); + } + +@endverbatim +@verbatim + + use HotwiredLaravel\TurboLaravel\Facades\TurboStream; + use HotwiredLaravel\TurboLaravel\Broadcasting\PendingBroadcast; + + public function creating_comments_from_native_recedes() + { + $post = Post::factory()->create(); + + $this->assertCount(0, $post->comments); + + $this->hotwireNative()->post(route('posts.comments.store', $post), [ + 'content' => 'Hello World', + ])->assertRedirectRecede(['status' => __('Comment created.')]); + + $this->assertCount(1, $post->refresh()->comments); + $this->assertEquals('Hello World', $post->comments->first()->content); + } + +@endverbatim diff --git a/docs/helpers.md b/docs/helpers.md index 937e543..afa1c50 100644 --- a/docs/helpers.md +++ b/docs/helpers.md @@ -11,7 +11,7 @@ Turbo Laravel has a set of Blade Directives, Components, helper functions, and r ## Blade Directives -### The `@domid()` Blade Directive +### The DOM ID Blade Directive Since Turbo relies 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: @@ -33,7 +33,7 @@ Which will generate a `comments_post_123` DOM ID, assuming your Post model has a ## Blade Components -### The `` Blade Component +### The Turbo Frame Blade Component 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: @@ -53,7 +53,7 @@ To the `:id` prop, you may pass a string, which will be used as-is as 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 any other attribute will also be forwarded to the `` tag that will be rendered by 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 `` Blade Component +### The Turbo Stream Blade Component If you're rendering a Turbo Stream inside a your Blade files, you may use the `` helper: @@ -65,7 +65,7 @@ If you're rendering a Turbo Stream inside a your Blade files, you may use the `< Just like in the Turbo Frames' `:id` prop, the `:target` prop of the Turbo Stream component accepts a string, a model instance, or an array to resolve the DOM ID using the `dom_id()` function. -### The `` Blade Component +### The Refresh Method Blade Component We can configure which update method Turbo should so to update the document: @@ -86,7 +86,7 @@ The output would be: ``` -### The `` Blade Component +### The Refresh Scroll Behavior Blade Component You can also configure the scroll behavior on Turbo: @@ -107,7 +107,7 @@ The output would be: ``` -### The `` Blade Component +### The Refresh Behaviors Blade Component You may configure both the refresh method and scroll behavior using the `` component in your main layout's `` tag or on specific pages to configure how Turbo should update the page. Here's an example: @@ -122,7 +122,7 @@ This will render two HTML `` tags: ``` -### The `` Blade Component +### The Page Cache Exemption Blade Component This component may be added to any page you don't want Turbo to keep a cache in the page cache. Example: @@ -136,7 +136,7 @@ It will render the HTML `` tag: ``` -### The `` Blade Component +### The Page Preview Exemption Blade Component This component may be added to any page you don't want Turbo to show as a preview on regular navigation visits. No-preview pages will only be used in restoration visits (when you use the browser's back or forward buttons, or when when moving backward in the navigation stack). Example: @@ -150,7 +150,7 @@ It will render the HTML `` tag: ``` -### The `` Blade Component +### The Page Reload Blade Component This component may be added to any page you want Turbo to reload. This will break out of Turbo Frame navigations. May be used at a login screen, for instance. Example: @@ -168,7 +168,7 @@ It will render the HTML `` tag: The package ships with a set of helper functions. These functions are all namespaced under `HotwiredLaravel\\TurboLaravel\\` but we also add them globally for convenience, so you may use them directly without the `use` statements (this is useful in contexts like Blade views, for instance). -### The `dom_id()` +### The DOM ID Helper Function The mentioned namespaced `dom_id()` helper function may also be used from anywhere in your application, like so: @@ -182,7 +182,7 @@ When a new instance of a model is passed to any of these DOM ID helpers, since i These helpers strip out the model's FQCN (see [config/turbo-laravel.php](https://github.com/hotwired-laravel/turbo-laravel/blob/main/config/turbo-laravel.php) if you use an unconventional location for your models). -### The `dom_class()` +### The DOM CSS Class Helper Function The `dom_class()` helper function may be used from anywhere in your application, like so: @@ -202,7 +202,7 @@ dom_class($comment, 'reactions_list'); This will generate a DOM class of `reactions_list_comment`. -### The `turbo_stream()` +### The Turbo Stream Helper Function You may generate Turbo Streams using the `Response::turboStream()` macro, but you may also do so using the `turbo_stream()` helper function: @@ -214,7 +214,7 @@ turbo_stream()->append($comment); Both the `Response::turboStream()` and the `turbo_stream()` function work the same way. The `turbo_stream()` function may be easier to use. -### The `turbo_stream_view()` +### The Turbo Stream View Helper Function You may combo Turbo Streams using the `turbo_stream([])` function passing an array, but you may prefer to create a separate Blade view with all the Turbo Streams, this way you may also use template extensions and everything else Blade offers: @@ -228,13 +228,13 @@ return turbo_stream_view('comments.turbo.created', [ ## Request & Response Macros -### The `request()->wantsTurboStream()` macro +### Detect If Request Accepts Turbo Streams The `request()->wantsTurboStream()` macro added to the request class will check if the request accepts Turbo Stream and return `true` or `false` accordingly. Turbo will add a `Accept: text/vnd.turbo-stream.html, ...` header to the requests. That's how we can detect if the request came from a client using Turbo. -### The `request()->wasFromTurboFrame()` macro +### Detect If Request Was Made From Turbo Frame The `request()->wasFromTurboFrame()` macro added to the request class will check if the request was made from a Turbo Frame. When used with no parameters, it returns `true` if the request has a `Turbo-Frame` header, no matter which specific Turbo Frame. @@ -246,16 +246,16 @@ if (request()->wasFromTurboFrame(dom_id($post, 'create_comment'))) { } ``` -### The `request()->wasFromHotwireNative()` macro +### Detect If Request Was Made From Hotwire Native Client The `request()->wasFromHotwireNative()` macro added to the request class will check if the request came from a Hotwire Native client and returns `true` or `false` accordingly. Hotwire Native clients are encouraged to override the `User-Agent` header in the WebViews to mention the words `Hotwire Native` on them. This is what this macro uses to detect if it came from a Hotwire Native client. -### The `response()->turboStream()` macro +### Turbo Stream Response Macro The `response()->turboStream()` macro works similarly to the `turbo_stream()` function above. It was only added to the response for convenience. -### The `response()->turboStreamView()` macro +### The Turbo Stream View Response Macro The `response()->turboStreamView()` macro works similarly to the `turbo_stream_view()` function above. It was only added to the response for convenience. diff --git a/docs/turbo-frames.md b/docs/turbo-frames.md index 78921fa..7f53cae 100644 --- a/docs/turbo-frames.md +++ b/docs/turbo-frames.md @@ -39,7 +39,7 @@ Any other attribute passed to the Blade Component will get forwarded to the unde This will work for any other attribute you want to forward to the underlying component. -## The `request()->wasFromTurboFrame()` Macro +## Detecting Turbo Frames Requests You may want to detect if a request came from a Turbo Frame in the backend. You may use the `wasFromTurboFrame()` method for that: diff --git a/src/Commands/PublishBoostGuidelineCommand.php b/src/Commands/PublishBoostGuidelineCommand.php new file mode 100644 index 0000000..67dfb49 --- /dev/null +++ b/src/Commands/PublishBoostGuidelineCommand.php @@ -0,0 +1,23 @@ +info('Boost guideline was published!'); + } +} diff --git a/src/TurboServiceProvider.php b/src/TurboServiceProvider.php index 85790ce..dd6e86e 100644 --- a/src/TurboServiceProvider.php +++ b/src/TurboServiceProvider.php @@ -5,6 +5,7 @@ use HotwiredLaravel\TurboLaravel\Broadcasters\Broadcaster; use HotwiredLaravel\TurboLaravel\Broadcasters\LaravelBroadcaster; use HotwiredLaravel\TurboLaravel\Broadcasting\Limiter; +use HotwiredLaravel\TurboLaravel\Commands\PublishBoostGuidelineCommand; use HotwiredLaravel\TurboLaravel\Commands\TurboInstallCommand; use HotwiredLaravel\TurboLaravel\Facades\Turbo as TurboFacade; use HotwiredLaravel\TurboLaravel\Http\Middleware\TurboMiddleware; @@ -76,6 +77,7 @@ private function configurePublications(): void $this->commands([ TurboInstallCommand::class, + PublishBoostGuidelineCommand::class, ]); }