Validasi form berarti memeriksa isian sebelum diterima. Ada dua lapis: browser memeriksa duluan lewat atribut HTML seperti required, lalu server memeriksa lagi, termasuk hal yang cuma server tahu. Bayangin meja Laundry Bu Rini. Kasir menolak nota yang kolom beratnya kosong, langsung di meja. Nota yang lolos dibawa ke belakang, dan bagian belakang mengecek apakah mesin hari ini masih muat. Kalau gak muat, nota dikembalikan dengan catatan merah di kolom yang salah, dan isian lainnya tetap.
🧪 Tebakan cepat: Server menolak order 25 kg dengan status 422 dan mengirim ulang form berisi pesan error. Belum ada pengaturan htmx apa pun. Apa yang terlihat di halaman? Jawabannya ada di bagian "422 gak ditukar".
Lapis 1: browser memeriksa duluan
<form id="terima" hx-post="/order" hx-target="this" hx-swap="outerHTML">
<input name="nama" placeholder="Nama" required>
<input name="berat" type="number" min="1" placeholder="kg" required>
<span class="error"></span>
<button>Terima cucian</button>
</form>
required: gak boleh kosong.type="number" min="1": harus angka, minimal 1.- Kasir mengosongkan Berat lalu menekan Terima cucian: browser menampilkan gelembung pesan ("Please fill out this field.", bahasanya mengikuti browser) dan gak memicu event
submit. htmx gak pernah tahu: 0 request. - htmx juga memeriksa form-nya sendiri sebelum mengirim. Kalau ada yang gak valid, htmx memicu
htmx:validation:faileddanhtmx:validation:halted, lalu gak mengirim apa pun.novalidatedi form mematikan kedua pemeriksaan itu. - Pemeriksaan browser cuma kenyamanan. Siapa pun bisa melewatinya lewat DevTools, jadi server tetap harus memeriksa semuanya.
Lapis 2: server menolak dengan 422
Mesin Bu Rini muat 30 kg sehari. Papan sudah berisi 10 kg (3 + 2 + 5), jadi sisanya 20 kg. Browser gak tahu angka itu, cuma server. Order 25 kg lolos min="1", lalu server membalas dengan status 422 Unprocessable Content ("isiannya sampai, tapi ditolak") dan form yang sama, berisi pesan:
<form id="terima" hx-post="/order" hx-target="this" hx-swap="outerHTML">
<input name="nama" placeholder="Nama" required value="Bu Tini">
<input name="berat" type="number" min="1" placeholder="kg" required value="25" aria-invalid="true">
<span class="error">Kapasitas mesin hari ini tinggal 20 kg.</span>
<button>Terima cucian</button>
</form>
- Isian lama dikirim balik lewat
value, jadi kasir gak perlu mengetik ulang. - Pesan error ada di sebelah kotak yang salah.
- Di Laravel:
return response()->view('order._form', [...], 422);.
422 gak ditukar
Di htmx 2, balasan 4xx dan 5xx gak ditukar. htmx memicu htmx:responseError, dan form-nya tetap seperti tadi, tanpa pesan apa pun. Kasir cuma melihat tombol yang gak bereaksi. Itu jawaban tebakannya.
responseHandling: izinkan 422 ditukar
<meta name="htmx-config" content='{"responseHandling": [
{"code": "204", "swap": false},
{"code": "[23]..", "swap": true},
{"code": "422", "swap": true, "error": true},
{"code": "[45]..", "swap": false, "error": true}
]}'>
- Taruh di
<head>. Aturan dibaca dari atas, dan yang pertama cocok menang, jadi422harus di atas[45]..(semua 4xx dan 5xx). "swap": true: balasan 422 ditukar seperti balasan sukses. Denganhx-target="this"danhx-swap="outerHTML", form lama diganti form berisi pesan."error": true: tetap dihitung gagal.htmx:responseErrortetap jalan, danevent.detail.successfultetapfalse, jadiif (event.detail.successful) this.reset()dari pelajaran form tetap benar.- Tulis keempat aturannya. Daftar ini mengganti daftar bawaan seluruhnya: kalau isinya cuma aturan 422, balasan 200 malah gak ditukar lagi.
- Saat sukses, server membalas form kosong yang baru, plus
<ul hx-swap-oob="beforeend:#papan-order"><li id="order-15">...</li></ul>supaya order-nya masuk ke papan (pelajaran OOB).
Cek satu kotak sebelum submit
<input name="berat" type="number" min="1" required
hx-post="/order/cek-berat" hx-trigger="blur"
hx-target="next .error" hx-swap="innerHTML">
<span class="error"></span>
blur: saat kasir meninggalkan kotak Berat. Karena input ini ada di dalam form dan request-nya bukan GET, htmx mengirim semua field form-nya:nama=Bu%20Tini&berat=25.next .error: elemen sesudahnya yang cocok dengan.error, yaitu span di sebelahnya.- Server membalas 200 dengan pesannya, atau balasan kosong kalau aman (yang menghapus pesan lama). Jangan 422 di sini, karena balasan itu gak ditukar.
hx-swap="innerHTML"wajib ditulis di sini: input mewarisihx-swap="outerHTML"dari form. Tanpa itu, span-nya ikut tertukar jadi teks biasa, dan blur berikutnya gak menemukan.errorlagi.- Untuk cek sambil mengetik:
hx-trigger="keyup changed delay:500ms", request dikirim 500 ms sesudah ketikan terakhir. Tanpadelay, setiap huruf mengirim satu request.
⚠️ Jebakan yang sering ketemu
- Mengira 422 otomatis tampil. Tanpa
responseHandling, balasan 422 gak ditukar dan halamannya diam. responseHandlingyang cuma berisi 422. Aturan bawaan hilang, dan balasan sukses gak ditukar lagi.- Lupa
hx-swap="innerHTML"di input.hx-swapdanhx-targetdari form diwariskan ke input di dalamnya. keyuptanpadelay. Satu request per huruf.- Cuma mengandalkan
required. Server tetap harus memeriksa. - Di latihan: input email butuh
hx-post="/api/validate-email"danhx-trigger="blur", laluhx-target="next .error"supaya pesannya masuk ke span di sebelahnya, bukan ke#form-resultyang diwariskan dari form.
TL;DR: Browser memeriksa duluan (required, min) dan menahan submit tanpa request. Server memeriksa lagi, termasuk yang cuma ia tahu, dan membalas 422 dengan form berisi pesan dan isian lama. htmx 2 gak menukar 422 kecuali responseHandling di <meta name="htmx-config"> mengizinkannya: tulis keempat aturan, 422 dengan "swap": true, "error": true di atas [45]... Untuk cek per kotak: hx-post, hx-trigger="blur" (atau keyup changed delay:500ms), hx-target="next .error", dan hx-swap="innerHTML", dengan server membalas 200 berisi pesan atau kosong.