ข้ามไปยังเนื้อหา

Form Actions

React form แทบทุกครั้งจัดการด้วย JavaScript: controlled input ป้อนค่าเข้า state แล้วเรียก fetch ตอน submit SvelteKit ใช้แนวทางต่างออกไป — form actions คือ handler ฝั่ง server ที่ทำงานร่วมกับ native HTML form submission ได้เลย ทำให้ form ทำงานได้แม้ไม่มี JavaScript แล้วค่อยเพิ่ม use:enhance เพื่อ layer ประสบการณ์แบบ fetch เข้าไปแบบ progressive

รันตัวอย่างนี้ในโปรเจกต์ SvelteKit (npx sv create)

React ต้องต่อสาย controlled input, เรียก preventDefault, และยิง fetch เอง SvelteKit ให้คุณ export object actions จาก +page.server.js แล้วชี้ attribute action ของ form ไปที่มัน

React
// React — controlled form with fetch
import { useState } from 'react';
export default function CreatePost() {
const [title, setTitle] = useState('');
const [error, setError] = useState(null);
async function handleSubmit(e) {
e.preventDefault();
const res = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title }),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) {
const { message } = await res.json();
setError(message);
} else {
setTitle('');
}
}
return (
<form onSubmit={handleSubmit}>
{error && <p className="error">{error}</p>}
<input
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="Post title"
/>
<button type="submit">Create</button>
</form>
);
}
Svelte
<!-- +page.svelte -->
<script>
let { form } = $props();
</script>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<form method="POST" action="?/create">
<input name="title" placeholder="Post title" />
<button type="submit">Create</button>
</form>
<!-- +page.server.js -->
<!--
import { fail } from '@sveltejs/kit';
export const actions = {
create: async ({ request }) => {
const data = await request.formData();
const title = data.get('title');
if (!title) {
return fail(400, { error: 'Title is required' });
}
await db.post.create({ data: { title } });
},
};
-->

form prop จะถูกเติมค่าอัตโนมัติจากสิ่งที่ action return กลับมา — รวมถึง validation error จาก fail() ด้วย ถ้าไม่มี JavaScript หน้าจะ reload; ถ้ามี JS ก็ทำงานแบบเดียวกัน

React form ต้องใช้ JavaScript เพื่อทำอะไรก็ตามที่มีประโยชน์ use:enhance ของ SvelteKit อัปเกรด native form ให้ใช้ fetch — ให้ความรู้สึกแบบ SPA พร้อมยังคง fallback แบบไม่มี JS ไว้

React
// React — onSubmit is the only way to intercept
import { useState } from 'react';
export default function ContactForm() {
const [status, setStatus] = useState('idle');
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
const formData = new FormData(e.currentTarget);
const res = await fetch('/api/contact', {
method: 'POST',
body: formData,
});
setStatus(res.ok ? 'done' : 'error');
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" placeholder="Email" />
<button type="submit" disabled={status === 'submitting'}>
{status === 'submitting' ? 'Sending…' : 'Send'}
</button>
{status === 'done' && <p>Sent!</p>}
</form>
);
}
Svelte
<!-- +page.svelte -->
<script>
import { enhance } from '$app/forms';
let { form } = $props();
let submitting = $state(false);
</script>
<form
method="POST"
action="?/contact"
use:enhance={() => {
submitting = true;
return async ({ update }) => {
submitting = false;
await update();
};
}}
>
<input name="email" type="email" placeholder="Email" />
<button type="submit" disabled={submitting}>
{submitting ? 'Sending…' : 'Send'}
</button>
{#if form?.success}
<p>Sent!</p>
{/if}
</form>

use:enhance ดักการ submit, ยิง action ผ่าน fetch, และอัปเดต form prop ด้วยข้อมูลที่ return กลับมา — ทั้งหมดโดยไม่ต้อง reload หน้าเต็ม ถ้าไม่มี JavaScript form ก็ยัง submit และทำงานได้ตามปกติ

หน้าเดียวสามารถมีหลาย form action ได้ ตั้งชื่อไว้ใน actions export แล้วอ้างอิงด้วย ?/actionName ใน attribute action ของ form

src/routes/posts/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
// default action — triggered by action="" or no action attribute
default: async ({ request }) => {
const data = await request.formData();
console.log(data.get('query'));
},
// named action — triggered by action="?/create"
create: async ({ request, locals }) => {
const data = await request.formData();
const title = String(data.get('title') ?? '').trim();
if (title.length < 3) {
return fail(400, { error: 'Title must be at least 3 characters', title });
}
await locals.db.post.create({ data: { title } });
redirect(303, '/posts');
},
// another named action — triggered by action="?/delete"
delete: async ({ request, locals }) => {
const data = await request.formData();
const id = data.get('id');
await locals.db.post.delete({ where: { id } });
},
};

ชี้แต่ละ form ไปที่ action ของมัน:

<!-- create form -->
<form method="POST" action="?/create">
<input name="title" />
<button>Create</button>
</form>
<!-- delete form -->
<form method="POST" action="?/delete">
<input type="hidden" name="id" value={post.id} />
<button>Delete</button>
</form>

ใช้ fail(status, data) เพื่อ return validation error พร้อมกับค่าที่ submit มา เพื่อให้ form แสดงค่าเดิมกลับมาได้โดยไม่ต้องเขียนโค้ดเพิ่ม

+page.server.js
import { fail } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email');
if (!email || !email.includes('@')) {
return fail(422, {
email,
errors: { email: 'Please enter a valid email' },
});
}
await subscribe(email);
return { success: true };
},
};
+page.svelte
<script>
let { form } = $props();
</script>
<form method="POST">
<input
name="email"
type="email"
value={form?.email ?? ''}
/>
{#if form?.errors?.email}
<p class="error">{form.errors.email}</p>
{/if}
<button>Subscribe</button>
{#if form?.success}
<p>Subscribed!</p>
{/if}
</form>

SvelteKit repopulate ค่าใน field ให้อัตโนมัติผ่าน form prop เมื่อ validation ล้มเหลว — ไม่ต้องเขียน logic นี้เอง

  • Form actions ถูกกำหนดใน +page.server.js เป็น exported actions object — แต่ละ key คือชื่อ action
  • Default action: export const actions = { default: async ({ request }) => { ... } } — ไม่ต้องมี ?/name
  • Named actions ใช้ ?/actionName ใน attribute action ของ form
  • use:enhance จาก $app/forms อัปเกรด form ให้ใช้ fetch โดยไม่ทิ้ง fallback แบบไม่มี JS
  • return fail(statusCode, data) สำหรับ validation error — ข้อมูลจะไปอยู่ใน form prop
  • ข้อมูลที่ action return กลับมาเข้าถึงได้ในหน้าผ่าน let { form } = $props()
  • ต่างจาก React ตรงที่ form ทำงานได้แม้ไม่มี JavaScript เลย — progressive enhancement มีให้ในตัว
คุณกำหนด SvelteKit form actions ไว้ที่ไหน?
คุณชี้ form ไปหา named action ชื่อ "create" อย่างไร?
use:enhance ทำอะไรกับ SvelteKit form?