Skip to content

Improve Livewire guidelines #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 12, 2025
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
16 changes: 16 additions & 0 deletions .ai/livewire/2/core.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
- `wire:model` is live by default.
- **Namespace**: Components typically exist in `App\Http\Livewire`.
- **Events**: Use `emit()`, `emitTo()`, `emitSelf()` and `dispatchBrowserEvent()` for events.
- Alpine is included separately to Livewire.
- You can listen for `livewire:load` to hook into Livewire initialization, and `Livewire.onPageExpired` for when the page expires:
@verbatim
<code-snippet name="livewire:load example" lang="js">
document.addEventListener('livewire:load', function () {
Livewire.onPageExpired(() => {
alert('Your session expired');
});

Livewire.onError(status => console.error(status));
});
</code-snippet>
@endverbatim
63 changes: 29 additions & 34 deletions .ai/livewire/3/core.blade.php
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@

#### Key Changes from Livewire 2

- **Namespace**: Components now use `App\Livewire` (not `App\Http\Livewire`)
- **Events**: Use `$this->dispatch()` (not `emit` or `dispatchBrowserEvent`)
- **Layout path**: `components.layouts.app` (not `layouts.app`)
- **Deferred by default**: Use `wire:model.live` for real-time updates
- **Alpine included**: Don't manually include Alpine.js

#### Livewire Best Practices

- **Single root element** in Blade components
- **Add wire:key** in loops:

## Key Changes from Livewire 2
- These changed in Livewire 2, but may not have been updated in this project. Verify this project's setup to ensure you conform with project conventions.
- **Wire:model**: Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
- **Namespace**: Components now use `App\Livewire` (not `App\Http\Livewire`).
- **Events**: Use `$this->dispatch()` (not `emit` or `dispatchBrowserEvent`).
- **Layout path**: `components.layouts.app` (not `layouts.app`).

## New directives
- `wire:show`, `wire:transition`, `wire:cloak, `wire:offline`, `wire:target` are available for use. Use the docs to find usages.

## Alpine
- Alpine is now included with Livewire, don't manually include Alpine.js.
- Plugins built in to Alpine: persist, intersect, collapse, and focus.

## Lifecycle hooks
- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring:
@verbatim
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
```
<code-snippet name="livewire:load example" lang="js">
document.addEventListener('livewire:init', function () {
Livewire.hook('request', ({ fail }) => {
if (fail && fail.status === 419) {
alert('Your session expired');
}
});

Livewire.hook('message.failed', (message, component) => {
console.error(message);
});
});
</code-snippet>
@endverbatim

- **Use attributes** for event listeners:

```php
#[On('todo-created')]
public function refreshList()
{
// ...
}
```

- **Loading states**: Use `wire:loading` and `wire:dirty`
- **Confirmations**: Use `wire:confirm="Are you sure?"`
File renamed without changes.
43 changes: 43 additions & 0 deletions .ai/livewire/core.blade.php
Original file line number Diff line number Diff line change
@@ -1 +1,44 @@
## Livewire Core
- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests.
- Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components
- State should live on the server, with the UI reflecting it.
- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions.

## Livewire Best Practices
- Livewire components require a single root element.
- Use `wire:loading` and `wire:dirty` for delightful loading states.
- Add `wire:key` in loops:
@verbatim
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
```
@endverbatim
- Prefer lifecycle hooks like `mount()`, `updatedFoo()`) for initialization and reactive side effects:
@verbatim
<code-snippet name="Lifecycle hook examples" lang="php">
public function mount(User $user) { $this->user = $user; }
public function updatedSearch() { $this->resetPage(); }
</code-snippet>
@endverbatim

## Testing Livewire
@verbatim
<code-snippet name="Example Livewire component test" lang="php">
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->assertSee(1)
->assertStatus(200);
</code-snippet>
@endverbatim
@verbatim
<code-snippet name="Testing a Livewire component exists within a page" lang="php">
$this->get('/posts/create')
->assertSeeLivewire(CreatePost::class);
</code-snippet>
@endverbatim
1 change: 0 additions & 1 deletion .ai/tailwindcss/3/core.blade.php
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
- Always use Tailwind CSS v3, verify you're using only supported classes.
- Use the `search-docs` tool to find exactly what's supported in this project's Tailwind setup.
Empty file removed .ai/tailwindcss/4/.gitkeep
Empty file.
1 change: 0 additions & 1 deletion .ai/tailwindcss/4/core.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
- Always use Tailwind CSS v4, do not use the deprecated utilities.
- Use the `search-docs` tool to find exactly what's supported in this project's Tailwind setup.
- In Tailwind v4 you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:
@verbatim
<code-snippet name="Tailwind v4 import tailwind diff" lang="diff"
Expand Down
1 change: 1 addition & 0 deletions .ai/tailwindcss/core.blade.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own.
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..)
- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically
- Use the `search-docs` tool to find exactly what's supported in this project's Tailwind setup.

## Spacing
- Use gap utilities for spacing, don't use margins
Expand Down
8 changes: 3 additions & 5 deletions .ai/volt/core.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. There is documentation available for it.
- Volt is an elegantly crafted **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to coexist in the same file
- Make new Volt components using `php artisan make:volt [name] [--test] [--pest]`
- Volt is a **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to coexist in the same file
- **Single-File Components**: Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@volt` directive.
- You must check existing Volt components to find out if they're functional or class based. If you can't detect that, ask the user which they prefer before writing a Volt component.

