Validasi yang sesungguhnya sudah kita miliki di sisi server — section Validasi dan Form membahasnya sampai ke form request dan pesan error kustom. Lalu kenapa chapter ini ada? Karena menunggu round-trip penuh demi tahu bahwa kolom judul kurang dua karakter adalah pengalaman yang lambat, padahal browser tahu jawabannya lebih cepat. Validasi klien adalah pemanis: atribut HTML5 untuk pencegahan, sedikit JavaScript untuk umpan balik, dan satu endpoint validasi untuk cek tanpa submit. Prinsipnya satu dan tanpa pengecualian.
Atribut HTML5: Pencegahan Gratis #
Form tugas kita memakai tiga atribut validasi bawaan browser:
{{-- resources/views/tugas-baru.blade.php (inti) --}}
<form method="POST" action="{{ route('tugas.simpan') }}" data-guard class="space-y-4">
@csrf
<div>
<label for="judul" class="block text-sm font-medium text-slate-700">Judul</label>
<input
id="judul"
name="judul"
type="text"
required
minlength="3"
value="{{ old('judul') }}"
data-validasi-url="{{ url('/validasi/judul') }}"
class="mt-1 w-full rounded-lg ring-1 ring-slate-300 px-3 py-2 text-sm"
>
<p class="mt-1 text-xs text-slate-600" data-pesan="judul"></p>
@error('judul')
<p class="mt-1 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="email" class="block text-sm font-medium text-slate-700">Email Pemilik</label>
<input
id="email"
name="email"
type="email"
required
value="{{ old('email') }}"
class="mt-1 w-full rounded-lg ring-1 ring-slate-300 px-3 py-2 text-sm"
>
@error('email')
<p class="mt-1 text-sm text-rose-600">{{ $message }}</p>
@enderror
</div>
<x-ui.button type="submit">Simpan</x-ui.button>
</form>required menolak submit saat kosong, minlength="3" memaksa panjang minimal, dan type="email" menjalankan pemeriksaan format surel — semuanya gratis, tanpa satu baris JavaScript. Saat submit ditekan dalam kondisi invalid, browser menampilkan bubble error dan memfokuskan kolom bermasalah. Nilai old('judul') tetap terpasang karena itu urusan server, bukan browser. Kalau UX bubble bawaan dirasa kaku, atribut novalidate pada <form> mematikannya — namun begitu novalidate aktif, semua pencegahan HTML5 ikut mati dan JavaScript kita harus mengambil alih sepenuhnya. Form di atas memilih jalan tengah: atribut tetap aktif, JavaScript hanya menyempurnakan.
Tombol Submit Adaptif #
Pencegahan kedua: tombol Simpan yang menyala hanya saat form sehat. Modul resources/js/form-guard.js mengerjakannya dengan form.checkValidity() — API bawaan browser yang menilai seluruh atribut HTML5 di atas:
// resources/js/form-guard.js
export function perbaruiStatusTombol(form, tombol) {
tombol.disabled = !form.checkValidity();
}
export function pasangPenjagaForm(form) {
const tombol = form.querySelector('button[type="submit"]');
perbaruiStatusTombol(form, tombol);
form.addEventListener('input', () => {
perbaruiStatusTombol(form, tombol);
});
}Karena fungsi murni ini hanya bergantung pada dua objek, logikanya bisa diuji di Node tanpa browser — kami memanggilnya dengan objek tiruan yang menyediakan checkValidity dan disabled:
node --input-type=module -e "
import { perbaruiStatusTombol } from './resources/js/form-guard.js';
const formKosong = { checkValidity: () => false };
const tombol = { disabled: false };
perbaruiStatusTombol(formKosong, tombol);
console.log('Form kosong → tombol.disabled =', tombol.disabled);
"Output yang Dihasilkan:
Form kosong → tombol.disabled = truePengaitannya ke halaman ada di app.js: semua form[data-guard] dipasangi penjaga saat DOMContentLoaded, sehingga halaman tanpa atribut itu tidak terpengaruh. Kombinasi disabled plus atribut HTML5 juga menyentuh aksesibilitas — tombol mati memberi sinyal jelas sebelum pengguna mencoba submit.
Validasi Inline via fetch #
Lapisan ketiga menjawab kebutuhan yang tak bisa dipenuhi HTML5: aturan yang hanya diketahui server. Kolom judul kita mengecek min:3 yang sama dengan server, tapi bayangkan versi nyatanya — unique ke tabel tasks. Endpoint kecil melayani cek itu:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
Route::post('/validasi/judul', function (Request $request) {
$validator = Validator::make($request->only('judul'), [
'judul' => 'required|min:3',
]);
if ($validator->fails()) {
return response()->json([
'ok' => false,
'pesan' => $validator->errors()->first('judul'),
], 422);
}
return response()->json(['ok' => true, 'pesan' => 'Judul lolos pemeriksaan.']);
});Di sisi klien, modul form-guard.js juga menampung fungsi yang menembak endpoint ini saat kolom judul kehilangan fokus:
export async function cekJudulSaatBlur(input, targetPesan) {
input.addEventListener('blur', async () => {
const response = await fetch(input.dataset.validasiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
body: JSON.stringify({ judul: input.value }),
});
const data = await response.json();
targetPesan.textContent = data.pesan;
targetPesan.classList.toggle('text-rose-600', !data.ok);
targetPesan.classList.toggle('text-emerald-600', data.ok);
});
}URL endpoint dan target pesan tidak dikodekan keras — keduanya dibaca dari atribut data-validasi-url dan data-pesan di markup, sehingga modul yang sama bisa melayani form mana pun. Header X-CSRF-TOKEN diambil dari meta tag yang kita pasang di layout, mengikuti praktik dari chapter CSRF Protection
. Feature test memastikan endpoint berperilaku benar untuk kedua cabangnya, sekaligus membuktikan server tidak bisa dikelabui:
public function test_endpoint_validasi_menolak_judul_pendek(): void
{
$response = $this->postJson('/validasi/judul', ['judul' => 'AB']);
$response->assertStatus(422);
$this->assertSame(
'The judul field must be at least 3 characters.',
$response->json('pesan'),
);
}
public function test_server_tetap_menolak_data_yang_dilewati_klien(): void
{
$response = $this->from(route('tugas.baru'))
->post('/tugas', ['judul' => 'AB', 'email' => 'budi@@example']);
$response->assertRedirect(route('tugas.baru'));
$response->assertSessionHasErrors(['judul', 'email']);
}Output yang Dihasilkan:
PASS Tests\Feature\FormFrontendTest
✓ halaman form memuat atribut validasi html5 0.18s
✓ endpoint validasi menolak judul pendek 0.02s
✓ endpoint validasi menerima judul layak 0.01s
✓ server tetap menolak data yang dilewati klien 0.01s
Tests: 4 passed (17 assertions)Pola Dua Lapis yang Benar #
Test terakhir di atas adalah inti pola ini: validasi klien dan server memakai aturan yang sama (required|min:3), tapi keduanya berdiri sendiri-sendiri. Kalau JavaScript mati, atribut HTML5 hilang, dan pengguna mengirim AB langsung — server tetap menolak, redirect kembali, old() mengisi ulang form, dan @error menampilkan pesan. Alur lengkap itu sudah kita bangun di chapter Forms dan Old Input
.
Lapisan-lapisan ini punya urutan investasi yang jelas: pasang atribut HTML5 dulu (gratis), tombol adaptif kedua (satu modul kecil), endpoint inline terakhir (hanya untuk aturan server-only seperti unique). Dengan form yang responsif, kekurangan tinggal satu: interaktivitas di luar form masih bergantung pada trik CSS murni. Chapter berikutnya memperkenalkan Alpine.js — framework kecil untuk dropdown, tab, dan modal yang hidup, tanpa meninggalkan Blade.