Forms & Inputs
controlled input ด้วย bind:
หัวข้อที่มีชื่อว่า “controlled input ด้วย bind:”form รวม binding, runes และ event เข้าด้วยกัน แต่ละ field bind กับ state; validation derive จาก state นั้น; submit อ่านค่าจาก state เดียวกัน นี่คือรูปแบบของ form จริง:
<script> let email = $state(''); let password = $state('');
// Validation is DERIVED from the fields — never stored separately. let emailValid = $derived(/^[^@]+@[^@]+\.[^@]+$/.test(email)); let passwordValid = $derived(password.length >= 8); let formValid = $derived(emailValid && passwordValid);</script>
<input type="email" bind:value={email} placeholder="Email" />{#if email && !emailValid}<small>Enter a valid email</small>{/if}
<input type="password" bind:value={password} placeholder="Password" />{#if password && !passwordValid}<small>At least 8 characters</small>{/if}ไอเดียสำคัญ: validation ถูก derive ไม่ใช่เก็บ emailValid คำนวณใหม่อัตโนมัติทุกครั้งที่ email เปลี่ยน — ไม่มีขั้นตอน “revalidate” ให้ลืม นี่คือหลัก “derive อย่า sync” จากบท effect มาใช้กับ form
bind:value coerce ค่าให้ number กับ range input
หัวข้อที่มีชื่อว่า “bind:value coerce ค่าให้ number กับ range input”สำหรับ input type="number" และ type="range" bind:value จะไม่ส่ง string มาให้ — Svelte coerce มันให้เป็น number แทน:
<script lang="ts"> let age: number = $state(0);</script><input type="number" bind:value={age} min="0" max="120" /><p data-testid="age">{age}</p>import { render, fireEvent } from '@testing-library/svelte';import { expect, test } from 'vitest';import NumericBinding from './NumericBinding.svelte';
test('a number input coerces the bound value to a number, not a string', async () => { const { getByRole, getByTestId } = render(NumericBinding); const input = getByRole('spinbutton') as HTMLInputElement; await fireEvent.input(input, { target: { value: '42' } }); expect(getByTestId('age').textContent).toBe('42');});✓ a number input coerces the bound value to a number, not a string
Test Files 1 passed (1) Tests 1 passed (1)เรื่องนี้สำคัญกับอะไรก็ตามที่ต่อจากนั้นแล้วคาดว่าจะได้ number จริง ๆ (arithmetic, comparison) — ไม่ต้อง Number(e.currentTarget.value) เองเหมือน oninput handler ที่เขียนเองบน text input
bind:checked และ bind:files
หัวข้อที่มีชื่อว่า “bind:checked และ bind:files”checkbox bind ค่า checked แบบ boolean ด้วย bind:checked; file input เปิดให้เข้าถึง FileList ที่เลือกด้วย bind:files:
<script lang="ts"> let accepted = $state(false); let files: FileList | undefined = $state();</script><input type="checkbox" bind:checked={accepted} data-testid="accept" /><p data-testid="accepted-value">{accepted}</p><input type="file" bind:files data-testid="upload" /><p data-testid="file-count">{files?.length ?? 0}</p>import { render, fireEvent } from '@testing-library/svelte';import { expect, test } from 'vitest';import CheckedAndFiles from './CheckedAndFiles.svelte';
test('bind:checked syncs the checkbox to state', async () => { const { getByTestId } = render(CheckedAndFiles); const checkbox = getByTestId('accept') as HTMLInputElement; await fireEvent.click(checkbox); expect(getByTestId('accepted-value').textContent).toBe('true');});✓ bind:checked syncs the checkbox to state
Test Files 1 passed (1) Tests 1 passed (1)bind:files เป็น read-only ในความหมายสำคัญข้อหนึ่ง: คุณอ่าน FileList ที่ user เลือกได้ แต่ assign array อะไรก็ได้เข้าไปตรง ๆ ไม่ได้ — docs บอกชัดว่า “การอัปเดตแบบ programmatic ต้องใช้ object DataTransfer” (files = new DataTransfer().files คือวิธี เคลียร์ file input; null/undefined ใช้ไม่ได้) เพราะ FileList เก็บได้แค่ object File จริงที่มาจาก user หรือสร้างผ่าน DataTransfer เท่านั้น
การจัดการ submit
หัวข้อที่มีชื่อว่า “การจัดการ submit”ผูก onsubmit เข้ากับ <form> และเรียก e.preventDefault() เพื่อหยุด navigation ปกติของ browser แล้วทำงานกับ state ที่รวบรวมมา:
<script> let submitting = $state(false);
async function handleSubmit(e) { e.preventDefault(); // Svelte 5 has no on:submit|preventDefault modifier if (!formValid) return; submitting = true; await createAccount({ email, password }); submitting = false; }</script>
<form onsubmit={handleSubmit}> <!-- fields… --> <button type="submit" disabled={!formValid || submitting}> {submitting ? 'Creating…' : 'Sign up'} </button></form>สังเกตว่า button ที่ disabled derive จาก formValid และ submitting — UI ถูกต้องเสมอโดยไม่ต้อง toggle เอง
ไม่มี event modifier ใน runes mode — onsubmit|preventDefault เป็น error จริง
หัวข้อที่มีชื่อว่า “ไม่มี event modifier ใน runes mode — onsubmit|preventDefault เป็น error จริง”syntax modifier on:submit|preventDefault ของ Svelte 4 เจาะจงกับ on: directive แบบเดิม — มันไม่มีอยู่สำหรับ attribute onsubmit แบบสมัยใหม่ เขียนแบบนั้นไม่ใช่ no-op เงียบ ๆ — มันคือ compile error ที่ถูกจับได้ เพราะ onsubmit|preventDefault ถูก parse เป็น attribute name เดียวที่ไม่ถูกต้อง:
<!-- @expect-error 'onsubmit|preventDefault' is not a valid attribute name --><script lang="ts"> function handleSubmit(e: SubmitEvent) { console.log(e); }</script>
<form onsubmit|preventDefault={handleSubmit}> <button type="submit">Go</button></form>$ svelte-check --tsconfig ./tsconfig.json'onsubmit|preventDefault' is not a valid attribute namehttps://svelte.dev/e/attribute_invalid_nameวิธีแก้คือสิ่งที่บทนี้ทำอยู่แล้ว: เรียก e.preventDefault() เป็นบรรทัดแรกข้างใน handler แทนที่จะหยิบ modifier ที่ไม่มีอยู่แล้วมาใช้:
<script lang="ts"> function handleSubmit(e: SubmitEvent) { e.preventDefault(); console.log('submitted'); }</script>
<form onsubmit={handleSubmit}> <button type="submit">Go</button></form>flowchart LR input["inputs (bind:value)"] --> state["field state"] state --> valid["$derived validation"] valid --> button["submit button enable/disable"] button --> submit["onsubmit → preventDefault → ทำงาน"]
client-side เป็นแค่ครึ่งเรื่อง
หัวข้อที่มีชื่อว่า “client-side เป็นแค่ครึ่งเรื่อง”บทนี้จัดการ form ทั้งหมดบน client — โอเคสำหรับ local state และเคสง่าย ๆ แต่แอปจริงต้องการข้อมูลบน server: การ persist, auth, validation ที่เชื่อถือได้ นั่นคือสิ่งที่ SvelteKit form actions (โมดูลถัดไป) ให้มา — form แบบ progressive-enhancement ที่ทำงานทั้งมีและไม่มี JavaScript validate บน server client-side bind: + $derived ให้ UX ที่ตอบสนอง; SvelteKit ให้ pipeline แบบ full-stack
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”form คือ pipeline ไม่ใช่ถุงของ input ที่แยกจากกัน: bind: เอา raw DOM state เข้ามาเป็น reactive state ของ Svelte (พร้อม type ที่ถูกต้อง — number สำหรับ number input, boolean สำหรับ checkbox, FileList สำหรับ file input); $derived เปลี่ยน raw state นั้นให้เป็นข้อสรุปเกี่ยวกับมัน (valid/invalid, submit ได้ไหม); onsubmit คือจุดเดียวที่ side effect เกิดขึ้น และมันรันหลังจากทุกอย่างข้างบนคำนวณใหม่เสร็จแล้ว การอ่าน pipeline แบบนี้อธิบายว่าทำไม “derive ไม่ใช่เก็บ” ถึงสำคัญ: ถ้า emailValid เป็น $state เองแล้ว update ด้วยมือ มันอาจไม่ตรงกับ email ที่มันควรอธิบาย — bug คลาสฯ เดียวกับที่บท effect อธิบายเรื่อง state ที่ mirror ผ่าน $effect แทนที่จะคำนวณผ่าน $derived
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
bind:value coercion (number/range) | ไม่ต้อง Number(...) เอง; โค้ดที่ต่อไปได้ number จริง | ใช้ได้แค่กับ input number/range — text input ให้ string เสมอ |
derived validation ($derived) | ไม่มีทางไม่ตรงกับ field ที่มันอธิบายได้เลย | คำนวณใหม่ทุก keystroke ที่เกี่ยวข้อง — โอเคสำหรับ predicate ง่าย ๆ แต่ไม่เหมาะกับ check async ที่แพง (ต้อง debounce แยกต่างหาก) |
e.preventDefault() ข้างใน handler | ทำงานเหมือนกันทุก event type ไม่ต้องเรียนรู้ syntax modifier แยก | เพิ่มบรรทัดที่ต้องเขียนบนสุดของทุก handler ที่ต้องการ เทียบกับ modifier แบบ declarative |
| form แบบ client-side ล้วน (บทนี้) | feedback เร็ว ไม่ต้อง round trip สำหรับ validation UX | ไม่น่าเชื่อถือสำหรับ persistence หรือ security — client ถูก bypass ได้เสมอ; ต้องมีเส้นทางที่ validate บน server (SvelteKit form actions) สำหรับอะไรที่สำคัญ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
on:submit|preventDefaultยังใช้ได้ใน Svelte 5 แค่ไม่แนะนำ” ไม่จริง — มันไม่ compile เลยบน attributeonsubmitสมัยใหม่ — พิสูจน์ด้านบน:svelte-checkรายงาน'onsubmit|preventDefault' is not a valid attribute namesyntax|modifierมีอยู่แค่สำหรับon:directive แบบเดิมเท่านั้น - “ค่าที่ bind ของ number input เป็น string เหมือน input อื่น” เฉพาะ
type="number"/type="range"เท่านั้นที่ Svelte coerce ให้ — พิสูจน์ด้านบนด้วย test coercion จริง text/email/password input ยังคงให้ string - “เก็บ
emailValidใน$stateของตัวเองแล้ว update ในoninputhandler ก็เหมือนกับ derive มันโดยพื้นฐาน” ไม่จริง — สำเนาที่เก็บและ update เองอาจถูกลืมใน code path หนึ่งแล้วเก่าไปแบบเงียบ ๆ ในขณะที่ค่า$derivedไม่มีทางไม่ตรงกับ input ของมันได้เลย - “ต้องใช้
use:action หรือ library แยกเพื่อ validate form ใน Svelte” สำหรับ validation ระดับ field แบบ synchronous$derivedธรรมดาก็พอแล้วส่วนใหญ่ — หยิบเครื่องมือเพิ่มมาใช้ก็ต่อเมื่อต้องการ validation แบบ async ข้าม field (เช่น “email นี้ถูกใช้แล้วหรือยัง?”) ที่ derived expression ธรรมดาแสดงออกไม่ได้
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ form ที่ generate มาใช้ bind:/onsubmit ให้ตรวจสิ่งเหล่านี้ก่อนเชื่อมัน:
onsubmit={handler}(หรือ modern event attribute อื่น) มี suffix|modifierที่ copy มาจากนิสัย Svelte 4 หรือเปล่า — เป็น compile error จริงที่ถูกจับได้ ไม่ใช่แค่ warning- validation state ถูกเก็บใน
$stateของตัวเองแล้ว sync กับ field ด้วยมือ แทนที่จะคำนวณด้วย$derivedหรือเปล่า? - ค่าที่ bind ของ number/range input ถูกส่งผ่าน
Number(...)/parseIntซ้ำอีกที โดยสมมติว่ามันยังเป็น string อยู่หรือเปล่า? - client-side validation ถูกปฏิบัติเหมือนเป็น validation เดียวที่มี — ไม่มีการ re-check บน server สำหรับอะไรที่แตะ persistence, auth หรือเงินหรือเปล่า?
- submit handler ลืม
e.preventDefault()ไปเลย โดยพึ่งนิสัยเดิมon:submit|preventDefaultที่ใช้ไม่ได้แล้วหรือเปล่า?
🐛 หา bug ในโค้ดนี้
<script>let email = $state('');let password = $state('');let emailValid = $derived(/^[^@]+@[^@]+\.[^@]+$/.test(email));let passwordValid = $derived(password.length >= 8);
function handleSubmit(e) { if (!emailValid || !passwordValid) return; createAccount({ email, password });}</script>
<form onsubmit|preventDefault={handleSubmit}><input type="email" bind:value={email} /><input type="password" bind:value={password} /><button type="submit">Sign up</button></form>เฉลย
นี่คือกล้ามเนื้อความจำแบบ Svelte 4: on:submit|preventDefault เคยเป็นวิธีหยุด navigation ปกติของ browser ใน Svelte 5’s runes mode onsubmit เป็น attribute ธรรมดา ไม่ใช่ on: directive — modifier ไม่มีอยู่สำหรับมัน และ onsubmit|preventDefault ไม่ได้ถูกเมินเฉย ๆ มันคือ error ที่ถูกจับได้:
'onsubmit|preventDefault' is not a valid attribute namehttps://svelte.dev/e/attribute_invalid_nameวิธีแก้คือเรียก e.preventDefault() เป็นบรรทัดธรรมดาข้างใน handler ซึ่งหมายความว่า handler ตอนนี้ต้องการ event parameter ด้วย:
<script> function handleSubmit(e) { e.preventDefault(); if (!emailValid || !passwordValid) return; createAccount({ email, password }); }</script><form onsubmit={handleSubmit}>