Validation
A focused showcase of every validation strategy the library supports — declarative restrictions, custom validation functions, async server-side checks, and reactive cross-field rules — all in a single form.
What It Demonstrates
restrictionproperties (minLength, maxLength, pattern, whiteSpace, fractionDigits, minExclusive, …) that need zero validation code- A custom
validationfunction for password complexity with a real-time strength bar - An
asyncvalidation function that simulates a server-side uniqueness check - Cross-field validation — the rule re-runs automatically when the referenced field changes using
computedProps - A custom
passwordfield type with a show/hide toggle and animated strength indicator
Example
IsDirty: false
Touched: false
Valid: true
// form values:
{
"account": {},
"profile": {},
"dateRange": {},
"billing": {}
}
Restrictions — No Code Required
Restrictions are declared inline on the field and automatically converted to vee-validate rules by the library. No validation function is needed.
The username field combines three restrictions at once — length bounds and a character-set pattern:
{
name: 'username',
fieldOptions: { label: 'Username' },
description: 'Letters, numbers and underscores only. 3-20 characters.',
restriction: {
minLength: 3,
maxLength: 20,
pattern: '^[a-zA-Z0-9_]+$',
},
},Other restriction flavours used in this example:
| Field | Restriction | What it enforces |
|---|---|---|
displayName | whiteSpace: 'collapse' | Rejects leading/trailing spaces and double spaces |
bio | maxLength: 200 | Hard character cap |
startDate / endDate | pattern: '^\\d{4}-\\d{2}-\\d{2}$' | YYYY-MM-DD format |
amount | minExclusive: 0, maxInclusive: 10000, fractionDigits: 2 | Positive monetary value, max 2 decimals |
promoCode | pattern: '^[A-Z0-9]{4,8}$' | 4–8 uppercase alphanumeric chars |
Async Validation
The validation property accepts a function that returns a Promise. vee-validate handles the pending state automatically — the field stays invalid while the promise is in flight.
The email field first checks the format via a restriction, then calls the async function to simulate a round-trip to the server. Try entering admin@example.com to see the taken-email error.
const takenEmails = ['admin@example.com', 'test@test.com', 'hello@world.com'];
async function validateEmailAvailability(value: unknown): Promise<true | string> {
if (!value) return true;
await new Promise(resolve => setTimeout(resolve, 700));
return takenEmails.includes(String(value).toLowerCase())
? `${value} is already registered`
: true;
}TIP
You can combine restriction and validation on the same field. The restriction runs first; the async function only fires if the value already passes it.
Password Complexity + Strength Bar
The validation function on the password field checks five criteria and lists any that are still missing:
function validatePassword(value: unknown): true | string {
const str = String(value ?? '');
if (!str) return true;
const missing: string[] = [];
if (str.length < 8) missing.push('8+ characters');
if (!/[A-Z]/.test(str)) missing.push('uppercase letter');
if (!/[a-z]/.test(str)) missing.push('lowercase letter');
if (!/[0-9]/.test(str)) missing.push('number');
if (!/[^A-Za-z0-9]/.test(str)) missing.push('special character');
return missing.length === 0 ? true : `Needs: ${missing.join(', ')}`;
}The animated strength bar (PasswordStrengthBar.vue) is rendered directly inside the custom #password-input slot in AdvancedFormTemplate. It reads the same value.value ref that vee-validate tracks, so the bar updates on every keystroke without any extra wiring.
The new password field type was added to the template by:
- Registering
password: stringindefineMetadata - Adding a
showStrengthBar?: booleanextended property - Dropping in a
#password-inputslot that rendersPasswordInput(with the eye toggle) followed byPasswordStrengthBar
Cross-field Validation
Each field's validation function is a plain closure that reads a reactive ref at the moment it runs. The validation for confirmPassword and endDate both do exactly that:
Confirm password — reads passwordValue.value at validation time:
{
name: 'confirmPassword',
type: 'password',
showStrengthBar: false,
fieldOptions: { label: 'Confirm password' },
description: 'Re-validated automatically whenever the password field changes.',
validation: (value: unknown) => {
if (!value) return true;
const pwd = passwordValue.value;
return value === pwd || 'Passwords do not match';
},
computedProps: [
(_, fieldValue) => {
if (passwordValue.value && fieldValue.value)
validateField('account.confirmPassword')
}
]
},End date — reads startDateValue.value at validation time. YYYY-MM-DD strings compare correctly as plain strings, so no date parsing is needed:
{
name: 'endDate',
fieldOptions: { label: 'End date' },
placeholder: 'YYYY-MM-DD',
restriction: { pattern: '^\\d{4}-\\d{2}-\\d{2}$' },
validation: (value: unknown) => {
const start = startDateValue.value;
if (!value || !start) return true;
return String(value) > String(start) || 'End date must be after start date';
},
computedProps: [
(_, fieldValue) => {
if (startDateValue.value && fieldValue.value)
validateField('dateRange.endDate')
}
]
},Re-triggering when the source field changes
A validation function only runs when its own field is validated (on blur, input, or submit). Both fields above use computedProps to re-trigger validateField whenever the source field changes.
computedProps callbacks run inside a Vue computed, which means any reactive ref read inside them is tracked automatically. When passwordValue (or startDateValue) changes, the computed re-runs and — if both fields already have a value — calls validateField on the dependent field. The two concerns stay cleanly separated: the logic lives in validation, and the re-trigger lives in computedProps.
Website Validation
A plain synchronous function validates the URL format. Using the browser's built-in URL constructor keeps the code short and handles edge cases correctly:
function validateWebsite(value: unknown): true | string {
if (!value) return true;
try {
const url = new URL(String(value));
return (url.protocol === 'https:' || url.protocol === 'http:') || 'Must be a valid URL';
}
catch {
return 'Must be a valid URL (e.g. https://example.com)';
}
}Full Metadata
const metadata: Metadata[] = [
// ─── Account details ────────────────────────────────────────────────────────
{
name: 'account',
type: 'heading',
fieldOptions: { label: 'Account details' },
description: 'Combines restriction-based rules with custom and async validation.',
children: [
{
name: 'username',
fieldOptions: { label: 'Username' },
description: 'Letters, numbers and underscores only. 3-20 characters.',
restriction: {
minLength: 3,
maxLength: 20,
pattern: '^[a-zA-Z0-9_]+$',
},
},
{
name: 'email',
fieldOptions: { label: 'Email' },
description: 'Format is checked with a restriction. Availability is verified asynchronously — try admin@example.com.',
restriction: { pattern: '^.+@.+\\..+$' },
validation: validateEmailAvailability,
},
{
name: 'password',
type: 'password',
showStrengthBar: true,
fieldOptions: { label: 'Password' },
description: 'Must contain 8+ characters, uppercase, lowercase, a number and a special character.',
validation: validatePassword,
},
{
name: 'confirmPassword',
type: 'password',
showStrengthBar: false,
fieldOptions: { label: 'Confirm password' },
description: 'Re-validated automatically whenever the password field changes.',
validation: (value: unknown) => {
if (!value) return true;
const pwd = passwordValue.value;
return value === pwd || 'Passwords do not match';
},
computedProps: [
(_, fieldValue) => {
if (passwordValue.value && fieldValue.value)
validateField('account.confirmPassword')
}
]
},
],
},
// ─── Profile ────────────────────────────────────────────────────────────────
{
name: 'profile',
type: 'heading',
fieldOptions: { label: 'Profile' },
description: 'Whitespace and length restrictions keep data clean without writing a single validation function.',
children: [
{
name: 'displayName',
fieldOptions: { label: 'Display name' },
description: 'Leading/trailing spaces and repeated whitespace are rejected via the whiteSpace: collapse restriction.',
restriction: {
minLength: 2,
maxLength: 50,
whiteSpace: 'collapse',
},
},
{
name: 'bio',
minOccurs: 0,
fieldOptions: { label: 'Bio' },
description: 'Optional. Max 200 characters.',
restriction: { maxLength: 200 },
},
{
name: 'website',
minOccurs: 0,
fieldOptions: { label: 'Website' },
description: 'Optional. Validated with a custom function — must be a valid http(s) URL.',
placeholder: 'https://example.com',
validation: validateWebsite,
},
],
},
// ─── Date range (cross-field) ────────────────────────────────────────────────
{
name: 'dateRange',
type: 'heading',
fieldOptions: { label: 'Date range' },
description: 'End date is validated against the start date. computedProps re-triggers validation on endDate whenever startDate changes.',
children: [
{
name: 'startDate',
fieldOptions: { label: 'Start date' },
placeholder: 'YYYY-MM-DD',
restriction: { pattern: '^\\d{4}-\\d{2}-\\d{2}$' },
},
{
name: 'endDate',
fieldOptions: { label: 'End date' },
placeholder: 'YYYY-MM-DD',
restriction: { pattern: '^\\d{4}-\\d{2}-\\d{2}$' },
validation: (value: unknown) => {
const start = startDateValue.value;
if (!value || !start) return true;
return String(value) > String(start) || 'End date must be after start date';
},
computedProps: [
(_, fieldValue) => {
if (startDateValue.value && fieldValue.value)
validateField('dateRange.endDate')
}
]
},
],
},
// ─── Billing ────────────────────────────────────────────────────────────────
{
name: 'billing',
type: 'heading',
fieldOptions: { label: 'Billing' },
description: 'Numeric and format restrictions — no custom code needed.',
children: [
{
name: 'amount',
fieldOptions: { label: 'Amount (USD)' },
description: 'Must be greater than 0, at most $10,000, and no more than 2 decimal places.',
placeholder: '0.00',
restriction: {
minExclusive: 0,
maxInclusive: 10000,
fractionDigits: 2,
},
},
{
name: 'promoCode',
minOccurs: 0,
fieldOptions: { label: 'Promo code' },
description: 'Optional. Must be 4-8 uppercase letters or digits (e.g. SAVE2025).',
placeholder: 'SAVE2025',
restriction: { pattern: '^[A-Z0-9]{4,8}$' },
},
],
},
];