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'

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 ​

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.

Read form.isValidating if you need to know reactively whether any field still has an async check pending — form.isValid already accounts for it (it reads false while isValidating is true), so isValidating is mainly useful for showing a distinct "validating…" state in the UI.

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}`,
}