Skip to content

Commit b1ba507

Browse files
committed
Adds a Laravel Boost Command
1 parent 397f41d commit b1ba507

File tree

3 files changed

+254
-0
lines changed

3 files changed

+254
-0
lines changed

.ai/hotwire.blade.php

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
## Hotwire/Turbo Core Principles
2+
- For standard application development, use Hotwire (Turbo + Stimulus)
3+
- Send HTML over the wire instead of JSON. Keep complexity on the server side.
4+
- Use Turbo Drive for smooth page transitions without full page reloads.
5+
- Decompose pages with Turbo Frames for independent sections that update separately.
6+
- Use Turbo Streams for real-time updates and dynamic content changes.
7+
- Leverage Stimulus for progressive JavaScript enhancement when Turbo isn't sufficient (if Stimulus is available)
8+
- Prefer server-side template rendering and state management over client-side frameworks.
9+
- Enable "morphing" for seamless page updates that preserve scroll position and focus.
10+
- Use data attributes for JavaScript hooks
11+
- For more complex JavaScript dependencies, use Importmap Laravel
12+
13+
## Turbo Setup & Base Helpers
14+
@verbatim
15+
- Turbo automatically handles page navigation, form submissions, and CSRF protection
16+
- Enable morphing in your layout (preserves DOM state during page updates): `<x-turbo::refresh-method method="morph" />`
17+
- Configure scroll behavior in your layout: `<x-turbo::refresh-scroll scroll="preserve" />`
18+
- Enable both morphing and scroll preservation with a single component: `<x-turbo::refreshes-with method="morph" scroll="preserve" />`
19+
- Generate unique DOM IDs from models: use function `dom_id($model, 'optional_prefix')` or Blade directive `@domid($model, 'optional_prefix')`
20+
- Generate CSS classes from models: use function `dom_class($model, 'optional_prefix')` or Blade directive `@domclass($model, 'optional_prefix')`
21+
@endverbatim
22+
23+
## Turbo Frames Best Practices
24+
- Use frames to decompose pages into independent sections that can update without full page reloads:
25+
@verbatim
26+
```blade
27+
<x-turbo::frame :id="$post">
28+
<h3>{{ $post->title }}</h3>
29+
<p>{{ $post->content }}</p>
30+
<a href="{{ route('posts.edit', $post) }}">Edit</a>
31+
</x-turbo::frame>
32+
```
33+
@endverbatim
34+
- Forms and links inside frames automatically target their containing frame (no configuration needed):
35+
@verbatim
36+
```blade
37+
<x-turbo::frame :id="$post">
38+
<form action="{{ route('posts.store') }}" method="POST">
39+
@csrf
40+
<input type="text" name="title" required>
41+
<button type="submit">Create Post</button>
42+
</form>
43+
</x-turbo::frame>
44+
```
45+
@endverbatim
46+
- Override default frame targeting with `data-turbo-frame` attribute:
47+
- Use a frame's DOM ID to target a specific frame
48+
- Use `_top` to break out of frames and navigate the full page:
49+
@verbatim
50+
```blade
51+
<a href="{{ route('posts.show', $post) }}" data-turbo-frame="_top">View Full Post</a>
52+
```
53+
@endverbatim
54+
55+
## Turbo Streams for Dynamic Updates
56+
- Return Turbo Stream responses from controllers to update specific page elements without full page reload:
57+
@verbatim
58+
<code-snippet name="Controller returning Turbo Streams" lang="php">
59+
public function store(Request $request)
60+
{
61+
$post = Post::create($request->validated());
62+
63+
if ($request->wantsTurboStream()) {
64+
return turbo_stream([
65+
turbo_stream()->append('posts', view('posts.partials.post', ['post' => $post])),
66+
turbo_stream()->update('create_post', view('posts.partials.form', ['post' => new Post()])),
67+
]);
68+
}
69+
70+
return back();
71+
}
72+
</code-snippet>
73+
@endverbatim
74+
- Available Turbo Stream actions for manipulating DOM elements:
75+
@verbatim
76+
<code-snippet name="Turbo Stream actions" lang="php">
77+
// Append content
78+
turbo_stream()->append($comment, view('comments.partials.comment', [
79+
'comment' => $comment,
80+
]));
81+
82+
// Prepend content
83+
turbo_stream()->prepend($comment, view('comments.partials.comment', [
84+
'comment' => $comment,
85+
]));
86+
87+
// Insert before
88+
turbo_stream()->before($comment, view('comments.partials.comment', [
89+
'comment' => $comment,
90+
]));
91+
92+
// Insert after
93+
turbo_stream()->after($comment, view('comments.partials.comment', [
94+
'comment' => $comment,
95+
]));
96+
97+
// Replace content (swaps the target element)
98+
turbo_stream()->replace($comment, view('comments.partials.comment', [
99+
'comment' => $comment,
100+
]));
101+
102+
// Update content (keeps the target element and only updates its contents)
103+
turbo_stream()->update($comment, view('comments.partials.comment', [
104+
'comment' => $comment,
105+
]));
106+
107+
// Removes content
108+
turbo_stream()->remove($comment);
109+
</code-snippet>
110+
@endverbatim
111+
- Broadcast Turbo Streams over WebSockets to push real-time updates to all connected users:
112+
@verbatim
113+
<code-snippet name="Broadcasting Turbo Streams" lang="php">
114+
// Add the trait to the model:
115+
use HotwiredLaravel\TurboLaravel\Models\Broadcasts;
116+
117+
class Post extends Model
118+
{
119+
use Broadcasts;
120+
}
121+
122+
// When you want to trigger the broadcasting from anywhere (including model events)...
123+
$post->broadcastAppend()->to('posts');
124+
$post->broadcastUpdate();
125+
$post->broadcastRemove();
126+
</code-snippet>
127+
@endverbatim
128+
129+
## Form Handling & Validation
130+
- Use Laravel's resource route naming conventions for automatic form re-rendering, if the matching route exists:
131+
- `*.store` action redirects to `*.create` route (shows form again with validation errors)
132+
- `*.update` action redirects to `*.edit` route (shows form again with validation errors)
133+
- `*.destroy` action redirects to `*.delete` route
134+
- Validation errors are automatically displayed when using this convention with Turbo
135+
136+
## Performance & UX Enhancements
137+
- Use `data-turbo-permanent` to preserve specific elements during Turbo navigation (prevents re-rendering):
138+
@verbatim
139+
```blade
140+
<div id="flash-messages" data-turbo-permanent>
141+
<!-- Flash messages that persist across navigation -->
142+
</div>
143+
```
144+
@endverbatim
145+
- Preloading is automatically enabled on all links. You may disable it for specific links with the `data-turbo-preload` attribute (if you need to):
146+
@verbatim
147+
```blade
148+
<a href="{{ route('posts.show', $post) }}" data-turbo-preload="false">
149+
{{ $post->title }}
150+
</a>
151+
```
152+
@endverbatim
153+
154+
## Testing Hotwire/Turbo
155+
@verbatim
156+
<code-snippet name="Testing Turbo Stream responses" lang="php">
157+
public function test_creating_post_returns_turbo_stream()
158+
{
159+
$this->turbo()
160+
->post(route('posts.store'), ['title' => 'Test Post'])
161+
->assertTurboStream(fn (AssertableTurboStream $turboStreams) => (
162+
$turboStreams->has(2)
163+
&& $turboStreams->hasTurboStream(fn ($turboStream) => (
164+
$turboStream->where('target', 'flash_messages')
165+
->where('action', 'prepend')
166+
->see('Post was successfully created!')
167+
))
168+
&& $turboStreams->hasTurboStream(fn ($turboStream) => (
169+
$turboStream->where('target', 'posts')
170+
->where('action', 'append')
171+
->see('Test Post')
172+
))
173+
));
174+
}
175+
</code-snippet>
176+
@endverbatim
177+
@verbatim
178+
<code-snippet name="Testing Turbo Frame responses" lang="php">
179+
public function test_frame_request_returns_partial_content()
180+
{
181+
$this->fromTurboFrame(dom_id($post))
182+
->get(route('posts.update', $post))
183+
->assertSee('<turbo-frame id="'.dom_id($post).'">', false)
184+
->assertViewIs('posts.edit');
185+
}
186+
</code-snippet>
187+
@endverbatim
188+
@verbatim
189+
<code-snippet name="Testing broadcast streams" lang="php">
190+
use HotwiredLaravel\TurboLaravel\Facades\TurboStream;
191+
use HotwiredLaravel\TurboLaravel\Broadcasting\PendingBroadcast;
192+
193+
public function test_post_creation_broadcasts_stream()
194+
{
195+
TurboStream::fake();
196+
197+
$post = Post::create(['title' => 'Test Post']);
198+
199+
TurboStream::assertBroadcasted(function (PendingBroadcast $broadcast) use ($post) {
200+
return $broadcast->target === 'posts'
201+
&& $broadcast->action === 'append'
202+
&& $broadcast->partialView === 'posts.partials.post'
203+
&& $broadcast->partialData['post']->is($post)
204+
&& count($broadcast->channels) === 1
205+
&& $broadcast->channels[0]->name === sprintf('private-%s', $post->broadcastChannel());
206+
});
207+
}
208+
</code-snippet>
209+
@endverbatim
210+
@verbatim
211+
<code-snippet name="Testing Hotwire Native Resume, Recede, or Refresh" lang="php">
212+
use HotwiredLaravel\TurboLaravel\Facades\TurboStream;
213+
use HotwiredLaravel\TurboLaravel\Broadcasting\PendingBroadcast;
214+
215+
public function creating_comments_from_native_recedes()
216+
{
217+
$post = Post::factory()->create();
218+
219+
$this->assertCount(0, $post->comments);
220+
221+
$this->hotwireNative()->post(route('posts.comments.store', $post), [
222+
'content' => 'Hello World',
223+
])->assertRedirectRecede(['status' => __('Comment created.')]);
224+
225+
$this->assertCount(1, $post->refresh()->comments);
226+
$this->assertEquals('Hello World', $post->comments->first()->content);
227+
}
228+
</code-snippet>
229+
@endverbatim
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace HotwiredLaravel\TurboLaravel\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Illuminate\Support\Facades\File;
7+
8+
class PublishBoostGuidelineCommand extends Command
9+
{
10+
public $signature = 'turbo:publish-boost';
11+
12+
public $description = 'Publishes the Boost Guideline';
13+
14+
public function handle(): void
15+
{
16+
$from = dirname(__DIR__, levels: 2).DIRECTORY_SEPARATOR.'.ai'.DIRECTORY_SEPARATOR.'hotwire.blade.php';
17+
18+
File::ensureDirectoryExists(base_path(implode(DIRECTORY_SEPARATOR, ['.ai', 'guidelines'])), recursive: true);
19+
File::copy($from, base_path(implode(DIRECTORY_SEPARATOR, ['.ai', 'guidelines', 'hotwire.blade.php'])));
20+
21+
$this->info('Boost guideline was published!');
22+
}
23+
}

src/TurboServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use HotwiredLaravel\TurboLaravel\Broadcasters\Broadcaster;
66
use HotwiredLaravel\TurboLaravel\Broadcasters\LaravelBroadcaster;
77
use HotwiredLaravel\TurboLaravel\Broadcasting\Limiter;
8+
use HotwiredLaravel\TurboLaravel\Commands\PublishBoostGuidelineCommand;
89
use HotwiredLaravel\TurboLaravel\Commands\TurboInstallCommand;
910
use HotwiredLaravel\TurboLaravel\Facades\Turbo as TurboFacade;
1011
use HotwiredLaravel\TurboLaravel\Http\Middleware\TurboMiddleware;
@@ -76,6 +77,7 @@ private function configurePublications(): void
7677

7778
$this->commands([
7879
TurboInstallCommand::class,
80+
PublishBoostGuidelineCommand::class,
7981
]);
8082
}
8183

0 commit comments

Comments
 (0)