Advanced Livewire Form Objects:Tips, Tricks & Best Practices

  • Home
  • Livewire
  • Advanced Livewire Form Objects:Tips, Tricks & Best Practices
Front
Back
Right
Left
Top
Bottom
OBJECT

Advanced Form Object Patterns

In Part 1, we covered the fundamentals of Livewire Form Objects. Now let’s dive into advanced patterns that solve real-world challenges you’ll face in production applications.
NESTED

Nested Form Objects

Building a multi-section form like a job application? Nest form objects for ultimate organization:
Copy to clipboard
// app/Livewire/Forms/PersonalInfoForm.php
class PersonalInfoForm extends Form
{
    public $name  = '';
    public $email = '';
    public $phone = '';
    
    public function rules(): array
    {
        return [
            'name'  => 'required|min:3',
            'email' => 'required|email',
            'phone' => 'required',
        ];
    }
}

// app/Livewire/Forms/ExperienceForm.php
class ExperienceForm extends Form
{
    public $company  = '';
    public $position = '';
    public $years    = '';
    
    public function rules(): array
    {
        return [
            'company'  => 'required',
            'position' => 'required',
            'years'    => 'required|numeric',
        ];
    }
}

// app/Livewire/Forms/JobApplicationForm.php
class JobApplicationForm extends Form
{
    public PersonalInfoForm $personal;
    public ExperienceForm $experience;
    
    public function mount(): void
    {
        $this->personal   = new PersonalInfoForm();
        $this->experience = new ExperienceForm();
    }
    
    public function submit(): void
    {
        $this->personal->validate();
        $this->experience->validate();
        
        JobApplication::create([
            'name'     => $this->personal->name,
            'email'    => $this->personal->email,
            'phone'    => $this->personal->phone,
            'company'  => $this->experience->company,
            'position' => $this->experience->position,
            'years'    => $this->experience->years,
        ]);
    }
}


// View binding:
// <input wire:model="form.personal.name">
// <input wire:model="form.experience.company">
VALIDATION

Dynamic Validation Rules

Adjust validation based on user input. According to Laravel’s validation documentation, conditional rules provide flexibility without sacrificing security.
OrderForm.php
Copy to clipboard
class OrderForm extends Form
{
    public $shipping_method          = 'standard';
    public $express_date             = '';
    public $billing_same_as_shipping = true;
    public $billing_address          = '';
    
    public function rules(): array
    {
        return [
            'shipping_method' => 'required|in:standard,express',
            'express_date'    => $this->shipping_method === 'express'
                ? 'required|date|after:today' 
                :  'nullable',
            'billing_address' => $this->billing_same_as_shipping
                ? 'nullable' 
                :  'required|min:10',
        ];
    }
}
FILES

File Uploads in Form Objects

Handle file uploads elegantly:
Copy to clipboard
// app/Livewire/Forms/ProfileForm.php
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;

class ProfileForm extends Form
{
    public $name                          = '';
    public $bio                           = '';
    public ?TemporaryUploadedFile $avatar = null;
    
    public function rules(): array
    {
        return [
            'name'   => 'required|min:3',
            'bio'    => 'required|max:500',
            'avatar' => 'nullable|image|max:2048',
        ];
    }
    
    public function save($userId): void
    {
        $this->validate();
        
        $user = User::findOrFail($userId);
        
        $data = [
            'name' => $this->name,
            'bio'  => $this->bio,
        ];
        
        // Validate file
        if ($this->avatar) {
            // Store file
            $data['avatar'] = $this->avatar->store('avatars', 'public');
        }
        
        $user->update($data);
        
        $this->reset('avatar');
    }
}

// app/Livewire/EditProfile.php
use Livewire\WithFileUploads;

class EditProfile extends Component
{
    use WithFileUploads;
    
    public ProfileForm $form;
    
    public function save(): void
    {
        $this->form->save(auth()->id());
        session()->flash('success', 'Profile updated!');
    }
}


// View:
<form wire:submit="save">
    <input type="text" wire:model="form.name">
    
    <textarea wire:model="form.bio"></textarea>
    
    <input type="file" wire:model="form.avatar">
    
    @if ($form->avatar)
        <img src="{{ $form->avatar->temporaryUrl() }}" width="100">
    @endif
    
    <button type="submit">Save Profile</button>
</form>
STATE

Form State Management

Track form state for better UX like Component with auto-save:
OrderForm.php
Copy to clipboard
// app/Livewire/Forms/ArticleForm.php
class ArticleForm extends Form
{
    public $title     = '';
    public $content   = '';
    public $status    = 'draft';
    public $lastSaved = null;
    
    public function rules(): array
    {
        return [
            'title'   => $this->status === 'published' ? 'required|min:5' : 'nullable',
            'content' => $this->status === 'published' ? 'required|min:100' : 'nullable',
        ];
    }
    
    public function saveDraft($articleId): void
    {
        Article::findOrFail($articleId)->update([
            'title'   => $this->title,
            'content' => $this->content,
            'status'  => 'draft',
        ]);
        
        $this->lastSaved = now()->format('g:i A');
    }
    
    public function publish($articleId): void
    {
        $this->status = 'published';
        $this->validate();
        
        Article::findOrFail($articleId)->update([
            'title' => $this->title,
            'content' => $this->content,
            'status' => 'published',
            'published_at' => now(),
        ]);
    }
}


// app/Livewire/ArticleEditor.php
class ArticleEditor extends Component
{
    public ArticleForm $form;
    public Article $article;
    
    public function mount(Article $article): void
    {
        $this->article = $article;
        $this->form->fill($article);
    }
    
