Skip to content

Commit 23b7d7f

Browse files
authored
Merge pull request #157 from hotwired-laravel/tm/boost-cmd
Boost Command
2 parents 397f41d + d781668 commit 23b7d7f

File tree

5 files changed

+273
-19
lines changed

5 files changed

+273
-19
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

docs/helpers.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Turbo Laravel has a set of Blade Directives, Components, helper functions, and r
1111

1212
## Blade Directives
1313

14-
### The `@domid()` Blade Directive
14+
### The DOM ID Blade Directive
1515

1616
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:
1717

@@ -33,7 +33,7 @@ Which will generate a `comments_post_123` DOM ID, assuming your Post model has a
3333

3434
## Blade Components
3535

36-
### The `<x-turbo::frame>` Blade Component
36+
### The Turbo Frame Blade Component
3737

3838
You may also prefer using the `<x-turbo::frame>` Blade component that ships with the package. This way, you don't need to worry about using the `@domid()` helper for your Turbo Frame:
3939

@@ -53,7 +53,7 @@ To the `:id` prop, you may pass a string, which will be used as-is as the DOM ID
5353

5454
Additionally, you may also pass along any prop that is supported by the Turbo Frame custom Element to the `<x-turbo::frame>` Blade component, like `target`, `src`, or `loading`. These are the listed attributes, but any other attribute will also be forwarded to the `<turbo-frame>` tag that will be rendered by the `<x-turbo::frame>` component. For a full list of what's possible to do with Turbo Frames, see the [documentation](https://turbo.hotwired.dev/handbook/frames).
5555

56-
### The `<x-turbo::stream>` Blade Component
56+
### The Turbo Stream Blade Component
5757

5858
If you're rendering a Turbo Stream inside a your Blade files, you may use the `<x-turbo::stream>` helper:
5959

@@ -65,7 +65,7 @@ If you're rendering a Turbo Stream inside a your Blade files, you may use the `<
6565

6666
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.
6767

68-
### The `<x-turbo::refresh-method method="morph" />` Blade Component
68+
### The Refresh Method Blade Component
6969

7070
We can configure which update method Turbo should so to update the document:
7171

@@ -86,7 +86,7 @@ The output would be:
8686
<meta name="turbo-refresh-method" content="morph">
8787
```
8888

89-
### The `<x-turbo::refresh-scroll scroll="preserve" />` Blade Component
89+
### The Refresh Scroll Behavior Blade Component
9090

9191
You can also configure the scroll behavior on Turbo:
9292

@@ -107,7 +107,7 @@ The output would be:
107107
<meta name="turbo-refresh-scroll" content="preserve">
108108
```
109109

110-
### The `<x-turbo::refreshes-with>` Blade Component
110+
### The Refresh Behaviors Blade Component
111111

112112
You may configure both the refresh method and scroll behavior using the `<x-turbo::refreshes-with />` component in your main layout's `<head>` tag or on specific pages to configure how Turbo should update the page. Here's an example:
113113

@@ -122,7 +122,7 @@ This will render two HTML `<meta>` tags:
122122
<meta name="turbo-refresh-scroll" content="preserve">
123123
```
124124

125-
### The `<x-turbo::exempts-page-from-cache />` Blade Component
125+
### The Page Cache Exemption Blade Component
126126

127127
This component may be added to any page you don't want Turbo to keep a cache in the page cache. Example:
128128

@@ -136,7 +136,7 @@ It will render the HTML `<meta>` tag:
136136
<meta name="turbo-cache-control" content="no-cache">
137137
```
138138

139-
### The `<x-turbo::exempts-page-from-preview />` Blade Component
139+
### The Page Preview Exemption Blade Component
140140

141141
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:
142142

@@ -150,7 +150,7 @@ It will render the HTML `<meta>` tag:
150150
<meta name="turbo-cache-control" content="no-preview">
151151
```
152152

153-
### The `<x-turbo::page-requires-reload />` Blade Component
153+
### The Page Reload Blade Component
154154

155155
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:
156156

@@ -168,7 +168,7 @@ It will render the HTML `<meta>` tag:
168168

169169
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).
170170

171-
### The `dom_id()`
171+
### The DOM ID Helper Function
172172

173173
The mentioned namespaced `dom_id()` helper function may also be used from anywhere in your application, like so:
174174

@@ -182,7 +182,7 @@ When a new instance of a model is passed to any of these DOM ID helpers, since i
182182

183183
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).
184184

185-
### The `dom_class()`
185+
### The DOM CSS Class Helper Function
186186

187187
The `dom_class()` helper function may be used from anywhere in your application, like so:
188188

@@ -202,7 +202,7 @@ dom_class($comment, 'reactions_list');
202202

203203
This will generate a DOM class of `reactions_list_comment`.
204204

205-
### The `turbo_stream()`
205+
### The Turbo Stream Helper Function
206206

207207
You may generate Turbo Streams using the `Response::turboStream()` macro, but you may also do so using the `turbo_stream()` helper function:
208208

@@ -214,7 +214,7 @@ turbo_stream()->append($comment);
214214

215215
Both the `Response::turboStream()` and the `turbo_stream()` function work the same way. The `turbo_stream()` function may be easier to use.
216216

217-
### The `turbo_stream_view()`
217+
### The Turbo Stream View Helper Function
218218

219219
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:
220220

@@ -228,13 +228,13 @@ return turbo_stream_view('comments.turbo.created', [
228228

229229
## Request & Response Macros
230230

231-
### The `request()->wantsTurboStream()` macro
231+
### Detect If Request Accepts Turbo Streams
232232

233233
The `request()->wantsTurboStream()` macro added to the request class will check if the request accepts Turbo Stream and return `true` or `false` accordingly.
234234

235235
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.
236236

237-
### The `request()->wasFromTurboFrame()` macro
237+
### Detect If Request Was Made From Turbo Frame
238238

239239
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.
240240

@@ -246,16 +246,16 @@ if (request()->wasFromTurboFrame(dom_id($post, 'create_comment'))) {
246246
}
247247
```
248248

249-
### The `request()->wasFromHotwireNative()` macro
249+
### Detect If Request Was Made From Hotwire Native Client
250250

251251
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.
252252

253253
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.
254254

255-
### The `response()->turboStream()` macro
255+
### Turbo Stream Response Macro
256256

257257
The `response()->turboStream()` macro works similarly to the `turbo_stream()` function above. It was only added to the response for convenience.
258258

259-
### The `response()->turboStreamView()` macro
259+
### The Turbo Stream View Response Macro
260260

261261
The `response()->turboStreamView()` macro works similarly to the `turbo_stream_view()` function above. It was only added to the response for convenience.

docs/turbo-frames.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Any other attribute passed to the Blade Component will get forwarded to the unde
3939

4040
This will work for any other attribute you want to forward to the underlying component.
4141

42-
## The `request()->wasFromTurboFrame()` Macro
42+
## Detecting Turbo Frames Requests
4343

4444
You may want to detect if a request came from a Turbo Frame in the backend. You may use the `wasFromTurboFrame()` method for that:
4545

0 commit comments

Comments
 (0)