Svelte form stores with Specma validation.
This guide focuses on real Svelte component usage, especially $form and $children.
npm install svelte-specma
Peer dependency: svelte >=3 <6
Create one setup module and call configure(...) once at app startup.
// src/lib/specma-setup.js
import * as specma from "specma";
import { configure } from "svelte-specma";
configure(specma);
Import this file once (for example in root layout or main entrypoint).
specable store in a Svelte componentspecable(...) returns a Svelte store. Use $form in markup.
<script>
import "./specma-setup";
import { specable, register } from "svelte-specma";
const form = specable("", {
required: true,
spec: (v) => /.+@.+/.test(v) || "Invalid email",
onSubmit: async (value, form) => {
const saved = await fakeApiSave(value);
form.reset(saved);
}
});
async function submit() {
try {
await form.submit();
} catch (e) {
console.error(e);
}
}
function fakeApiSave(v) {
return new Promise((resolve) => setTimeout(() => resolve(v), 300));
}
</script>
<form on:submit|preventDefault={submit}>
<input use:register={form} />
{#if $form.active && $form.error}
<p>{$form.error}</p>
{/if}
<button disabled={$form.submitting || $form.validating}>
{$form.submitting ? "Saving..." : "Save"}
</button>
</form>
$form.valueFor object initial values, specable(...) creates a collection store.
<script>
import "./specma-setup";
import { specable } from "svelte-specma";
const form = specable(
{ name: "", age: 0 },
{
spec: {
name: (v) => !!v || "Name is required",
age: (v) => v >= 18 || "Must be 18+"
},
onSubmit: async (value, form) => {
const saved = await fakeApiSave(value);
form.reset(saved);
}
}
);
function onNameInput(e) {
form.set({ name: e.target.value }, true);
}
function onAgeInput(e) {
form.set({ age: Number(e.target.value) }, true);
}
async function save() {
await form.submit();
}
function fakeApiSave(v) {
return new Promise((resolve) => setTimeout(() => resolve(v), 300));
}
</script>
<input value={$form.value?.name || ""} on:input={onNameInput} />
<input value={$form.value?.age || 0} on:input={onAgeInput} />
{#if $form.active && $form.errors.length}
<ul>
{#each $form.errors as err}
<li>{err.which}: {err.error}</li>
{/each}
</ul>
{/if}
<button on:click={save} disabled={$form.submitting}>Save</button>
$children usage with Specma spread fieldsMost $children usage happens on a declared spread field (for example presets).
<script>
import "./specma-setup";
import { spread } from "specma";
import { specable } from "svelte-specma";
import { assocPresetsIds } from "./assocPresetsIds";
const form = specable(
assocPresetsIds(consumable),
{
fields: {
allowInput: 1,
groupId: 1,
key: 1,
name: 1,
presets: spread({ name: 1, quantity: 1 })
},
required,
spec,
onSubmit(values) {
// ...someAction
}
}
);
const presetsForm = form.getChild(["presets"]);
const children = presetsForm ? presetsForm.children : null;
function addPreset() {
const next = [...($form.value?.presets || []), { name: "", quantity: 1 }];
form.set({ ...$form.value, presets: next });
}
function updatePreset(index, patch) {
const next = [...($form.value?.presets || [])];
next[index] = { ...next[index], ...patch };
form.set({ ...$form.value, presets: next }, true);
}
function removePreset(index) {
const next = ($form.value?.presets || []).filter((_, i) => i !== index);
form.set({ ...$form.value, presets: next });
}
</script>
{#if children && Array.isArray($children)}
{#each $children as presetChild, index}
<div>
<input
value={$form.value?.presets?.[index]?.name || ""}
on:input={(e) => updatePreset(index, { name: e.target.value })}
/>
<input
type="number"
value={$form.value?.presets?.[index]?.quantity || 1}
on:input={(e) => updatePreset(index, { quantity: Number(e.target.value) })}
/>
{#if presetChild.active && presetChild.error}
<small>{presetChild.error}</small>
{/if}
<button on:click={() => removePreset(index)}>Remove</button>
</div>
{/each}
{/if}
<button on:click={addPreset}>Add preset</button>
Notes:
form.getChild(["presets"]) to access the nested collection store for the spread field.children store (const children = presetsForm.children) and iterate $children.ids are random unless getId is configured.submit() behavior:
onSubmit is missing: resolves undefinedfalse, onSubmit is not calledonSubmit succeeds: resolves trueonSubmit throws: error propagatessubmitting always returns to false after completionYou can also validate manually:
const valid = await form.activate();
if (!valid) return;
form.set(value, shouldActivate = false)form.reset(newInitialValue?)await form.activate(bool = true)await form.submit()form.children (store of child stores)$form.value, $form.error, $form.errors, $form.valid, $form.submittingregister actionFor single-field binding:
<script>
import { register } from "svelte-specma";
export let form;
</script>
<input use:register={form} />
With transforms:
<input
use:register={[
form,
{
toInput: (v) => String(v ?? ""),
toValue: (raw) => Number(raw)
}
]}
/>
TypeError: SvelteSpecma must be configured...configure(specma) was not called before creating forms.form.activate() or form.submit(), or pass true as second arg in set(...).onSubmit(value, form) with form.reset(savedValue).