This is the legacy documentation for Superforms 0.x. The 1.0 version can be found at superforms.rocks!

Client-side validation

Built-in browser validation

There is already a web standard for client-side form validation, which is virtually effortless to use with Superforms. For more advanced cases, you can use a Zod schema or the Superforms validation object for a complete client-side validation.

Usage

const { form, enhance, constraints, validate } = superForm(data.form, {
  validators: AnyZodObject | {
    field: (value) => string | string[] | null | undefined;
  },
  validationMethod: 'auto' | 'oninput' | 'onblur' | 'submit-only',
  defaultValidator: 'keep' | 'clear' = 'keep'
})

constraints

To use the built-in browser constraints, simply spread the $constraints store for a field on its input field:

<input
  name="email"
  type="email"
  bind:value={$form.email}
  {...$constraints.email} />

The constraints is an object with validation properties mapped from the schema:

{
  pattern?: string;      // z.string().regex(r)
  step?: number;         // z.number().step(n)
  minlength?: number;    // z.string().min(n)
  maxlength?: number;    // z.string().max(n)
  min?: number | string; // number if z.number.min(n), ISO date string if z.date().min(d)
  max?: number | string; // number if z.number.max(n), ISO date string if z.date().max(d)
  required?: true;       // Not nullable(), nullish() or optional()
}

Realtime validators

The built-in browser validation can be a bit constrained (pun intented), for example you can’t easily control the position and appearance of the error messages. Instead you can set the validators option to a Zod schema, which is the most convenient, but increases the size of the client bundle a bit. A more lightweight alternative is to use a custom validation object.

validators

validators: AnyZodObject | {
  field: (value) => string | string[] | null | undefined;
}

The custom validators option is an object with the same keys as the form, with a function that receives the field value and should return either a string or string[] as a validation failed message, or null or undefined if the field is valid.

Here’s how to validate a string length, for example:

const { form, errors, enhance } = superForm(data.form, {
  validators: {
    name: (value) =>
      value.length < 3 ? 'Name must be at least 3 characters' : null
  }
});

For nested data, just keep building on the validators structure. Note that arrays have a single validator for the whole array:

// On the server
const schema = z.object({
  name: z.string().min(3),
  tags: z.string().min(2).array()
});
// On the client
const { form, errors, enhance } = superForm(data.form, {
  validators: {
    name: (name) =>
      name.length < 3 ? 'Name must be at least 3 characters' : null,
    tags: (tag) => (tag.length < 2 ? 'Tag must be at least 2 characters' : null)
  }
});

validationMethod

validationMethod: 'auto' | 'oninput' | 'onblur' | 'submit-only',

The validation happens per field when the a value is changed, not just when tabbing through a field. The default validation method is based on the “reward early, validate late” patttern, a researched way of validating input data that makes for a high user satisfaction:

  • If no field error, validate on blur
  • If field error exists, validate on input

But you can also use the oninput or onblur setting to always validate on one of these events instead, or submit-only to only validate on submit.

defaultValidator

There is one additional option for specifying the on:input behavior for fields with errors:

defaultValidator: 'keep' | 'clear' = 'keep'

The default value keep means that validation errors will be displayed until the form submits (and is set to clear errors). clear will remove the error as soon as that field value is modified.

validate

The validate function gives you complete control over the validation process. Examples how to use it:

const { form, enhance, validate } = superForm(data.form)

// Simplest case, validate what's in the field right now
validate('name')

// Validate without updating, for error checking
const nameErrors = await validate('name', { update: false })

// Validate and update field with a custom value
validate('name', { value: 'Test' })

// Validate a custom value, update errors only
validate('name', { value: 'Test', update: 'errors' })

// Validate and update nested data, and also taint the field
validate(['tags', 1, 'name'], { value: 'Test', taint: true })

Asynchronous validation and debouncing

All the validators are asynchronous, so you can return a Promise and it will work. But for round-trip validation like checking if a username is taken, you might want to delay the validation so a request is not sent for every keystroke. There is no built-in delay option, so this can be achieved with the on:input event and a debounce function from a package like throttle-debounce.

Errors can be set by updating the $errors store:

// Needs to be a string[]
$errors.username = ['Username is already taken.']

Test it out

This example demonstrates how validators are used to check if tags are of the correct length.

Set a tag name to blank and see that no errors show up until you move focus outside the field (blur). When you go back and correct the mistake, the error is removed as soon as you enter more than one character (input).