svelte-specma
Version:
Svelte store for data validation using Specma
292 lines (229 loc) • 7.13 kB
Markdown
# svelte-specma
Svelte form stores with Specma validation.
This guide focuses on real Svelte component usage, especially `$form` and `$children`.
## Install
```bash
npm install svelte-specma
```
Peer dependency: `svelte >=3 <6`
## 1) Configure once
Create one setup module and call `configure(...)` once at app startup.
```js
// 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.
```svelte
<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.
```svelte
<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`).
```svelte
<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 `children` store (`const children = presetsForm.children`) and iterate `$children`.
- For spread arrays, child `id`s are random unless `getId` is configured.
## 5) Submission and validation flow (important)
`submit()` behavior:
- if `onSubmit` is missing: resolves `undefined`
- validates first
- if invalid: resolves `false`, `onSubmit` is not called
- if valid and `onSubmit` succeeds: resolves `true`
- if `onSubmit` throws: error propagates
- `submitting` always returns to `false` after completion
You can also validate manually:
```js
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:
```svelte
<script>
import { register } from "svelte-specma";
export let form;
</script>
<input use:register={form} />
```
With transforms:
```svelte
<input
use:register={[
form,
{
toInput: (v) => String(v ?? ""),
toValue: (raw) => Number(raw)
}
]}
/>
```
After the initial synchronization, `toInput` also receives the element's current
input. It may return that input unchanged when it is another valid representation
of the store value. This is useful for preserving intermediate numeric input such
as `"1.0"` when its converted value is already `1`:
```svelte
<input
use:register={[
form,
{
toInput: (value, currentInput) =>
currentInput !== undefined && Number(currentInput) === value
? currentInput
: String(value ?? ""),
toValue: (raw) => raw === "" ? null : Number(raw)
}
]}
/>
```
`currentInput` is omitted when the action first subscribes, including after a
change of store, so the new store value is rendered canonically.
## 8) Troubleshooting
- `TypeError: SvelteSpecma must be configured...`
- `configure(specma)` was not called before creating forms.
- Validation not showing:
- call `form.activate()` or `form.submit()`, or pass `true` as second arg in `set(...)`.
- Need reset after remote save:
- do it in `onSubmit(value, form)` with `form.reset(savedValue)`.