Solid — complete checkout example
The same scenario as the README implemented end-to-end in Solid: nested groups, a typed array of line items, a coupon validated server-side (re-checked when the country changes, cancelled while typing), submit with server errors, and a draft that survives page refreshes.
npm install @modyra/solidSolid’s primitives map almost 1:1 onto the engine’s reactive contract
(createSignal → signal, createMemo → computed, createEffect →
effect), so there is no useMdyField-style hook here at all: field
handles are read directly as accessors inside JSX (handle.value()),
and Solid’s compiler wraps each such read in its own fine-grained update
— no whole-component re-render, no subscription wiring to write.
The component
Section titled “The component”import { useSolidForm, array, crossField, field, group, min, pattern, required, serverValidator } from "@modyra/solid";import { OrderApi } from "./order-api";
function TextField(props: { handle: any; label: string }) { return ( <label> {props.label} <input value={props.handle.value() ?? ""} onInput={(e) => props.handle.set(e.target.value)} onBlur={props.handle.markAsTouched} aria-invalid={!props.handle.valid()} /> {props.handle.touched() && props.handle.errors()[0] && ( <p role="alert">{props.handle.errors()[0].message}</p> )} </label> );}
function NumberField(props: { handle: any; label: string }) { return ( <label> {props.label} <input type="number" value={props.handle.value() ?? ""} onInput={(e) => props.handle.set(e.target.valueAsNumber)} onBlur={props.handle.markAsTouched} aria-invalid={!props.handle.valid()} /> {props.handle.touched() && props.handle.errors()[0] && ( <p role="alert">{props.handle.errors()[0].message}</p> )} </label> );}
export function Checkout() { const form = useSolidForm({ country: field("IT"), shipping: group({ city: field("", [required()]), zip: field("", [required(), pattern(/^\d{5}$/, "5 digits")]), }), items: array( group({ sku: field("", [required()]), qty: field<number>(1, [min(1)]), }), { initial: [{ sku: "TSHIRT-BLK-M", qty: 2 }] }, ), coupon: field("", [], serverValidator( async (code, ctx) => { if (!code) return null; const res = await OrderApi.checkCoupon(code, ctx.form.fieldValue("country"), { signal: ctx.signal, // aborted automatically when stale }); return res.valid ? null : "Coupon not valid for your country"; }, { dependsOn: ["country"], debounceMs: 400, timeoutMs: 5000 }, )), }, { validators: [ crossField(["items"], (v) => v.items.length === 0 ? "Add at least one item to the order" : null), ], draft: { key: "checkout-draft", exclude: ["coupon"] }, });
const submit = (e: Event) => { e.preventDefault(); void form.submit(async (order) => { const res = await OrderApi.create(order); if (!res.ok) { return res.errors.map(e => ({ path: e.field, kind: "server", message: e.message })); } }); };
return ( <form onSubmit={submit}> <label> Country <select value={form.f.country.value()} onInput={(e) => form.f.country.set(e.target.value)}> <option value="IT">Italy</option> <option value="DE">Germany</option> <option value="US">United States</option> </select> </label>
<fieldset> <legend>Shipping address</legend> <TextField handle={form.f.shipping.city} label="City" /> <TextField handle={form.f.shipping.zip} label="ZIP" /> </fieldset>
<fieldset> <legend>Items</legend> <For each={form.f.items.rows()}> {(row, i) => ( <div class="item-row"> <TextField handle={row.sku} label={`SKU #${i() + 1}`} /> <NumberField handle={row.qty} label="Qty" /> <button type="button" onClick={() => form.f.items.remove(i())}>✕</button> </div> )} </For> {form.f.items.errors()[0] && ( <p role="alert">{form.f.items.errors()[0].message}</p> )} <button type="button" onClick={() => form.f.items.push({ sku: "", qty: 1 })}> Add item </button> </fieldset>
<label> {form.f.coupon.pending() ? "Coupon (checking…)" : "Coupon"} <input value={form.f.coupon.value()} onInput={(e) => form.f.coupon.set(e.target.value)} onBlur={form.f.coupon.markAsTouched} aria-invalid={!form.f.coupon.valid()} /> </label> {form.f.coupon.errors()[0] && <p role="alert">{form.f.coupon.errors()[0].message}</p>}
<button type="submit" disabled={!form.state.canSubmit()}>Place order</button> <button type="button" onClick={() => form.reset()}>Reset</button> </form> );}(For is Solid’s own keyed-list control-flow component, imported from
solid-js — used here instead of .map() so row removal/insertion stays
fine-grained instead of re-rendering the whole list.)
What to notice
Section titled “What to notice”- No subscription layer to write — every
handle.value()call is a Solid signal read; the compiler tracks it and updates only that DOM node. Where React needsuseMdyField/useSignals, Solid needs nothing. useSolidForm, notcreateSolidForm— theusevariant disposes the form automatically when the enclosing owner (component orcreateRoot) cleans up;createSolidFormis available if you want to manage the lifecycle yourself.- The coupon logic lives in the engine — debounce,
dependsOnre-validation, abort of stale fetches and thependingflag are the same code as every other adapter, running on Solid’s own reactive graph instead of a bridge. - Headless by design —
@modyra/solidships no components: theTextField/NumberFieldwrappers above are the seam where your design system plugs in.
Going further
Section titled “Going further”@modyra/solidpackage README- Multi-framework architecture — how the binding works, and the
--conditions=browsernote for Node/SSR - Typed forms guide — the engine API, framework-free