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)
Form handler พื้นฐาน
หัวข้อที่มีชื่อว่า “Form handler พื้นฐาน”React ต้องต่อสาย controlled input, เรียก preventDefault, และยิง fetch เอง SvelteKit ให้คุณ export object actions จาก +page.server.js แล้วชี้ attribute action ของ form ไปที่มัน
// React — controlled form with fetchimport { 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> );}<!-- +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 ก็ทำงานแบบเดียวกัน
Progressive enhancement ด้วย use:enhance
หัวข้อที่มีชื่อว่า “Progressive enhancement ด้วย use:enhance”React form ต้องใช้ JavaScript เพื่อทำอะไรก็ตามที่มีประโยชน์ use:enhance ของ SvelteKit อัปเกรด native form ให้ใช้ fetch — ให้ความรู้สึกแบบ SPA พร้อมยังคง fallback แบบไม่มี JS ไว้
// React — onSubmit is the only way to interceptimport { 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> );}<!-- +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 และทำงานได้ตามปกติ
Named actions ใน +page.server.js
หัวข้อที่มีชื่อว่า “Named actions ใน +page.server.js”หน้าเดียวสามารถมีหลาย form action ได้ ตั้งชื่อไว้ใน actions export แล้วอ้างอิงด้วย ?/actionName ใน attribute action ของ form
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>Validation errors และการ repopulate field
หัวข้อที่มีชื่อว่า “Validation errors และการ repopulate field”ใช้ fail(status, data) เพื่อ return validation error พร้อมกับค่าที่ submit มา เพื่อให้ form แสดงค่าเดิมกลับมาได้โดยไม่ต้องเขียนโค้ดเพิ่ม
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 }; },};<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เป็น exportedactionsobject — แต่ละ key คือชื่อ action - Default action:
export const actions = { default: async ({ request }) => { ... } }— ไม่ต้องมี?/name - Named actions ใช้
?/actionNameใน attributeactionของ form use:enhanceจาก$app/formsอัปเกรด form ให้ใช้fetchโดยไม่ทิ้ง fallback แบบไม่มี JS- return
fail(statusCode, data)สำหรับ validation error — ข้อมูลจะไปอยู่ในformprop - ข้อมูลที่ action return กลับมาเข้าถึงได้ในหน้าผ่าน
let { form } = $props() - ต่างจาก React ตรงที่ form ทำงานได้แม้ไม่มี JavaScript เลย — progressive enhancement มีให้ในตัว