    public function updated($property): void
    {
        if (str_starts_with($property, 'form.')) {
            $this->form->saveDraft($this->article->id);
        }
    }
    
    public function publish(): void
    {
        $this->form->publish($this->article->id);
        return redirect()->route('articles.show', $this->article);
    }
}
PERFORMANCE

Performance Optimization Tips

Track form state for better UX like Component with auto-save:
Use Debouncing for Live Validation
form.blade.php
Copy to clipboard
<!-- Validates immediately - lots of requests -->
<input wire:model.live="form.email">

<!-- Validates after 500ms of no typing - much better -->
<input wire:model.live.debounce.500ms="form.email">
Validate Only Changed Fields
ContactForm.php
Copy to clipboard
public function updatedFormEmail(): void
{
    $this->form->validateOnly('email');
}
Use Computed Properties Wisely
CheckoutForm.php
Copy to clipboard
class CheckoutForm extends Form
{
    public $items    = [];
    public $tax_rate = 0.08;
    
    #[Computed]
    public function subtotal(): float
    {
        return collect($this->items)->sum('price');
    }
    
    #[Computed]
    public function tax(): float
    {
        return $this->subtotal() * $this->tax_rate;
    }
    
    #[Computed]
    public function total(): float
    {
        return $this->subtotal() + $this->tax();
    }
}

// Access in views as properties: {{ $form->total }}
PITFALLS

Common Pitfalls and Solutions

Forgetting the form. Prefix
form.blade.php
Copy to clipboard
<!-- Wrong -->
<input wire:model="name">

<!-- Correct -->
<input wire:model="form.name">
Not Resetting After Submission
Always reset your form to clear data:
ContactForm.php
Copy to clipboard
public function submit(): void
{
    $this->form->validate();
    // Save data...
    $this->form->reset(); // Don't forget this!
    session()->flash('success', 'Saved!');
}
Over-Validating
Don’t validate on every keystroke for complex rules:
CheckoutForm.php
Copy to clipboard
<!-- Bad: Database query on every keystroke -->
<input wire:model.live="form.username">

<!-- Good: Validate on blur -->
<input wire:model.blur="form.username">
TESTING

Testing Form Objects

Form Objects make testing incredibly easy:
styles.php
Copy to clipboard
use App\Livewire\Forms\ContactForm;
use Illuminate\Validation\ValidationException;

test('contact form validates email', function () {
    $form          = new ContactForm();
    $form->name    = 'John Doe';
    $form->email   = 'invalid-email';
    $form->message = 'Hello there';
    
    expect(fn() => $form->validate())
        ->toThrow(ValidationException::class);
});

test('contact form accepts valid data', function () {
    $form          = new ContactForm();
    $form->name    = 'John Doe';
    $form->email   = '[email protected]';
    $form->message = 'Hello there';
    
    expect($form->validate())->toBeArray();
});

test('contact form stores data correctly', function () {
    $form          = new ContactForm();
    $form->name    = 'John Doe';
    $form->email   = '[email protected]';
    $form->message = 'Hello there';
    
    $form->store();
    
    $this->assertDatabaseHas('contacts', [
        'email' => '[email protected]',
    ]);
});
WHEN NOT

When NOT to Use Form Objects

Don't use for:
Do use for:
Production Checklist
Before deploying forms to production:
Quick Reference
Create form object
php artisan livewire:form ContactForm
Use in component
public ContactForm $form;
Bind in view
< i nput wire:model="form.property">
Custom methods
public function store() { /* logic */ }
Nested forms
public PersonalInfoForm $personal;
Conditional validation
'field' => $this->condition ? 'required' : 'nullable'
Form Objects transform your code by
Best practices

Remember

Form Objects aren't just a feature—they're a mindset shift toward cleaner, more maintainable Laravel applications.

The best code is boring code. Form Objects make your forms predictable, testable, and boring—in the best way possible.

Matt Stauffer Laravel Developer & Author

Thank You for Spending Your Valuable Time

I truly appreciate you taking the time to read blog. Your valuable time means a lot to me, and I hope you found the content insightful and engaging!
Front
Back
Right
Left
Top
Bottom
FAQ's

Frequently Asked Questions

Use nested Form Objects when you have distinct sections with their own validation logic (like personal info, experience, and references in a job application). This approach keeps each section's logic isolated and reusable. However, if your form fields are closely related and share validation rules, a single Form Object is simpler and more appropriate. The rule of thumb: if you can describe different "parts" of your form as separate concepts, nest them.

Always use the TemporaryUploadedFile type hint and reset the file property after storing it with $this->reset('avatar'). Use Livewire's WithFileUploads trait in your component, not in the Form Object itself. For large files, add wire:loading states and consider using chunk uploads. Set appropriate validation rules (max file size) and always store files in the form's save method, not during validation, to avoid processing files multiple times.

wire:model.live validates on every keystroke, which is useful for instant feedback on simple fields but can cause performance issues with complex validation (like database queries). wire:model.blur only validates when the user leaves the field, reducing server requests significantly. Use .live.debounce.500ms as a middle ground for fields that benefit from live validation but don't need instant feedback on every character typed.

Form Objects can absolutely contain business logic beyond validation! Methods like store(), update(), or publish() belong in Form Objects when they're directly related to form submission. This keeps your Livewire components thin and focused on UI concerns. However, complex domain logic or operations involving multiple models should live in dedicated service classes or actions that your Form Object calls.

Form Objects are plain PHP classes, so you can instantiate and test them directly without rendering components. Set properties, call validate(), and assert exceptions are thrown for invalid data. Test custom methods like store() by checking database state afterward. This isolation makes unit tests fast and focused. You can test the form logic separately from the component's UI behavior, leading to better test coverage and easier debugging.

Comments are closed