svelte-specma
v2.1.0
Published
Svelte store for data validation using Specma
Maintainers
Readme
svelte-specma
Svelte form stores with Specma validation.
This guide focuses on real Svelte component usage, especially $form and $children.
Install
npm install svelte-specmaPeer dependency: svelte >=3 <6
1) Configure once
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).
2) Most common: use a specable store in a Svelte component
specable(...) 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>3) Object form with $form.value
For 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>4) $children usage with Specma spread fields
Most $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:
- Use
form.getChild(["presets"])to access the nested collection store for the spread field. - Then subscribe to its
childrenstore (const children = presetsForm.children) and iterate$children. - For spread arrays, child
ids are random unlessgetIdis configured.
5) Submission and validation flow (important)
submit() behavior:
- if
onSubmitis missing: resolvesundefined - validates first
- if invalid: resolves
false,onSubmitis not called - if valid and
onSubmitsucceeds: resolvestrue - if
onSubmitthrows: error propagates submittingalways returns tofalseafter completion
You can also validate manually:
const valid = await form.activate();
if (!valid) return;6) Quick API you use most in Svelte
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.submitting
7) register action
For 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)
}
]}
/>8) Troubleshooting
TypeError: SvelteSpecma must be configured...configure(specma)was not called before creating forms.
- Validation not showing:
- call
form.activate()orform.submit(), or passtrueas second arg inset(...).
- call
- Need reset after remote save:
- do it in
onSubmit(value, form)withform.reset(savedValue).
- do it in
