Form Actions SvelteKit — Svelte

Form actions menangani form submission di server — tanpa perlu API endpoint terpisah. Membuat Action // src/routes/login/+page.server.js export const actions…

Form action adalah function di +page.server.js yang menerima isi sebuah <form> saat form itu dikirim dengan method="POST". Bayangin kertas pesanan di kasir Warung Bu Sri. Pembeli menulis nama, menu, dan jumlah porsi, lalu menyerahkannya ke kasir. Kalau namanya kosong, kasir mengembalikan kertasnya dengan catatan merah, dan tulisan yang sudah ada tetap di kertas itu. Kalau beres, kasir menyuruh pembeli pindah ke meja tunggu. Kertas itu <form>, kasir itu action di server, kertas yang dikembalikan itu fail(400, ...), dan pindah meja itu redirect(303, ...).

🧪 Tebakan cepat: JavaScript di browser pembeli mati, atau belum selesai dimuat. Dia mengisi form pesanan lalu menekan Pesan. Pesanannya tetap sampai ke server, atau tombolnya diam saja? Jawabannya ada di bagian "Tanpa JavaScript pun jalan".

Action di +page.server.js

Halaman /pesan Warung Bu Sri punya dua file. Ini yang di server:

// src/routes/pesan/+page.server.js
import { fail, redirect } from "@sveltejs/kit"
import { semuaMenu } from "#lib/menu.js"

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData()
    const nama = String(data.get("nama") ?? "").trim()
    const menuId = Number(data.get("menu"))
    const porsi = Number(data.get("porsi"))

    if (!nama) {
      return fail(400, { nama, menuId, porsi, pesan: "Nama pemesan wajib diisi" })
    }
    if (porsi < 1) {
      return fail(400, { nama, menuId, porsi, pesan: "Porsi minimal 1" })
    }

    const menu = semuaMenu.find((m) => m.id === menuId)
    console.log("pesanan masuk:", nama, menu.nama, porsi, "porsi")
    redirect(303, "/pesan/terima-kasih")
  },
}

Halamannya: export let form

<!-- src/routes/pesan/+page.svelte -->
<script>
  import { semuaMenu } from "#lib/menu.js"
  export let form
</script>

<h2>Pesan</h2>

