Skip to content

Validation

Built-in validators

ts
import {
  required,
  minLength,
  maxLength,
  min,
  max,
  pattern,
  email,
  url,
  sameAs,
  fileType,
  fileSize,
  fileCount,
} from '@macrulez/vue-form-schema'
FunctionDescription
requiredFails 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
emailBasic email format
urlValid 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

ts
import type { ValidatorFn } from '@macrulez/vue-form-schema'

const noSpaces: ValidatorFn = (value) =>
  typeof value === 'string' && value.includes(' ') ? 'No spaces allowed' : null

Async

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.

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

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

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

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

ts
// 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:

ts
{
  type: 'text',
  name: 'displayName',
  defaultValue: (values) => `${values.firstName} ${values.lastName}`,
}