Bindings
two-way binding เป็น sugar ของ value + event
หัวข้อที่มีชื่อว่า “two-way binding เป็น sugar ของ value + event”ปกติข้อมูลไหลทางเดียว: state → DOM binding เพิ่ม edge ย้อนกลับ เพื่อให้ form element เขียนกลับไปที่ state ด้วย bind:value เป็น shorthand ของ “set value ของ input จาก state นี้ และ update state นี้ตอน input”
<script> let name = $state('');</script>
<input bind:value={name} /><p>Hello {name}</p>
<!-- Equivalent, written by hand:<input value={name} oninput={(e) => name = e.currentTarget.value} /> -->binding sync ทั้งสองอัตโนมัติ และใช้ได้กับ element ที่เป็น input-like ทุกตัว: bind:value สำหรับ text/number/select, bind:checked สำหรับ checkbox, bind:files สำหรับ file input
Groups: bind:group
หัวข้อที่มีชื่อว่า “Groups: bind:group”radio button และ checkbox group ที่แชร์ state ก้อนเดียวใช้ bind:group — Svelte wire ทั้งชุดเข้ากับ variable ตัวเดียว
<script> let flavor = $state('vanilla'); // radios: a single value let toppings = $state([]); // checkboxes: an array</script>
<label><input type="radio" bind:group={flavor} value="vanilla" /> Vanilla</label><label><input type="radio" bind:group={flavor} value="chocolate" /> Chocolate</label>
<label><input type="checkbox" bind:group={toppings} value="nuts" /> Nuts</label><label><input type="checkbox" bind:group={toppings} value="cherry" /> Cherry</label>สำหรับ radio, flavor เก็บค่าที่เลือก; สำหรับ checkbox, toppings เป็น array ของค่าที่ check
bind:this สำหรับ reference ของ element และ component
หัวข้อที่มีชื่อว่า “bind:this สำหรับ reference ของ element และ component”เพื่อได้ reference ของ DOM node จริง (หรือ component instance) ใช้ bind:this ตัวแปรจะถูก set หลัง element mount — อ่านค่าใน effect หรือ event handler ไม่ใช่ตอน initial render
<script> let canvas = $state(); // will hold the DOM node
$effect(() => { const ctx = canvas.getContext('2d'); // safe: runs after mount ctx.fillRect(0, 0, 50, 50); });</script>
<canvas bind:this={canvas} width="200" height="200"></canvas>$bindable() บังคับก่อน parent จะ bind: กับ prop ได้
หัวข้อที่มีชื่อว่า “$bindable() บังคับก่อน parent จะ bind: กับ prop ได้”parent จะ bind: กับ prop ของ child component ได้ก็ต่อเมื่อ child เปิดให้ prop นั้น ๆ ด้วย $bindable() (มีมาตั้งแต่ rune $props() เอง) ถ้าไม่มี bind: กับ prop นั้นจะเป็น error จริงที่ถูกจับได้ — ไม่ใช่ no-op เงียบ ๆ:
<script lang="ts"> let { value }: { value: string } = $props();</script><input value={value} /><!-- @expect-error bind: to a prop the child never declared $bindable() --><script lang="ts"> import BindableChild from './BindableChild.svelte'; let text = $state('hi');</script><BindableChild bind:value={text} />svelte-check จับกรณีนี้ที่ระดับ type พร้อมข้อความที่บอกวิธีแก้ตรง ๆ:
$ svelte-check --tsconfig ./tsconfig.jsonsrc/lib/BindableParent.svelte:4:15Cannot use 'bind:' with this property. It is declared as non-bindable insidethe component.To mark a property as bindable: 'let { value = $bindable() } = $props()'วิธีแก้ก็ตรงตามข้อความเป๊ะ — ประกาศ prop ด้วย $bindable() ที่ child แล้ว bind: เดิมบน parent จะ compile ผ่านสะอาด:
<script lang="ts"> let { value = $bindable() }: { value: string } = $props();</script><input bind:value={value} /><script lang="ts"> import BindableChildFixed from './BindableChildFixed.svelte'; let text = $state('hi');</script><BindableChildFixed bind:value={text} />data flow สองทางข้าม component boundary นั้นทรงพลังแต่ทำให้ data flow ดูคลุมเครือถ้าใช้เยอะเกิน — props-down/events-up มักชัดกว่าสำหรับอะไรที่เกินกว่า form control ง่าย ๆ นี่คือเหตุผลที่ $bindable() เป็น opt-in ต่อ prop ไม่ใช่ default
Function binding: bind:value={get, set}
หัวข้อที่มีชื่อว่า “Function binding: bind:value={get, set}”ตั้งแต่ Svelte 5.9 binding รับคู่ function {get, set} แทน identifier ธรรมดาได้ ทำให้ validate หรือ transform ค่าตอนมันไหลเข้ามาได้:
<script lang="ts"> let value = $state('');</script><input bind:value={() => value, (v) => (value = v.toUpperCase())} /><p data-testid="value">{value}</p>การพิมพ์ในช่องนี้ทุกตัวอักษรจะผ่าน function set ก่อนตกลงไปเป็น state — และเพราะ DOM value ถูกขับด้วย function get ที่อ่าน state ก้อนเดียวกัน input จะสะท้อนค่าที่ transform แล้วกลับมาทันที:
import { render, fireEvent } from '@testing-library/svelte';import { expect, test } from 'vitest';import FunctionBinding from './FunctionBinding.svelte';
test('a function binding transforms the value on the way in, and the transformed value reflects back into the input', async () => { const { getByRole, getByTestId } = render(FunctionBinding); const input = getByRole('textbox') as HTMLInputElement; await fireEvent.input(input, { target: { value: 'hello' } }); expect(getByTestId('value').textContent).toBe('HELLO'); expect(input.value).toBe('HELLO');});✓ a function binding transforms the value on the way in, and the transformed value reflects back into the input
Test Files 1 passed (1) Tests 1 passed (1)นี่คือกลไกเดียวกับที่ $bindable() เองสร้างขึ้นมาข้างใน และเป็นวิธีที่แนวคิด “derive อย่าไป sync” จากโมดูล reactivity ขยายมาถึง binding: แทนที่จะเป็นกระจกสองทางธรรมดา คุณได้ประตูที่ควบคุมได้ ซึ่งค่าต้องผ่านทั้งสองทิศทาง
bind: กับ component เทียบกับ element
หัวข้อที่มีชื่อว่า “bind: กับ component เทียบกับ element”บน DOM element ธรรมดา bind:value (และเพื่อน ๆ) ถูกฝังไว้ใน Svelte เอง — input-like element ทุกตัวรองรับโดยไม่ต้อง setup อะไรเลย บน component bind: ใช้ได้แค่กับ prop ที่ component ประกาศเป็น $bindable() ตรง ๆ เท่านั้น ไม่มีพื้นผิว binding แบบ implicit เหมือนที่ <input> มี ความไม่สมมาตรนี้ตั้งใจให้เป็นแบบนั้น: ผู้เขียน component เป็นคนตัดสินว่า prop ไหนปลอดภัยให้ผู้เรียก mutate ได้ เหมือนที่ library ตัดสินว่า field ไหนใน public API ของมัน read-write หรือ read-only
flowchart TB
el["DOM element (input, select, ...)"] --> builtin["bind:value/checked/group/this — built in, no opt-in needed"]
comp["Component"] --> check{"prop declared\nwith $bindable()?"}
check -->|"yes"| ok["bind: works — two-way flow across the boundary"]
check -->|"no"| err["bind: fails svelte-check:\nCannot use 'bind:' with this property"] Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”binding คือ สัญญาที่มีทางเลือกให้ปิด ไม่ใช่ default บน element เบราว์เซอร์เปิดพื้นผิวที่ two-way ได้อยู่แล้ว (value, checked, DOM node เอง) Svelte แค่ wire มันเข้าด้วยกัน บน component ไม่มีพื้นผิวแบบนั้นจนกว่าผู้เขียนจะสร้างมันขึ้นมา — $bindable() คือการสร้างนั้น ทีละ prop มองทุก $bindable() เป็นการตัดสินใจ API เล็ก ๆ ว่า “ผู้เรียกสามารถเอื้อมเข้ามาที่ state ของฉันตรงนี้ได้” นี่คือเหตุผลที่ prop ธรรมดาที่ไม่ bindable ยังคงเป็นทางเดียว (props-down/events-up) เป็น default ที่สมเหตุสมผล ไม่ใช่การมองข้าม
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
| prop ทางเดียวธรรมดา | data flow ง่าย คาดเดาได้ — parent re-render เป็นทางเดียวที่ prop จะเปลี่ยน | child ต้อง emit event/callback เพื่อมีผลต่อ parent |
prop แบบ $bindable() | parent กับ child แชร์ state ก้อนเดียวโดยไม่ต้อง wire event | ผู้เรียกใดก็ mutate state ของ parent ผ่าน child ได้ — สืบที่มาของการเปลี่ยนแปลงยากขึ้น |
function binding bind:value={get, set} | validation/transformation อยู่ที่จุด binding เอง ไม่กระจายไปตาม oninput handler | อ่านที่จุดเรียกยากกว่า bind:value={x} ธรรมดา |
bind:this | เข้าถึง DOM node หรือ component instance ตรง ๆ พร้อม type | มีให้ใช้แค่หลัง mount — อ่านตอน render เป็น bug เหมือน escape hatch แบบ ref ทั่วไป |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “prop ของ component ทุกตัว
bind:ได้ เหมือนvalueของ element” ไม่จริง — ใช้ได้แค่ prop ที่ component เปิดให้ด้วย$bindable()เท่านั้น พิสูจน์ด้านบน:svelte-checkปฏิเสธbind:กับ prop ธรรมดาพร้อมข้อความบอกวิธีแก้ตรง ๆ - “
setของ function binding run แค่ครั้งเดียว ตอนค่าเริ่มต้น” มัน run ทุกครั้งที่เขียน — ทุกตัวอักษรที่พิมพ์ในตัวอย่าง input ด้านบน — เหมือนกับ setter โดยนัยของbind:valueธรรมดา - “
bind:thisมีให้ใช้ทันที เพราะประกาศติดกับ element เลย” มันเป็นundefined/ว่างจนกว่าจะ mount เสร็จ; อ่านมันตอน initial render ของ component (ไม่ใช่ใน effect หรือ handler) จะไม่ได้อะไรเลย - “component binding กับ props-down/events-up ใช้แทนกันได้ เลือกอันที่พิมพ์สั้นกว่า” มันมี blast radius ต่างกัน: prop ที่ bindable ให้ child mutate state ของ parent ได้ตรง ๆ ซึ่งสืบที่มาได้ยากกว่า callback prop ที่ชัดเจนเมื่อมีอะไรเปลี่ยนโดยไม่คาดคิด
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ component ที่ generate มาใช้ bind: ให้ตรวจสิ่งเหล่านี้ก่อนเชื่อมัน:
- template ของ component ทำ
<Child bind:value={x} />โดยที่ child ไม่เคยประกาศvalue = $bindable()ใน$props()ของตัวเองหรือเปล่า? - reference จาก
bind:thisถูกอ่านใน render body (ไม่ใช่ใน$effect/event handler) ซึ่งจะเห็นว่ามันยังว่างอยู่หรือเปล่า? - function binding (
bind:value={get, set}) แทนที่bind:valueธรรมดาบวก$effectแยกที่คอย “แก้” ค่าทีหลังได้ไหม? - prop ของ component ถูก mark
$bindable()ทั้งที่ component ไม่ได้ต้องการให้ parent สังเกตการเขียนจริง ๆ — เปิดพื้นผิว two-way ที่กว้างเกินความจำเป็นหรือเปล่า? - สำหรับ
bind:groupinput radio/checkbox เหล่านั้นอยู่ใน component instance เดียวกัน จริงหรือเปล่า —bind:groupใช้ข้าม component boundary ไม่ได้
🐛 หา bug ในโค้ดนี้
<script>let { open } = $props();</script><button onclick={() => (open = !open)}>{open ? 'Close' : 'Open'}</button>
<!-- Parent.svelte --><script>import Child from './Child.svelte';let panelOpen = $state(false);</script><Child bind:open={panelOpen} />{#if panelOpen}<p>Panel content</p>{/if}เฉลย
Child ไม่เคยประกาศ open เป็น $bindable() — มันเป็น prop ธรรมดาที่ destructure มา svelte-check จับกรณีนี้ที่ระดับ type ก่อนที่มันจะได้รันเลยด้วยซ้ำ:
Cannot use 'bind:' with this property. It is declared as non-bindable insidethe component.To mark a property as bindable: 'let { open = $bindable() } = $props()'วิธีแก้ตรงตามข้อความเป๊ะ:
<script> let { open = $bindable() } = $props();</script>จนกว่าจะเพิ่มสิ่งนั้น นี่คือ error ที่ถูกจับตอน compile ไม่ใช่ bug เงียบ ๆ — แต่เหตุผลข้างหลังสำคัญสำหรับการอ่านโค้ดที่ AI เขียนซึ่งผสมทั้งสอง pattern ไม่สม่ำเสมอกันทั่วโค้ดเบส: bind: บน component ใช้ได้เฉพาะ prop ที่ผู้เขียน component เอง เปิดไว้ตั้งใจเท่านั้น ไม่ใช่สิ่งที่ผู้เรียกบังคับจากภายนอกได้