Forms and binding
data-bind connects an element’s value to a signal in both directions and creates the signal from the element if it does not exist yet.
<div data-signals="{ f: { name: '', age: 30, agree: false, colour: 'teal', picks: [], size: 'm' } }"> <div class="row"><input data-bind:f.name placeholder="name"> <input type="number" data-bind:f.age> <input type="range" min="0" max="100" data-bind:f.age></div> <div class="row"> <label><input type="checkbox" data-bind:f.agree> agree</label> <label><input type="checkbox" value="red" data-bind:f.picks> red</label> <label><input type="checkbox" value="blue" data-bind:f.picks> blue</label> <label><input type="radio" value="s" data-bind:f.size> S</label> <label><input type="radio" value="m" data-bind:f.size> M</label> <select data-bind:f.colour><option>teal</option><option>amber</option></select> </div> <pre data-text="JSON.stringify($f, null, 1)"></pre></div>What each element binds to
Section titled “What each element binds to”| element | signal value | events |
|---|---|---|
| text, email, url, … | string | input |
number, range |
number ('' when empty) |
input |
checkbox without value |
boolean | input |
checkbox with value |
the value or ''; when the signal is an array, membership in it |
input |
| radio | the checked value; the group gets name from the signal path |
input |
select |
string; select multiple gives an array |
change |
file |
array of { name, type, size, contents } with base64 contents |
change |
textarea, custom elements with a value property |
string | input, change |
Numbers stay numbers: if the signal currently holds a number, the bound string is converted back.
Modifiers: __event.blur picks the events to listen for, __case.kebab changes the signal name casing.
Validation
Section titled “Validation”data-custom-validity sets the field’s validity message from an expression; an empty string means valid. The browser then blocks submission and shows the message.
<form data-signals="{ p1: '', p2: '' }" data-on:submit="alert('valid!')"> <div class="row"> <input type="password" placeholder="password" data-bind:p1 required> <input type="password" placeholder="repeat" data-bind:p2 data-custom-validity="$p1 === $p2 ? '' : 'Passwords differ.'"> <button>Submit</button> </div></form>Posting a form
Section titled “Posting a form”With contentType: 'form' the request serialises the closest form (or the one matched by selector) instead of the signals: URL-encoded, or multipart when the form says so, exactly as a browser would submit it, so any server stack reads the fields with what it already has. Native validation runs first. The server validates again and answers with patches; in JavaScript, sigmx/server reads the fields as signals and builds the reply.
<div class="ex contact" data-signals="{ sent: false }"> <form data-on:submit="@post('/api/form', { contentType: 'form' })" data-show="!$sent" data-indicator:sending> <div class="grid"> <label>Email <input class="field" name="email" type="email" placeholder="you@example.com"></label> <label>Message <input class="field" name="message" placeholder="At least five characters"></label> </div> <div id="form-status"></div> <div class="actions"><span class="muted small">Validated on the server; errors come back as a patch.</span><button class="btn" data-attr:disabled="$sending">Send</button></div> </form> <div class="thanks" data-show="$sent" style="display:none"> <span class="big" aria-hidden="true">✓</span> <p>Sent. <button class="btn ghost" type="button" data-on:click="$sent = false">Send another</button></p> </div></div><style is:global> .contact { padding: 1rem; background: var(--ex-surface); } .contact .grid { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; } @media (max-width: 40rem) { .contact .grid { grid-template-columns: 1fr; } } .contact label { display: grid; gap: .25rem; font-size: .85rem; } .contact #form-status { margin: .6rem 0; } .contact #form-status ul { margin: 0; padding: .6rem .9rem .6rem 1.8rem; border-radius: 10px; background: color-mix(in srgb, var(--ex-bad) 12%, transparent); color: var(--ex-bad); font-size: .85rem; } .contact .actions { display: flex; justify-content: space-between; align-items: center; gap: 1rem; } .contact .small { font-size: .78rem; } .contact .thanks { display: grid; justify-items: center; gap: .3rem; padding: .5rem; text-align: center; } .contact .big { display: grid; place-items: center; width: 3rem; height: 3rem; border-radius: 50%; background: color-mix(in srgb, var(--ex-good) 16%, transparent); color: var(--ex-good); font-size: 1.5rem; } .contact .thanks p { margin: 0; display: flex; align-items: center; gap: .6rem; }</style>// any handler that takes a Request and returns a Responseimport { patchElements, patchSignals, readSignals, sse } from 'sigmx/server'
export const POST = async (request: Request) => { try { const { email } = await readSignals(request, schema) // any Standard Schema validator return sse(patchElements(`<p id="form-status">Thanks, ${email}.</p>`), patchSignals({ sent: true })) } catch (e) { const items = e.issues.map((i) => `<li>${i.message}</li>`).join('') return sse(patchElements(`<div id="form-status" class="error"><ul>${items}</ul></div>`)) }}