Validation
Built-in validators
import {
required,
minLength,
maxLength,
min,
max,
pattern,
email,
url,
sameAs,
fileType,
fileSize,
fileCount,
} from '@macrulez/vue-form-schema'| Function | Description |
|---|---|
required | Fails for null, undefined, '', or empty array |
minLength(n, msg?) | Min length for string or array |
maxLength(n, msg?) | Max length for string or array |
min(n, msg?) | Numeric minimum |
max(n, msg?) | Numeric maximum |
pattern(re, msg?) | Regex match |
email | Basic email format |
url | Valid URL (new URL()) |
sameAs(field, msg?) | Value must equal another field |
fileType(types[], msg?) | File MIME type or extension whitelist |
fileSize(bytes, msg?) | Max file size |
fileCount(n, msg?) | Max number of files |
Custom validators
Sync
import type { ValidatorFn } from '@macrulez/vue-form-schema'
const noSpaces: ValidatorFn = (value) =>
typeof value === 'string' && value.includes(' ') ? 'No spaces allowed' : nullAsync
Async validators are debounced (300 ms) while the user is typing/blurring a field. Errors are merged into errors after resolution. On submit(), async validators are run immediately (bypassing the debounce) and awaited, so a pending check (e.g. "username taken") blocks submission rather than resolving after onSubmit has already fired.
import type { AsyncValidatorFn } from '@macrulez/vue-form-schema'
const uniqueUsername: AsyncValidatorFn = async (value) => {
const { taken } = await fetch(`/api/check?q=${value}`).then((r) => r.json())
return taken ? 'Username is taken' : null
}Multiple errors per field (validateMode)
useForm({
schema,
validateMode: 'all', // collect all errors per field (default: 'first')
})Cross-field validation
Use sameAs for password confirmation or write a custom validator — all validators receive allValues as the second argument.
import { sameAs } from '@macrulez/vue-form-schema'
const schema: FieldDefinition[] = [
{ type: 'text', name: 'password', label: 'Password', required: true },
{
type: 'text',
name: 'confirm',
label: 'Confirm password',
validators: [sameAs('password', 'Passwords must match')],
},
]Conditional fields
visible and disabled can be a boolean, a reactive function, or a safe string expression.
const schema: FieldDefinition[] = [
{ type: 'checkbox', name: 'hasCompany', label: 'I represent a company' },
{
type: 'text',
name: 'companyName',
label: 'Company name',
visible: (values) => values['hasCompany'] === true,
required: true,
},
// string expression — has access to the `values` variable
{
type: 'select',
name: 'drink',
label: 'Drink',
visible: 'values.age >= 18',
options: [
{ label: 'Beer', value: 'beer' },
{ label: 'Water', value: 'water' },
],
},
]Set clearOnHide: true in useForm to automatically reset a hidden field's value.
Dynamic options
options can be a static array, a sync function, or an async function.
// Sync — re-evaluated on every values change
{
type: 'select',
name: 'city',
options: (values) => citiesByCountry[values['country'] as string] ?? [],
}
// Async — fetched on mount and re-fetched when optionsDeps change
{
type: 'select',
name: 'framework',
optionsDeps: ['language'],
options: async (values) => {
const res = await fetch(`/api/frameworks?lang=${values['language']}`)
return res.json()
},
}While loading, optionsLoading.value['framework'] is true and the select is disabled in FormRenderer. Access the loading state directly via form.optionsLoading.
Computed defaultValue
defaultValue can also be a function evaluated at form initialisation with already-resolved partial values as context:
{
type: 'text',
name: 'displayName',
defaultValue: (values) => `${values.firstName} ${values.lastName}`,
}