Expand Down Expand Up @@ -107,11 +108,10 @@ public function increment()
$delete = fn(Product $product) => $product->delete();
?>

<!-- UI here -->
<!-- HTML here -->
@endvolt
</code-snippet>
@endverbatim

@verbatim
<code-snippet name="Real-time search with Volt" lang="php">
<flux:input
Expand All @@ -120,8 +120,6 @@ public function increment()
/>
</code-snippet>
@endverbatim


@verbatim
<code-snippet name="Loading states with Volt" lang="php">
<flux:button wire:click="save" wire:loading.attr="disabled">
Expand Down
16 changes: 11 additions & 5 deletions src/Mcp/Tools/SearchDocs.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ public function __construct(protected Roster $roster)

public function description(): string
{
return 'Search for up-to-date version-specific documentation related to this project and its packages. This tool will search Laravel hosted documentation based on the packages installed and is perfect for all Laravel related packages. Laravel, inertia, pest, livewire, filament, nova, nightwatch, and more.'.PHP_EOL.'You must use this tool to search for Laravel-ecosystem docs before using other approaches.';
return 'Search for up-to-date version-specific documentation related to this project and its packages. This tool will search Laravel hosted documentation based on the packages installed and is perfect for all Laravel related packages. Laravel, inertia, pest, livewire, filament, nova, nightwatch, and more.'.PHP_EOL.'You must use this tool to search for Laravel-ecosystem docs before using other approaches. The results provided are for this project\'s package version and does not cover all versions of the package.';
}

public function schema(ToolInputSchema $schema): ToolInputSchema
{
return $schema
->string('queries')
->description('### separated list of queries to perform. Useful to pass multiple if you aren\'t sure if it is "toggle" or "switch", or "infinite scroll" or "infinite load", for example.')->required()

->raw('queries', [
'description' => 'List of queries to perform, pass multiple if you aren\'t sure if it is "toggle" or "switch", for example',
'type' => 'array',
'items' => [
'type' => 'string',
'description' => 'Search query',
],
])->required()
->raw('packages', [
'description' => 'Package names to limit searching to from application-info. Useful if you know the package(s) you need. i.e. laravel/framework, inertiajs/inertia-laravel, @inertiajs/react',
'type' => 'array',
Expand All @@ -54,7 +59,7 @@ public function handle(array $arguments): ToolResult|Generator
$packagesFilter = array_key_exists('packages', $arguments) ? $arguments['packages'] : null;

$queries = array_filter(
array_map('trim', explode('###', $arguments['queries'])),
array_map('trim', $arguments['queries']),
fn ($query) => $query !== '' && $query !== '*'
);

Expand Down Expand Up @@ -90,6 +95,7 @@ public function handle(array $arguments): ToolResult|Generator
'token_limit' => $tokenLimit,
'format' => 'markdown',
];

try {
$response = $this->client()->asJson()->post($apiUrl, $payload);

Expand Down
14 changes: 7 additions & 7 deletions tests/Feature/Mcp/Tools/SearchDocsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
]);

$tool = new SearchDocs($roster);
$result = $tool->handle(['queries' => 'authentication###testing']);
$result = $tool->handle(['queries' => ['authentication', 'testing']]);

expect($result)->toBeInstanceOf(ToolResult::class);

Expand Down Expand Up @@ -57,7 +57,7 @@
]);

$tool = new SearchDocs($roster);
$result = $tool->handle(['queries' => 'authentication']);
$result = $tool->handle(['queries' => ['authentication']]);

expect($result)->toBeInstanceOf(ToolResult::class);

Expand All @@ -77,7 +77,7 @@
]);

$tool = new SearchDocs($roster);
$result = $tool->handle(['queries' => 'test### ###*### ']);
$result = $tool->handle(['queries' => ['test', ' ', '*', ' ']]);

expect($result)->toBeInstanceOf(ToolResult::class);

Expand Down Expand Up @@ -106,7 +106,7 @@
]);

$tool = new SearchDocs($roster);
$result = $tool->handle(['queries' => 'test']);
$result = $tool->handle(['queries' => ['test']]);

expect($result)->toBeInstanceOf(ToolResult::class);

Expand All @@ -129,7 +129,7 @@
]);

$tool = new SearchDocs($roster);
$result = $tool->handle(['queries' => 'nonexistent']);
$result = $tool->handle(['queries' => ['nonexistent']]);

expect($result)->toBeInstanceOf(ToolResult::class);

Expand All @@ -149,7 +149,7 @@
]);

$tool = new SearchDocs($roster);
$result = $tool->handle(['queries' => 'test', 'token_limit' => 5000]);
$result = $tool->handle(['queries' => ['test'], 'token_limit' => 5000]);

expect($result)->toBeInstanceOf(ToolResult::class);

Expand All @@ -169,7 +169,7 @@
]);

$tool = new SearchDocs($roster);
$result = $tool->handle(['queries' => 'test', 'token_limit' => 2000000]);
$result = $tool->handle(['queries' => ['test'], 'token_limit' => 2000000]);

expect($result)->toBeInstanceOf(ToolResult::class);

Expand Down