Episode ini membahas form handling dan validasi di ekosistem Svelte: binding dan event untuk input, validation patterns dan custom validators, server-side validation dengan form actions SvelteKit, serta accessibility form dan umpan balik UX yang baik.

Form adalah pintu masuk data utama bagi hampir semua aplikasi: login, registrasi, checkout, pencarian. Namun form juga salah satu sumber paling banyak bug dan frustrasi pengguna — pesan error yang membingungkan, data yang hilang, atau validasi yang hanya berjalan separuh.
Svelte membuat form handling terasa alami karena binding-nya langsung menyambung state ke input. Untuk validasi, ada dua lapisan yang saling melengkapi: client-side untuk umpan balik cepat dan server-side untuk keamanan. SvelteKit bahkan menyediakan form actions yang menangani seluruh alur submit dengan satu pola.
Episode ini membahas binding dan event untuk form, validation patterns dan custom validators, server-side validation dengan form actions SvelteKit, serta accessibility dan UX feedback. Kalian akan keluar dengan pola form yang bisa dipakai untuk kebutuhan apa pun.
Cara paling langsung mengikat input ke state adalah bind:value — pola yang sudah kalian kenal di episode 4:
<script>
let nama = $state("")
let email = $state("")
function submit(event) {
event.preventDefault()
console.log({ nama, email })
}
</script>
<form onsubmit={submit}>
<input bind:value={nama} placeholder="Nama" />
<input bind:value={email} type="email" placeholder="Email" />
<button type="submit">Kirim</button>
</form>bind:value={nama} membuat state sinkron dengan isi input tanpa menulis oninput manual. event.preventDefault() menghentikan reload halaman bawaan form. Setelah submit, semua nilai sudah tersedia di state — tidak perlu query DOM.
Validasi dasar bisa dilakukan langsung saat submit atau saat input berubah:
<script>
let email = $state("")
let error = $state("")
function submit(event) {
event.preventDefault()
if (!email.includes("@")) {
error = "Format email tidak valid"
return
}
error = ""
console.log("Valid!", email)
}
</script>
<form onsubmit={submit}>
<input bind:value={email} type="email" placeholder="Email" />
{#if error}
<p class="error" role="alert">{error}</p>
{/if}
<button type="submit">Kirim</button>
</form>if (!email.includes("@")) adalah validasi minimal. Pesan error disimpan di state dan ditampilkan dengan role="alert". Validasi client-side memberi umpan balik instan tanpa menunggu jaringan — tapi jangan pernah mengandalkannya sebagai satu-satunya lapisan pertahanan.
Bungkus aturan validasi ke dalam fungsi agar bisa dipakai ulang dan diuji:
export function validasiEmail(value) {
const pola = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return pola.test(value) ? null : "Alamat email tidak valid"
}
export function validasiWajib(value) {
return value.trim().length > 0 ? null : "Field ini wajib diisi"
}pola.test(value) memeriksa format email dengan regex. Setiap validator mengembalikan pesan error atau null saat lolos. Struktur konsisten ini membuat komposisi validator menjadi mudah — kalian bisa menggabungkan beberapa aturan untuk satu field.
SvelteKit menangani submit form tanpa JavaScript dengan form actions. File +page.server.js mengekspor action bernama default:
import { fail } from "@sveltejs/kit"
export const actions = {
default: async ({ request }) => {
const data = await request.formData()
const email = String(data.get("email") ?? "")
if (!email.includes("@")) {
return fail(400, { email, error: "Email tidak valid" })
}
return { sukses: true }
},
}request.formData() membaca data form di server. fail(400, { email, error }) mengembalikan response 400 beserta pesan error dan nilai yang diketik pengguna — sehingga field tidak kosong setelah error. Inilah validasi server: tidak bisa dilewati siapa pun.
Komponen halaman menerima hasil action melalui prop form:
<script>
let { form } = $props()
</script>
<form method="POST" action="?/default">
<input name="email" type="email" value={form?.email ?? ""} />
{#if form?.error}
<p role="alert">{form.error}</p>
{/if}
<button type="submit">Kirim</button>
</form>let { form } = $props() berisi hasil action terakhir. form?.error menampilkan pesan error dari server, dan value={form?.email ?? ""} mempertahankan isi input. Form bekerja penuh tanpa JavaScript, dan otomatis menjadi progresif saat JS dimuat.
Form yang aksesibel dimulai dari label yang benar. Setiap input wajib punya <label> — atau atribut aria-label jika label visual tidak memungkinkan:
<form>
<label for="nama">Nama lengkap</label>
<input id="nama" name="nama" />
<label for="umur">Umur</label>
<input id="umur" name="umur" type="number" min="0" max="120" />
<button type="submit">Simpan</button>
</form>label for="nama" menghubungkan label ke input lewat atribut id. Pembaca layar mengumumkan label saat pengguna fokus ke input. Atribut min dan max memberi validasi bawaan browser sekaligus panduan untuk keyboard.
Umpan balik form yang baik mencegah frustrasi:
aria-invalid="true" pada input yang gagal validasi membantu teknologi asistif menandai kesalahan. Ditambah pesan error yang terkait via aria-describedby, pengguna dengan screen reader mendapat konteks yang sama dengan pengguna visual.
Inti yang harus dibawa pulang:
bind:value menyambungkan input ke state; FormData untuk mengambil semua field sekaligus.event.preventDefault() untuk form yang di-handle JavaScript.fail() mengembalikan nilai input beserta error agar field tidak kosong setelah gagal.role="alert", dan umpan balik yang jelas membuat form aksesibel dan nyaman.Di episode 11 selanjutnya kita akan membahas configuration dan environment — environment variables dan runtime config, konfigurasi Vite dan SvelteKit, asset management dan static file handling, serta feature flags dan multi-environment setup. Form kalian akan berkomunikasi dengan lingkungan produksi yang terkelola dengan baik.