{#if form?.pesan}
  <p class="error">{form.pesan}</p>
{/if}

<form method="POST">
  <label>Nama <input name="nama" value={form?.nama ?? ""} /></label>
  <label>
    Menu
    <select name="menu">
      {#each semuaMenu as m (m.id)}
        <option value={m.id} selected={form?.menuId === m.id}>{m.nama}</option>
      {/each}
    </select>
  </label>
  <label>Porsi <input name="porsi" type="number" value={form?.porsi ?? 1} /></label>
  <button>Pesan</button>
</form>

Hasilnya, dicoba di SvelteKit 3 dengan JavaScript dimatikan:

Isian Request Jawaban server Yang tampil
nama kosong, Mie Ayam, 3 porsi POST /pesan, isi nama=&menu=2&porsi=3 400, halaman /pesan digambar ulang "Nama pemesan wajib diisi", Mie Ayam dan 3 masih terisi
Andi, Mie Ayam, 3 porsi POST /pesan, isi nama=Andi&menu=2&porsi=3 303, location: /pesan/terima-kasih browser membuka GET /pesan/terima-kasih: "Terima kasih!"

Terminal server mencatat pesanan masuk: Andi Mie Ayam 3 porsi.

Kembalikan isiannya lewat fail

Tanpa JavaScript, jawaban 400 adalah halaman baru. Browser lupa semua yang tadi diketik, dan isi kolom datang dari value={form?....}. Saat fail cuma mengembalikan { pesan }, kiriman "nama kosong, Mie Ayam, 3 porsi" kembali sebagai Nasi Goreng dan 1 porsi; pembeli harus mengisi ulang semuanya. Dengan use:enhance (di bawah) pun, Nama dan Porsi kembali ke nilai bawaannya begitu form berubah. Karena itu kirim balik isiannya: fail(400, { nama, menuId, porsi, pesan }).

Pengecualiannya password: jangan pernah dikirim balik. Untuk form login, kembalikan email saja.

Tanpa JavaScript pun jalan

<form method="POST"> adalah HTML biasa. Browser sudah tahu cara mengirimnya tanpa JavaScript, dan server menjawab dengan halaman utuh. Jadi pesanannya tetap sampai. Itu jawaban tebakannya. Cara membangun seperti ini disebut progressive enhancement: fiturnya jalan dulu tanpa JavaScript, lalu jadi lebih nyaman kalau JavaScript ada. Inilah kelebihan form action dibanding API endpoint yang dipanggil dengan fetch: tanpa JavaScript, fetch gak jalan sama sekali.

Lebih nyamannya datang dari use:enhance, sebuah action Svelte (pelajaran Actions) dari $app/forms:

<script>
  import { enhance } from "$app/forms"
  import { semuaMenu } from "#lib/menu.js"
  export let form
</script>

<form method="POST" use:enhance>
  <!-- kolom yang sama -->
</form>

Dengan JavaScript, use:enhance mencegat pengirimannya dan mengirim POST /pesan lewat fetch, dengan header x-sveltekit-action: true. Server menjawab JSON, bukan halaman:

400  {"type":"failure","status":400,"location":"/pesan","data":"[{\"nama\":1,\"menuId\":2,\"porsi\":3,\"pesan\":4},\"\",2,3,\"Nama pemesan wajib diisi\"]"}
200  {"type":"redirect","status":303,"location":"/pesan/terima-kasih"}

Beberapa tombol: named action

Satu halaman bisa punya beberapa action. Beri nama di objek actions, misalnya pesan: async ({ request }) => {...} dan batal: async ({ request }) => {...}, lalu tunjuk lewat atribut action:

<form method="POST" action="?/pesan">...</form>
<form method="POST" action="?/batal"><button>Batal</button></form>

default gak boleh dicampur dengan action bernama; SvelteKit menjawab 500 dan terminal menulis When using named actions, the default action cannot be used. Form tanpa ?/... ke halaman yang cuma punya action bernama mendapat 404.

Proyek baru: SvelteKit 3 dan Svelte 5

Di proyek dari npx sv create, runes menyala, jadi halaman menulis let { form } = $props(); export let form ditolak dengan Cannot use export let in runes mode. Sisanya sama. Editor latihan meng-compile dengan Svelte 4, jadi latihannya memakai export let form.

Latihan: form login

Editor latihan menjalankan beberapa file route sekaligus, seperti SvelteKit kecil: src/routes/login/+page.svelte, src/routes/login/+page.server.js, dan src/routes/dashboard/+page.svelte. Form POST mengirim isiannya ke action di +page.server.js sebagai Request sungguhan, jadi await request.formData() jalan. Server-nya gak ada sungguhan: action-nya jalan di pratinjau.

// src/routes/login/+page.server.js
import { fail, redirect } from "@sveltejs/kit"

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData()
    const email = data.get("email")
    const password = data.get("password")

    if (password !== "rahasia123") {
      return fail(400, { email, error: "Password salah" })
    }

    redirect(303, "/dashboard")
  },
}

Dengan password yang salah, fail mengirim objeknya ke halaman sebagai form: pesan Password salah tampil dan email yang sudah diketik tetap ada lewat value={form?.email ?? ""}. Dengan rahasia123, redirect membawa ke /dashboard.

⚠️ Jebakan yang sering ketemu

TL;DR: Form action ditulis di +page.server.js sebagai export const actions = { default: async ({ request }) => {...} } dan menerima <form method="POST">. Isinya dibaca dengan await request.formData() dan data.get("nama"), semuanya string, dan cuma kolom ber-name yang terkirim. fail(400, { ...isian, pesan }) menolak kiriman dan mengirim objeknya ke halaman sebagai form (export let form, atau let { form } = $props() di proyek runes); kembalikan isiannya supaya gak perlu diketik ulang, kecuali password. redirect(303, "/pesan/terima-kasih") setelah sukses mencegah kiriman ganda saat refresh. Form-nya HTML biasa, jadi tetap jalan tanpa JavaScript: itu progressive enhancement. use:enhance dari $app/forms menambah pengiriman lewat fetch tanpa memuat ulang halaman. Beberapa action diberi nama dan ditunjuk dengan action="?/batal", dan gak boleh dicampur dengan default. Latihannya: form login dengan <input name="email"> dan <input name="password">, dan action yang mengembalikan fail(400, { email, error: "Password salah" }) atau redirect(303, "/dashboard").

Yang akan kamu pelajari