Genial! - Clase/Objeto formulario en Laravel Livewire, simplifica tus Componentes
Esto me parece muy interesante: la posibilidad de separar la definición de cada uno de los campos del formulario en un archivo aparte.
Una de las cosas que critico en silencio de Livewire —sí, lo admito— es precisamente eso: que no podemos crear clases Request como lo hacemos normalmente en Laravel para definir los campos y las validaciones del formulario.
En lugar de eso, tenemos que hacer las validaciones directamente en el componente. Y sí, eso funciona, pero:
- Mezcla lógica de validación con lógica de presentación,
- Hace los componentes más largos y difíciles de mantener,
- Y rompe un poco con la forma tradicional y limpia de trabajar con formularios en Laravel.
Veamos un código:
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",
];
Imposibilitando la reutilización y modularización del componente, pero, mediante una clase formulario, podemos solventar esto fácilmente.
Formularios en Laravel Livewire
Para crear una clase formulario, que es equivalente al Request para los controladores:
$ php artisan livewire:form PostForm
Colocamos nuestros campos:
<?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 = '';
}
En este caso según el ejemplo tenemos que emplear el atributo de validate eso lo tenemos que migrar.
Ahora, nuestro componente y vista quedan mucho más limpias de la siguiente forma, usando una instancia de la clase anterior y no una propiedad por cada atributo:
El componente queda como:
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
]);
}
}
}
Y en la vista, las referencias a cada uno de los campos:
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>