name: livewire-form description: Create production-ready class-based Livewire v4 forms with validation, loading states, error handling, and Flux UI components (light mode only). Use this for data entry, user input, and CRUD operations. allowed-tools: Bash,Read,Write,Edit,Glob,Grep
Livewire v4 Form Builder
Build elegant, validated class-based Livewire v4 forms using Flux UI Pro components and Laravel best practices.
Pre-Flight Checklist
Check existing Livewire components to determine:
- Validation patterns (inline vs Form Request)
- Flux UI component usage patterns
- Common component structure
Search Flux documentation using Laravel Boost:
Use search-docs tool with queries like: ['flux input validation', 'flux select', 'flux button variants']Understand Flux UI styling rules:
- Flux components can ONLY be customized with spacing utilities (padding, margins)
- NEVER add custom colors, typography, borders, or other styling to Flux components
- Valid:
<flux:button class="mt-4 px-6"> - Invalid:
<flux:button class="text-blue-500 border-2 font-bold">
Plan with TodoWrite for complex forms
Form Creation Workflow
1. Create Livewire Component
php artisan make:livewire [Feature/FormName] --test --pest --no-interaction
2. Build Component Class (Livewire v4)
namespace App\Livewire\Features;
use App\Models\Post;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked;
use Livewire\Attributes\On;
use Livewire\Component;
class PostForm extends Component
{
// Typed public properties (Livewire v4 best practice)
public string $title = '';
public string $content = '';
public ?int $categoryId = null;
public bool $published = false;
// Use #[Locked] for properties that shouldn't be modified from frontend
#[Locked]
public ?int $postId = null;
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'content' => 'required|string',
'categoryId' => 'required|exists:categories,id',
'published' => 'boolean',
];
}
public function messages(): array
{
return [
'title.required' => 'Please enter a title',
'categoryId.required' => 'Please select a category',
];
}
// Use #[On('event')] instead of $listeners property
#[On('category-selected')]
public function setCategory(int $categoryId): void
{
$this->categoryId = $categoryId;
}
// Use #[Computed] for derived properties (cached until dependencies change)
#[Computed]
public function categories(): \Illuminate\Support\Collection
{
return Category::orderBy('name')->get();
}
public function save(): void
{
$validated = $this->validate();
Post::create([
'title' => $validated['title'],
'content' => $validated['content'],
'category_id' => $validated['categoryId'],
'published' => $validated['published'],
]);
$this->dispatch('post-created');
session()->flash('success', 'Post created successfully!');
$this->redirect(route('posts.index'));
}
public function render(): \Illuminate\Contracts\View\View
{
return view('livewire.features.post-form');
}
}
3. Build Blade View
Basic form structure:
<flux:card>
<flux:heading>Create Post</flux:heading>
<form wire:submit="save" class="space-y-6">
<!-- Text input -->
<flux:field>
<flux:label>Title</flux:label>
<flux:input wire:model.blur="title" placeholder="Enter title" required />
<flux:error name="title" />
</flux:field>
<!-- Select dropdown -->
<flux:field>
<flux:label>Category</flux:label>
<flux:select wire:model="categoryId" placeholder="Choose...">
@foreach($categories as $category)
<flux:option value="{{ $category->id }}">{{ $category->name }}</flux:option>
@endforeach
</flux:select>
<flux:error name="categoryId" />
</flux:field>
<!-- Textarea -->
<flux:field>
<flux:label>Content</flux:label>
<flux:textarea wire:model.blur="content" rows="4" />
<flux:error name="content" />
</flux:field>
<!-- Checkbox -->
<flux:field>
<flux:checkbox wire:model.boolean="published">
<flux:label>Published</flux:label>
</flux:checkbox>
</flux:field>
<!-- Submit button with loading state -->
<flux:button type="submit" variant="primary" wire:loading.attr="disabled">
<span wire:loading.remove wire:target="save">Save</span>
<span wire:loading wire:target="save">Saving...</span>
</flux:button>
</form>
</flux:card>
For complete component examples, see: references/flux-components.md
4. Wire Modifiers
Choose the right modifier for optimal UX:
| Modifier | Use Case | Update Timing |
|---|---|---|
wire:model.blur |
Standard form fields (recommended) | On blur |
wire:model.live |
Real-time updates | Every keystroke |
wire:model.live.debounce.300ms |
Search inputs | After 300ms delay |
wire:model |
Deferred updates | On form submit |
<!-- Standard form field (recommended) -->
<flux:input wire:model.blur="title" />
<!-- Real-time search -->
<flux:input wire:model.live.debounce.300ms="search" />
5. Validation
Inline validation (simple forms):
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
];
}
public function messages(): array
{
return [
'title.required' => 'Please enter a title',
];
}
Real-time validation:
public function updated($propertyName): void
{
$this->validateOnly($propertyName);
}
// Or validate specific field
public function updatedEmail(): void
{
$this->validateOnly('email');
}
Form Request (complex forms):
php artisan make:request Store[Model]Request --no-interaction
use App\Http\Requests\StorePostRequest;
public function save(StorePostRequest $request): void
{
$validated = $request->validated();
Post::create($validated);
}
6. Loading States
Always provide visual feedback:
<flux:button type="submit" wire:loading.attr="disabled" wire:click="save">
<span wire:loading.remove wire:target="save">Save</span>
<span wire:loading wire:target="save">Saving...</span>
</flux:button>
<!-- Disable field during submission -->
<flux:input
wire:model.blur="title"
wire:loading.attr="disabled"
wire:target="save"
/>
7. Error Handling
Individual field errors:
<flux:error name="title" />
All errors:
@if ($errors->any())
<flux:callout variant="danger">
<ul class="list-disc list-inside">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</flux:callout>
@endif
Conditional error display:
@error('email')
<flux:badge variant="danger">{{ $message }}</flux:badge>
@enderror
8. Success Feedback
Flash messages:
public function save(): void
{
$validated = $this->validate();
Post::create($validated);
session()->flash('success', 'Post created successfully!');
$this->redirect(route('posts.index'));
}
@if (session('success'))
<flux:callout variant="success">
{{ session('success') }}
</flux:callout>
@endif
Toast notifications (Flux Pro):
public function save(): void
{
$validated = $this->validate();
Post::create($validated);
$this->dispatch('toast', message: 'Saved!', variant: 'success');
}
9. Testing Forms
use Livewire\Livewire;
use App\Livewire\Features\PostForm;
test('creates post with valid data', function () {
$category = Category::factory()->create();
Livewire::test(PostForm::class)
->set('title', 'Test Post')
->set('content', 'Test content')
->set('categoryId', $category->id)
->call('save')
->assertHasNoErrors()
->assertDispatched('post-created');
expect(Post::where('title', 'Test Post')->exists())->toBeTrue();
});
test('validates required fields', function () {
Livewire::test(PostForm::class)
->set('title', '')
->call('save')
->assertHasErrors(['title']);
});
Advanced Patterns
For complex form scenarios, see:
- Multi-step forms →
references/advanced-patterns.md - Dynamic fields →
references/advanced-patterns.md - Form with relationships →
references/advanced-patterns.md - Complete working example →
references/complete-example.md
Quick Reference
Common Flux Components
| Component | Usage |
|---|---|
<flux:input> |
Text, email, password, number inputs |
<flux:textarea> |
Multi-line text |
<flux:select> |
Dropdown selection |
<flux:checkbox> |
Boolean/toggle |
<flux:radio> |
Single choice from group |
<flux:date-picker> |
Date selection (Pro) |
<flux:file-upload> |
File uploads (Pro) |
See references/flux-components.md for detailed examples.
Wire Directives
| Directive | Purpose |
|---|---|
wire:model.blur="field" |
Update on blur (recommended) |
wire:loading |
Show during request |
wire:loading.attr="disabled" |
Disable during request |
wire:target="method" |
Scope loading to method |
wire:key="unique-id" |
Required in loops |
Output Checklist
- ✅ Form validates all inputs
- ✅ Loading states on submit button
- ✅ Error messages display clearly
- ✅ Success feedback provided
- ✅ Accessibility (labels, required attributes)
- ✅ Tests written and passing
- ✅ Uses class-based Livewire components
- ✅ Uses Flux UI components consistently
- ✅ Light mode only (no dark mode support)
- ✅ Code formatted with Pint
Livewire v4 Attributes
| Attribute | Purpose |
|---|---|
#[On('event')] |
Listen for events (replaces $listeners property) |
#[Computed] |
Cache derived property until dependencies change |
#[Locked] |
Prevent property modification from frontend |
#[Renderless] |
Skip re-rendering after method call |
#[Validate] |
Inline validation on property |
New v4 Directives
| Directive | Purpose |
|---|---|
wire:sort |
Drag-and-drop sorting |
wire:intersect |
Trigger action when element enters viewport |
wire:ref |
Element reference for JavaScript interaction |
.renderless modifier |
Skip re-rendering for specific action |
.preserve-scroll modifier |
Maintain scroll position |
Important Reminders
- ALWAYS use class-based Livewire components (NOT Volt)
- ALWAYS use
#[On('event')]attribute for event listeners (NOT$listenersproperty) - ALWAYS use typed properties with explicit return types on all methods
- ALWAYS use
wire:model.blurfor standard inputs (better UX than.live) - ALWAYS add loading states to submit buttons
- ALWAYS use
wire:keyin dynamic field loops - ALWAYS validate on the server (Livewire actions)
- ALWAYS provide error feedback for each field
- NEVER add dark mode support (light mode only)
- NEVER customize Flux UI component colors, typography, or borders (only padding/margins)
- NEVER trust client-side validation alone
- NEVER use Volt (use class-based Livewire)
- NEVER use
protected $listeners(use#[On]attribute instead) - SEARCH Flux documentation before creating custom components