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).