Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions .ai/hotwire.blade.php
Original file line number Diff line number Diff line change
@@ -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): `<x-turbo::refresh-method method="morph" />`
- Configure scroll behavior in your layout: `<x-turbo::refresh-scroll scroll="preserve" />`
- Enable both morphing and scroll preservation with a single component: `<x-turbo::refreshes-with method="morph" scroll="preserve" />`
- 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
<x-turbo::frame :id="$post">
<h3>{{ $post->title }}</h3>
<p>{{ $post->content }}</p>
<a href="{{ route('posts.edit', $post) }}">Edit</a>
</x-turbo::frame>
```
@endverbatim
- Forms and links inside frames automatically target their containing frame (no configuration needed):
@verbatim
```blade
<x-turbo::frame :id="$post">
<form action="{{ route('posts.store') }}" method="POST">
@csrf
<input type="text" name="title" required>
<button type="submit">Create Post</button>
</form>
</x-turbo::frame>
```
@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
<a href="{{ route('posts.show', $post) }}" data-turbo-frame="_top">View Full Post</a>
```
@endverbatim

## Turbo Streams for Dynamic Updates
- Return Turbo Stream responses from controllers to update specific page elements without full page reload:
@verbatim
<code-snippet name="Controller returning Turbo Streams" lang="php">
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();
}
</code-snippet>
@endverbatim
- Available Turbo Stream actions for manipulating DOM elements:
@verbatim
<code-snippet name="Turbo Stream actions" lang="php">
// 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);
</code-snippet>
@endverbatim
- Broadcast Turbo Streams over WebSockets to push real-time updates to all connected users:
@verbatim
<code-snippet name="Broadcasting Turbo Streams" lang="php">
// 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();
</code-snippet>
@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
<div id="flash-messages" data-turbo-permanent>
<!-- Flash messages that persist across navigation -->
</div>
```
@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
<a href="{{ route('posts.show', $post) }}" data-turbo-preload="false">
{{ $post->title }}
</a>
```
@endverbatim

## Testing Hotwire/Turbo
@verbatim
<code-snippet name="Testing Turbo Stream responses" lang="php">
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')
))
));
}
</code-snippet>
@endverbatim
@verbatim
<code-snippet name="Testing Turbo Frame responses" lang="php">
public function test_frame_request_returns_partial_content()
{
$this->fromTurboFrame(dom_id($post))
->get(route('posts.update', $post))
->assertSee('<turbo-frame id="'.dom_id($post).'">', false)
->assertViewIs('posts.edit');
}
</code-snippet>
@endverbatim
@verbatim
<code-snippet name="Testing broadcast streams" lang="php">
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());
});
}
</code-snippet>
@endverbatim
@verbatim
<code-snippet name="Testing Hotwire Native Resume, Recede, or Refresh" lang="php">
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);
}
</code-snippet>
@endverbatim
36 changes: 18 additions & 18 deletions docs/helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

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

## Blade Components

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

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:

Expand All @@ -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 `<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).

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

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

Expand All @@ -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 `<x-turbo::refresh-method method="morph" />` Blade Component
### The Refresh Method Blade Component

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

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

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

You can also configure the scroll behavior on Turbo:

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

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

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:

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

### The `<x-turbo::exempts-page-from-cache />` 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:

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

### The `<x-turbo::exempts-page-from-preview />` 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:

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

### The `<x-turbo::page-requires-reload />` 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:

Expand All @@ -168,7 +168,7 @@ It will render the HTML `<meta>` 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:

Expand All @@ -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:

Expand All @@ -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:

Expand All @@ -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:

Expand All @@ -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.

Expand All @@ -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.
2 changes: 1 addition & 1 deletion docs/turbo-frames.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading
Loading