Command pattern form helper for Svelte powered by Standard Schema validation.
npm install @akcodeworks/svelte-command-formSvelte-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.
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.
- Schema-agnostic validation – Works with any library that implements the Standard Schema V1 interface. If you are unsure if your schema validation library is compatible see the list of compatible libraries.
- Command-first workflow – Wire forms directly to your remote command (command from $app/server), and let the helper manage submission, success, and error hooks.
- Typed form state – form, errors, and issues are all strongly typed from your schema, so your component code stays in sync with validation rules.
- Friendly + raw errors – Surface user-friendly errors for rendering, while also exposing the untouched validator issues array for logging/analytics.
- Helpers for remote inputs – Includes normalizeFiles for bundling file uploads and standardValidate for reusing schema validation outside the form class.
> Standard validate was yoinked straight from the StandardSchema GitHub
``bash`
pnpm add @akcodeworks/svelte-command-formor
npm install @akcodeworks/svelte-command-form
`html
{#if form.errors.name}
{form.errors.name.message}
`
CommandForm keeps two synchronized error stores:
- errors – per-field objects { message: string } that are easy to render.issues
- – the untouched Standard Schema issue array (useful for logs/analytics).
To display errors in the DOM, check the keyed entry in form.errors:
` {form.errors.name.message}html
{#if form.errors.name}
{/if}
{#if form.errors.age}
{form.errors.age.message}
Running
await form.validate() triggers the same schema parsing as submit() without sending data, so you can eagerly show validation feedback (e.g., on blur). Whenever validation passes, both errors and issues are cleared.Array and nested errors follow the dot-path reported by your schema. If the schema declares
names: z.array(z.string()) and the user submits [123], the error map becomes:`ts
{
'names.0': { message: 'Expected string' }
}
`Render that however makes sense—either surface the aggregated message near the group (
form.errors['names.0']?.message) or group entries by prefix to display per-item errors.$3
Any schema object that exposes the
~standard property works:`ts
import { z } from 'zod'; // or create a schema with any StandardSchemaV1 compliant lib.const userSchema = z.object({
name: z.string().min(2),
email: z.string().email()
});
const form = new CommandForm(userSchema, { command: saveUser });
`API
$3
####
schemaThe schema that the command accepts.
`typescript
// someCommand.schema.tsimport { z } from 'zod';
const schema = z.object({
name: z.string().min(1, 'Must have a name')
});
export { schema as someCommandSchema };
``html
`---
####
options.initialOptional initial values. Returning a functions lets you compute defaults per form instance and/or when computed values change, like when using
$derived()> You must set default values here if you are using them, default values are not able to be extracted from a
StandardSchemaV1Example:
`html
`---
####
options.commandThe command function that is being called.
Example:
`html
`---
####
options.invalidateOptional SvelteKit invalidation targets. Can be set to a single string, a string[] for multiple targets, or a literal of
all to run invalidateAll()> This only runs on successful form submissions
Example:
`html
`---
####
options.resetAllows you to select if the form should be reset. By default, the form never resets. This accepts a value of
onSuccess | onError or alwaysExample:
`html
`---
####
options.preprocess()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.
> Preprocessed data creates a
$state.snapshot() of your form data. Thus it is ephemeral, that way if the command fails, your form data is not already processed.`html
`---
####
options.onSuccess()Runs when the form is submitted. \*The data available inside of this function is the result of your preprocess function if you have one. This can also be a promise.
`html
`---
####
options.onSuccess()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.`html
`---
####
options.onSettled()Runs after the form submission finishes, whether it succeeds or fails. Receives the returned value from
command on success, or null on failure. NOTE: This runs AFTER any invalidation you may have called.`html
`---
####
options.onError()Runs if the command fails and an error is returned.
`html
`---
$3
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.In the following examples we will be using the following command form.
`html
`####
.formGives you access to the data within the form. Useful when binding to inputs.
`svelte
`---
####
.set(values, clear?: boolean )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.
`typescript
set({ name: 'Linus Torvalds' });// cmd.form will now be {name: "Linus Torvalds", age: 30}
set({ name: 'Linus Sebastian' }, true);
// cmd.form will now be {name: "Linus Sebastian"}
`---
####
.reset()Resets the form to the initial values that were passed in when it was instantiated.
> 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.---
####
.validate()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.
> 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.`svelte
{#if cmd.errors.name}
{#if}
`---
####
.submittingReturns 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.
`svelte
{#if cmd.submitting}
Please wait while we update your name...
{:else}
{/if}
`---
####
errorsReturns back an easily accessible object with any validation errors. See Errors for more information on how to render.
####
issuesReturns back the raw validation issues. See Issues for more information.
---
Handling file uploads
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):`html
`normalizeFiles outputs objects like:`ts
type NormalizedFile = {
name: string;
type: string;
size: number;
bytes: Uint8Array;
};
`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.Error handling
When validation fails,
CommandForm:1. Throws/catches
SchemaValidationError from standardValidate.
2. Converts issues into errors (per field) via transformIssues.
3. Stores the raw issue array in issues for programmatic access.If you need to manually set an error follow these steps.
1. Setup your hooks.server.ts file and add an error handler.
`typescript
export const handleError: HandleServerError = async ({ error }) => {
// Note: If you don't want bad actors seeing your validation issues, you can do an auth check here before returning
if (error instanceof SchemaValidationError) return error as SchemaValidationError;
};
`2. Throw a new
SchemaValidationError inside of the command.`typescript
export const test = command(schema, async (data) => {
const user = await db.user.findFirst({ where: { email: data.email } });
if (!user)
throw new SchemaValidationError([
{ path: ['email'], message: 'Name is invalid server error!!' }
]);
});
`In the above example this will populate the
form.errors.email.message field so you can display the error to the user on the client.> If you do not add the custom error handler in step 1, you will not get any issues back. This is a SvelteKit design principle when dealing with Remote Functions!
Manual Errors
You can add errors manually by using the
addErrors method (client only) or by throwing a new SchemaValidationError.`typescript
// server add error
const someFunc = command(schema, async (data) => {
const user = await db.find({where: email: data.email})
if(!user) throw new SchemaValidationError([{ message: "User does with this email does not exist!", path: ['email'] }])
})
``html
`> addError() does NOT throw an error, you will have to do that once you call it. If you want to throw an error, throw a new
SchemaValidationError`Feel free to contribute by opening a PR with a detailed description of why you are wanting to change what you are changing. If it can be tested with Vitest, that is preferred.