Membangun form interaktif di Livewire: state dengan property per field, aturan validasi #[Validate] dan validate(), validasi real-time dengan validateOnly, custom error messages, dan pola multi-step forms untuk alur input yang panjang.

Form adalah jantung aplikasi web — dan di sinilah Livewire menunjukkan keunggulannya paling terlihat. Validasi yang biasanya membutuhkan duplikasi (JS untuk real-time, PHP untuk server) kini ditulis sekali di PHP, dan error bisa ditampilkan real-time tanpa reload. Di episode ini kita membangun fondasi form: state, aturan validasi, feedback, dan multi-step.
Kunci pemahamannya: karena state form hidup di server, validasi PHP bisa dipanggil kapan saja — bahkan per field — tanpa mengirim seluruh form. Inilah yang memungkinkan validasi real-time yang benar-benar aman (tidak hanya tampilan).
Setiap field input menjadi public property di komponen:
<?php
namespace App\Livewire;
use Livewire\Component;
class CreatePost extends Component
{
public string $title = '';
public string $body = '';
public string $status = 'draft';
public function save()
{
// validasi + simpan
}
}<form wire:submit="save">
<div>
<label for="title">Judul</label>
<input type="text" id="title" wire:model="title">
</div>
<div>
<label for="body">Isi</label>
<textarea id="body" wire:model="body"></textarea>
</div>
<div>
<label for="status">Status</label>
<select id="status" wire:model="status">
<option value="draft">Draft</option>
<option value="published">Published</option>
</select>
</div>
<button type="submit">Simpan</button>
</form>wire:submit="save" mengirim seluruh state form ke save() dalam satu request — di v4, field wire:model default deferred ikut tersinkron pada saat itu juga.
Ada dua gaya: attribute #[Validate] di property, atau method rules() + panggilan validate().
use Livewire\Attributes\Validate;
class CreatePost extends Component
{
#[Validate('required|min:3|max:255')]
public string $title = '';
#[Validate('required|min:10')]
public string $body = '';
}Aturan ini otomatis aktif saat $this->validate() dipanggil:
public function save()
{
$this->validate();
Post::create([
'title' => $this->title,
'body' => $this->body,
'status' => $this->status,
]);
session()->flash('status', 'Post berhasil dibuat');
$this->reset();
}Alternatifnya, deklarasikan aturan dalam method dan panggil validate():
public function rules()
{
return [
'title' => ['required', 'min:3', 'max:255'],
'body' => ['required', 'min:10'],
];
}
public function save()
{
$this->validate();
// simpan...
}rules() lebih fleksibel untuk aturan dinamis (misalnya 'required' hanya jika status tertentu). Saat validasi gagal, Livewire melempar ValidationException, membatalkan aksi, dan mengisi error bag — tanpa reload.
Error tersedia lewat helper $errors di view:
<div>
<label for="title">Judul</label>
<input type="text" id="title" wire:model="title">
@error('title')
<span class="text-red-600">{{ $message }}</span>
@enderror
</div>Bisa juga lewat direktif @error biasa yang sudah didukung Laravel — Livewire mengisi error bag dengan format standar sehingga keduanya bekerja.
Untuk feedback langsung saat user mengisi field, panggil validasi per-field pada hook updated:
public function updatedTitle($value)
{
$this->validateOnly('title');
}validateOnly('title') memvalidasi hanya satu field dan mengembalikan error-nya. Dikombinasikan dengan wire:model.live pada input, error muncul saat user selesai mengetik field itu — tanpa menunggu submit:
<input type="text" wire:model.live="title">Warning
validateOnly memvalidasi satu field — jangan pakai untuk menampilkan semua error form saat user baru menyentuh satu field. Simpan validateOnly di hook updated* untuk feedback per-field, dan biarkan validate() penuh dijalankan saat submit.
Aturan Laravel punya pesan default yang generik. Ganti dengan pesan yang lebih manusiawi:
public function rules()
{
return [
'title' => ['required', 'min:3'],
];
}
public function messages()
{
return [
'title.required' => 'Judul wajib diisi.',
'title.min' => 'Judul minimal :min karakter.',
];
}Atau gabungkan aturan dan pesan dalam satu method:
public function validateTitle()
{
$this->validate([
'title' => 'required|min:3',
], [
'title.required' => 'Judul wajib diisi.',
]);
}Form panjang dipecah menjadi langkah-langkah. Kuncinya: simpan indeks langkah dan kondisi render per langkah, lalu validasi parsial di tiap langkah:
public int $step = 1;
public function next()
{
$rules = match ($this->step) {
1 => ['title' => 'required|min:3', 'body' => 'required'],
2 => ['category' => 'required'],
default => [],
};
$this->validate($rules);
$this->step++;
}
public function back()
{
$this->step = max(1, $this->step - 1);
}@if ($step === 1)
<input type="text" wire:model="title">
<textarea wire:model="body"></textarea>
@elseif ($step === 2)
<select wire:model="category">
<!-- opsi kategori -->
</select>
@endif
<button type="button" wire:click="back">Kembali</button>
<button type="button" wire:click="next">Lanjut</button>Setiap langkah hanya memvalidasi field miliknya — jadi user tidak dihujani error field yang belum mereka lihat. Perhatikan tiap langkah dipertahankan sebagai state, jadi kembali ke langkah sebelumnya tidak kehilangan data.
wire:submit: submit biasa me-reload halaman dan state hilang (episode 5).validate() di PHP tetap wajib.validateOnly untuk semua field: boros dan bikin pengalaman aneh. Pakai per-field di hook.null, aturan required akan menangkapnya dengan pesan membingungkan. Set default '' untuk field teks.$this->reset() agar form bersih untuk input berikutnya.Inti yang harus dibawa pulang:
wire:submit mengirim semua state ke action.#[Validate] atau rules() + validate(); @error untuk menampilkan pesan.validateOnly('field') di hook updated* untuk validasi real-time per field.messages(); multi-step dengan indeks langkah + validasi parsial.Di episode 10 selanjutnya kita akan membahas Form Objects (v4) — class Form khusus untuk merekap state dan aturan validasi, bind() untuk menghubungkan ke model, sehingga pola create/update bisa dipakai ulang dan tetap konsisten. Sampai jumpa di episode 10!