Awesome! - Form class/object in Laravel Livewire, simplify your components

I find this very interesting: the ability to separate the definition of each form field into a separate file.

One of the things I silently criticize about Livewire—yes, I admit it—is precisely that: we can't create Request classes like we normally do in Laravel to define form fields and validations.

  1. Instead, we have to do the validations directly in the component. And yes, that works, but:
  2. It mixes validation logic with presentation logic,
  3. It makes components longer and harder to maintain,

And it breaks a bit with the traditional, clean way of working with forms in Laravel.

class Save extends Component
{

    use WithFileUploads;

    public $title;
    public $slug;
    public $text;
    public $image;

    protected $rules = [
        'title' => "required|min:2|max:255",
        'text' => "nullable",
        'image' => "nullable|image|max:1024",
    ];

Making reuse and modularization of the component impossible, but, by using a form class, we can easily solve this.

Forms in Laravel Livewire

To create a form class, which is equivalent to the Request for controllers:

$ php artisan livewire:form PostForm

We place our fields:

<?php


namespace App\Livewire\Forms;


use Livewire\Attributes\Validate;
use Livewire\Form;


class PostForm extends Form
{


    #[Validate('required|min:2|max:255')]
    public $title = '';
    
    #[Validate('required')]
    public $date = '';
    
    #[Validate('required')]
    public $category_id = '';
    
    #[Validate('required')]
    public $posted = '';


    #[Validate('required')]
    public $type = '';
    
    #[Validate('required|min:2|max:5000')]
    public $text = '';


    #[Validate('required|min:2|max:255')]
    public $description = '';


    #[Validate('nullable|image|max:1024')]
    public $image = '';
}

In this case, according to the example, we need to use the validate attribute, which we need to migrate.

Now, our component and view are much cleaner as follows, using an instance of the previous class and not a property for each attribute:

The component looks like this:

app/Livewire/Dashboard/Post/Save.php

<?php

namespace App\Livewire\Dashboard\Post;

use App\Livewire\Forms\PostForm;
use Livewire\Attributes\Locked;
use Livewire\Component;

use App\Models\Category;
use App\Models\Post;

class Save extends Component
{

   public $post;
   public PostForm $form;

   #[Locked]
   public $id;

   public function render()
   {
       $categories = Category::get();
       return view('livewire.dashboard.post.save', compact('categories'));
   }

   function mount(?int $id = null)
   {
       if ($id != null) {
           $this->id = $id;
           $this->post = Post::findOrFail($id);
           $this->form->text = $this->post->text;
           $this->form->title = $this->post->title;
           $this->form->category_id = $this->post->category_id;
           $this->form->posted = $this->post->posted;
           $this->form->type = $this->post->type;
           $this->form->description = $this->post->description;
           $this->form->date = $this->post->date;
       }
   }

   function submit(/*$content*/)
   {
       $this->validate();

       if ($this->post) {
           $this->post->update($this->form->all());
           $this->dispatch('updated');
       } else {
           $this->post = Post::create($this->form->all());
           $this->dispatch('created');
       }

       // upload
       if ($this->form->image) {
           $imageName = $this->post->slug . '.' . $this->form->image->getClientOriginalExtension();
           $this->form->image->storeAs('images/post', $imageName, 'public_upload');

           $this->post->update([
               'image' => $imageName
           ]);
       }
   }
}

And in the view, the references to each of the fields:

resources/views/livewire/dashboard/post/save.blade.php

<div>
***
       <form wire:submit.prevent="submit" class="flex flex-col gap-4">

           <flux:input wire:model="form.title" :label="__('Title')" />
           <flux:input wire:model="form.date" :label="__('Date')" type="date" />
           <flux:textarea wire:model="form.description" :label="__('Description')" />

           <flux:textarea wire:model="form.text" :label="__('Text')" />

           <flux:label>{{ __('Posted') }}</flux:label>
           <flux:select wire:model='form.posted'>
               <option value=""></option>
               <option value="yes">Yes</option>
               <option value="not">Not</option>
           </flux:select>

           <flux:label>{{ __('Type') }}</flux:label>
           <flux:select wire:model='form.type'>
               <option value=""></option>
               <option value="advert">Advert</option>
               <option value="post">Post</option>
               <option value="course">Course</option>
               <option value="movie">Movie</option>
           </flux:select>

           <flux:label>{{ __('Category') }}</flux:label>
           <flux:select wire:model='form.category_id'>
               <option value=""></option>
               @foreach ($categories as $c)
                   <option value="{{ $c->id }}">{{ $c->title }}</option>
               @endforeach
           </flux:select>

           <flux:input wire:model="form.image" type='file' :label="__('Image')" />

           @if ($post && $post->image)
               <img class="w-40 my-3" src="{{ $post->getImageUrl() }}" alt="{{ $post->title }}">
           @endif

           <div>
               <flux:button variant="primary" type="submit">
                   {{ __('Save') }}
               </flux:button>
           </div>

       </form>


   </div>
</div>

I hated using Long Forms in Laravel Livewire until I discovered.. livewire:form

I was about to make a video trashing Livewire, which might be surprising if you've followed my other videos, in which I've thrown a little bit of dirt—humorously, or at least that's how I like to see it—on Inertia. Whenever I compare Livewire to Inertia, I also throw a few criticisms at Inertia.

And now, oddly enough, I'm throwing a little dirt at Livewire, because I've been a little annoyed with Laravel lately.

Livewire components and many fields are not good companions

I wasn't a big fan of using components in Livewire to define multiple fields. Obviously, this is a simple example, but we know that when we're working with real forms, it's not just two fields... it can be 10, 15, 20 fields.

For example, for my post creation form, I have about 20 fields, including content, description, metadata, categories, tags, etc.

And of course, with individual components per field, that becomes crazy: communication, state management, validation… everything becomes unnecessarily complicated.

I agree to receive announcements of interest about this Blog.

Let's learn how we can simplify form management by creating a form class.

- Andrés Cruz

En español