Skip to content

Commit e16f842

Browse files
committed
add preprocess overhaul docs
1 parent 1be3f0b commit e16f842

File tree

6 files changed

+276
-51
lines changed

6 files changed

+276
-51
lines changed

README.md

Lines changed: 264 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
Svelte-Command-Form allows you to have easy to use forms with commands instead of remote forms. Is this redundant? Maybe. However, you may not want to use an HTML form everytime. The API is greatly influenced by SvelteKit-Superforms, so if you are used to that you shouldn't have a problem here.
44

5+
Whenever possible you should use the SvelteKit provided `form` remote function since commands will fail in non-JS environments, but there may be cases where that is not practical or you just like the ease of interacting with an object instead of form data.
6+
57
## Features
68

7-
- **Schema-agnostic validation** – Works with any library that implements the Standard Schema v1 interface (Zod, Valibot, TypeBox, custom validators, …).
8-
- **Command-first workflow** – Wire forms directly to your remote command ([`command` from `$app/server`](https://kit.svelte.dev/docs/load#command-functions)), and let the helper manage submission, success, and error hooks.
9+
- **Schema-agnostic validation** – Works with any library that implements the [Standard Schema V1]("https://standardschema.dev/") interface. If you are unsure if your schema validation library is compatible see the list of [compatible libraries](https://standardschema.dev/#what-schema-libraries-implement-the-spec).
10+
- **Command-first workflow** – Wire forms directly to your remote command ([`command` from `$app/server`](https://svelte.dev/docs/kit/remote-functions#command)), and let the helper manage submission, success, and error hooks.
911
- **Typed form state**`form`, `errors`, and `issues` are all strongly typed from your schema, so your component code stays in sync with validation rules.
1012
- **Friendly + raw errors** – Surface user-friendly `errors` for rendering, while also exposing the untouched validator `issues` array for logging/analytics.
1113
- **Helpers for remote inputs** – Includes `normalizeFiles` for bundling file uploads and `standardValidate` for reusing schema validation outside the form class.
1214

15+
> Standard validate was yoinked straight from the `StandardSchema` GitHub
16+
1317
## Installation
1418

1519
```bash
@@ -102,59 +106,285 @@ const form = new CommandForm(userSchema, { command: saveUser });
102106

103107
### `new CommandForm(schema, options)`
104108

105-
| Option | Type | Description |
106-
| ------------ | -------------------------------------- | ------------------------------------------------------------------------------------- |
107-
| `initial` | `Partial<T>` \| `() => Partial<T>` | Optional initial values. Returning a function lets you compute defaults per instance. |
108-
| `command` | `(input: TIn) => Promise<TOut>` | Required remote command. The resolved value is stored in `result`. |
109-
| `invalidate` | `string \| string[] \| 'all'` | Optional SvelteKit invalidation target(s) to refresh once a submission succeeds. |
110-
| `reset` | `'onSuccess' \| 'always' \| 'onError'` | Optional reset behavior (default: no auto reset). |
111-
| `onSubmit` | `(data) => void \| Promise<void>` | Called right after the schema parse succeeds, before `command`. |
112-
| `onSuccess` | `(result) => void \| Promise<void>` | Runs after `command` resolves. |
113-
| `onError` | `(err) => void \| Promise<void>` | Runs after client, schema, or HTTP errors are handled. |
109+
#### `schema`
110+
111+
The schema that the command accepts.
112+
113+
```typescript
114+
// someCommand.schema.ts
115+
116+
import { z } from 'zod';
117+
118+
const schema = z.object({
119+
name: z.string().min(1, 'Must have a name')
120+
});
121+
122+
export { schema as someCommandSchema };
123+
```
124+
125+
```html
126+
<script lang="ts">
127+
import { someCommandSchema } from '$lib/someCommand.schema.ts';
128+
129+
const cmd = new CommandForm(someCommandSchema, {
130+
// ... other options
131+
});
132+
</script>
133+
```
134+
135+
---
136+
137+
#### `options.initial`
138+
139+
Optional initial values. Returning a functions lets you compute defaults per form instance and/or when computed values change, like when using `$derived()`
140+
141+
> You must set default values here if you are using them, default values are not able to be extracted from a `StandardSchemaV1`
142+
143+
**Example:**
144+
145+
```html
146+
<script lang="ts">
147+
let { data } = $props();
148+
let { name } = $derived(data);
149+
150+
const cmd = new CommandForm(schema, {
151+
// if you do not use a function to get the value of name here
152+
// you will never get the updated value
153+
initial: () => ({
154+
name
155+
})
156+
// ...other options
157+
});
158+
</script>
159+
160+
<input bind:value="{form.name}" />
161+
<button onclick="{cmd.form.submit}">Change Name</button>
162+
```
163+
164+
---
165+
166+
#### `options.command`
167+
168+
The command function that is being called.
169+
170+
**Example:**
171+
172+
```html
173+
<script lang="ts">
174+
import someCommand from '$lib/remote/some-command.remote';
175+
176+
const cmd = new CommandForm(schema, {
177+
command: someCommand
178+
// ...other options
179+
});
180+
</script>
181+
```
182+
183+
---
114184

115-
#### Instance fields
185+
#### `options.invalidate`
116186

117-
- `form``$state` proxy representing the form model. Bind inputs directly to its keys.
118-
- `errors``$state` map of `{ [field]: { message } }` that is ideal for user-facing feedback.
119-
- `issues``$state<SchemaIssues | null>` storing the untouched array emitted by `standardValidate`. Use this for logging or non-standard UI patterns.
120-
- `submitting` – Boolean getter reflecting `submit()` progress.
121-
- `result` – Getter exposing the last command result (or `null`).
187+
Optional SvelteKit invalidation targets. Can be set to a single string, a string[] for multiple targets, or a literal of `all` to run `invalidateAll()`
122188

123-
#### Methods
189+
> This only runs on successful form submissions
124190
125-
- `set(values, clear?)` – Merge values into the form. Pass `true` to replace instead of merge.
126-
- `reset()` – Restore the form to its initial state.
127-
- `validate()` – Runs schema validation without submitting, updating both `errors` and `issues`.
128-
- `submit()` – Parses the schema, calls hooks, executes the configured command, manages invalidation, and populates error state on failure.
129-
- `getErrors()` / `getIssues()` – Accessor helpers useful outside of `$state` reactivity (e.g., from tests).
130-
- `addError({path: string, message: string})` - Allows you to set an error on the form programatically (client side only)
191+
**Example:**
192+
193+
```html
194+
<script lang="ts">
195+
const cmd = new CommandForm(schema, {
196+
invalidate: 'user:details' // invalidates routes with depends("user:details") set
197+
// ...other options
198+
});
199+
</script>
200+
```
131201

132-
### `standardValidate(schema, input)`
202+
---
133203

134-
A small helper that runs the Standard Schema `validate` function, awaits async results, and throws `SchemaValidationError` when issues are returned. Use it to share validation logic between the form and other server utilities.
204+
#### `options.reset`
135205

136-
### `SchemaValidationError`
206+
Allows you to select if the form should be reset. By default, the form never resets. This accepts a value of `onSuccess` | `onError` or `always`
137207

138-
Custom error class wrapping the exact `issues` array returned by your schema. Catch it to reuse `transformIssues` or custom logging.
208+
**Example:**
139209

140-
### `normalizeFiles(files: File[])`
210+
```html
211+
<script lang="ts">
212+
const cmd = new CommandForm(schema, {
213+
reset: 'always' // the form will reset after submission no matter what
214+
// ...other options
215+
});
216+
</script>
217+
```
141218

142-
Utility that converts a `File[]` into JSON-friendly objects `{ name, type, size, bytes }`, making it easy to send uploads through command functions.
219+
---
220+
221+
#### `options.preprocess()`
222+
223+
Allows you to preprocess any data you have set when the form is submitted. This will run prior to any parsing on the client. For example if you would need to convert an input of type 'date' to an ISO string on the client before submitting. If this is a promise, it will be awaited before continuing.
224+
225+
```html
226+
<script lang="ts">
227+
const cmd = new CommandForm(schema, {
228+
preprocess: (data) => {
229+
cmd.set({ someDate: new Date(data.someDate).toISOString() });
230+
}
231+
// ... other options
232+
});
233+
</script>
234+
235+
<input type="date" bind:value="{cmd.form.someDate}" />
236+
```
237+
238+
---
239+
240+
#### `options.onSuccess()`
241+
242+
Runs if the form is submitted and returns sucessfully. You will have access to the returned value from the `command` that is ran. This can also be a promise.
243+
244+
```html
245+
<script lang="ts">
246+
const cmd = new CommandForm(schema, {
247+
onSuccess: (response) => {
248+
toast.success(`${response.name} has been updated!`);
249+
}
250+
// ... other options
251+
});
252+
</script>
253+
254+
<input type="date" bind:value="{cmd.form.someDate}" />
255+
```
256+
257+
---
258+
259+
#### `options.onError()`
260+
261+
Runs if the command fails and an error is returned.
262+
263+
```html
264+
<script lang="ts">
265+
const cmd = new CommandForm(schema, {
266+
onError: (error) => {
267+
toast.error('Oops! Something went wrong!');
268+
console.error(error);
269+
}
270+
// ... other options
271+
});
272+
</script>
273+
```
274+
275+
---
276+
277+
### Methods & Values
278+
279+
When you create a `new CommandForm` you get access to several methods and values that will help you manage your form state, submit, reset, and/or display errors.
280+
281+
In the following examples we will be using the following command form.
282+
283+
```html
284+
<script lang="ts">
285+
const cmd = new CommandForm(schema, {
286+
initial: {
287+
name: 'Ada Lovelace',
288+
age: '30'
289+
}
290+
});
291+
</script>
292+
```
293+
294+
#### `.form`
295+
296+
Gives you access to the data within the form. Useful when binding to inputs.
297+
298+
```svelte
299+
<input placeholder="What is your name?" bind:value={cmd.form.name} />
300+
```
301+
302+
---
303+
304+
#### `.set(values, clear?: boolean )`
305+
306+
Allows you to programatically merge form field values in bulk or add other values. If you set clear to true, it will replace all values instead of merging them in.
307+
308+
```typescript
309+
set({ name: 'Linus Torvalds' });
310+
311+
// cmd.form will now be {name: "Linus Torvalds", age: 30}
312+
313+
set({ name: 'Linus Sebastian' }, true);
314+
315+
// cmd.form will now be {name: "Linus Sebastian"}
316+
```
317+
318+
---
319+
320+
#### `.reset()`
321+
322+
Resets the form to the initial values that were passed in when it was instantiated.
323+
324+
> Note: If you are using an accessor function inside of `options.initial` it will reset to the newest available value instead of what it was when you instantiated it.
325+
326+
---
327+
328+
#### `.validate()`
329+
330+
Runs the parser and populates any errors. Useful if you want to display errors in realtime as the user is filling out the form. It will also clear any errors as they are corrected each time it is run.
331+
332+
> If you are using `options.preprocess` this is not ran during `validate()` however if you are using a schema library preprocessor such as `zod.preprocess` it should be ran within the parse.
333+
334+
```svelte
335+
<input bind:value={cmd.form.name} onchange={cmd.validate} />
336+
337+
{#if cmd.errors.name}
338+
<!-- display the error -->
339+
{#if}
340+
```
341+
342+
---
343+
344+
#### `.submitting`
345+
346+
Returns a boolean indicatiing whether the form is in flight or not. Useful for setting disabled states or showing loading spinners while the data is processed.
347+
348+
```svelte
349+
{#if cmd.submitting}
350+
Please wait while we update your name...
351+
{:else}
352+
<input bind:value={cmd.form.name} />
353+
{/if}
354+
<button onclick={cmd.submit} disabled={cmd.submitting}>Submit</button>
355+
```
356+
357+
---
358+
359+
#### `errors`
360+
361+
Returns back an easily accessible object with any validation errors. See [Errors](#errors) for more information on how to render.
362+
363+
#### `issues`
364+
365+
Returns back the raw validation issues. See [Issues](#issues) for more information.
366+
367+
---
143368

144369
## Handling file uploads
145370

146-
SvelteKit command functions currently expect JSON-serializable payloads, so `File` objects cannot be passed directly from the client to a command. Use the provided `normalizeFiles` helper to convert browser `File` instances into serializable blobs inside the `onSubmit` hook (so the parsed data that reaches your command already contains normalized entries):
371+
SvelteKit command functions currently expect JSON-serializable payloads, so `File` objects cannot be passed directly from
372+
the client to a command.
373+
374+
Use the provided `normalizeFiles` helper to convert browser
375+
`File` instances into serializable blobs inside the `onSubmit` hook (so the parsed
376+
data that reaches your command already contains normalized entries):
147377

148378
```html
149379
<script lang="ts">
150380
import { CommandForm, normalizeFiles } from 'svelte-command-form';
151381
import { zodSchema } from '$lib/schemas/upload.schema';
152382
import { uploadCommand } from '$lib/server/upload.remote';
153383
154-
const form = new CommandForm(zodSchema, {
384+
const cmd = new CommandForm(zodSchema, {
155385
command: uploadCommand,
156-
async onSubmit(data) {
157-
data.attachments = await normalizeFiles(data.attachments);
386+
async preprocess(data) {
387+
cmd.form.attachments = await normalizeFiles(data.attachments);
158388
}
159389
});
160390
@@ -180,19 +410,6 @@ type NormalizedFile = {
180410

181411
Both the Zod and Valibot schemas above can be adapted to accept either `File[]` (for client-side validation) or this normalized structure if you prefer validating the serialized payload on the server.
182412

183-
## Initial values and schema defaults
184-
185-
Standard Schema v1 does **not** provide a cross-library location for default values. A Zod or Valibot schema may specify defaults internally, but those defaults are not discoverable through the shared `~standard` interface. If there is an easy way to do this feel free to submit a PR. Because of that, `CommandForm` cannot pull defaults from your schema automatically. Instead, pass defaults via `options.initial`:
186-
187-
```ts
188-
const form = new CommandForm(userSchema, {
189-
initial: { name: 'Ada Lovelace', age: 30, attachments: [] },
190-
command: saveUser
191-
});
192-
```
193-
194-
`initial` can also be a function if you need to recompute defaults per instantiation (`initial: () => ({ createdAt: new Date().toISOString() }))` or if you are using a `$derived()`. Any keys not provided remain `undefined` (or `null` if you explicitly set them) until the user interacts with them or you call `form.set`. If your schema rejects `undefined`/`null`, make it nullable (`z.string().nullable()`, `z.array(...).optional()`, etc.) or seed the field via `initial`.
195-
196413
## Error handling
197414

198415
When validation fails, `CommandForm`:

src/lib/command-form/command-form.svelte.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class CommandForm<Schema extends StandardSchemaV1, TOut> {
9797
this._result = null;
9898

9999
try {
100+
await this.options.preprocess?.(this.form)
100101
const parsed = await this.parseForm();
101102
await this.options.onSubmit?.(parsed);
102103

src/lib/types/command-form.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ type CommandFormOptions<TIn, TOut> = {
1111
invalidate?: string | string[] | 'all';
1212
command: RemoteCommand<TIn, TOut>;
1313
reset?: 'onSuccess' | 'always' | 'onError';
14+
preprocess?: (data: TIn) => Promise<void> | void;
1415
onSubmit?: (data: TIn) => Promise<void> | void;
1516
onSuccess?: (result: Awaited<TOut>) => Promise<void> | void;
1617
onError?: (err: unknown) => Promise<void> | void;

src/routes/+page.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<script lang="ts">
22
import { CommandForm } from '$lib/command-form/command-form.svelte.ts';
33
import { test } from './test.remote.ts';
4-
import { schema } from './test.schema.ts';
4+
import { schema, TestEnum } from './test.schema.ts';
55
66
const f = new CommandForm(schema, {
77
command: test,
88
initial: {
9-
hobbies: ['coding']
9+
hobbies: ['coding'],
10+
status: TestEnum.ONE
1011
},
1112
onSubmit: async (data) => {
1213
console.log(data);

src/routes/test.remote.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { command } from "$app/server";
22
import { schema } from "./test.schema.ts";
33

44

5+
6+
57
export const test = command(schema, async (d) => {
68
console.log("running comand")
79
console.log(d)

0 commit comments

Comments
 (0)