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.
- Instead, we have to do the validations directly in the component. And yes, that works, but:
- It mixes validation logic with presentation logic,
- 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>