A complete form validation example demonstrating the validation plugin:
form/
├── index.ts # Shared types and injection key
├── Form.vue # Parent form component
└── FormField.vue # Individual field component
import type { InjectionKey, Ref } from 'vue';
import type { DeskCore } from '#vue-airport/composables/useCheckIn';
import type { DeskWithContext } from '#vue-airport';
import type {
ValidationError,
ValidationPluginComputed,
ValidationPluginMethods,
} from '@vue-airport/plugins-validation';
export interface FieldData {
id: string;
label: string;
value: string | number;
type: 'text' | 'email' | 'number';
required?: boolean;
touched?: boolean;
error?: ValidationError;
}
export interface FormContext {
fieldData: Ref<FieldData[]>;
errorById: (id: string) => ValidationError | undefined;
}
export type DeskWithValidation = DeskWithContext<FieldData, FormContext> &
ValidationPluginMethods<FieldData> &
ValidationPluginComputed<FieldData>;
export const FORM_DESK_KEY: InjectionKey<DeskCore<FieldData> & FormContext> = Symbol('formDesk');
export { default as Form } from './Form.vue';
export { default as FormField } from './FormField.vue';
Creates a desk with validation plugin:
<script setup lang="ts">
import { useCheckIn, type DeskWithContext } from '#vue-airport';
import { createValidationPlugin, type ValidationError } from '@vue-airport/plugins-base';
import { type FieldData, FORM_DESK_KEY, type FormContext, FormField } from '.';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
/**
* Form Example - Validation Plugin
*
* Demonstrates:
* - Custom validation plugin
* - Form field validation
* - Real-time validation feedback
* - Form submission with validation
* - Error cache management
*/
// Create custom validation plugin
const validationPlugin = createValidationPlugin<FieldData>({
validate: (data: FieldData) => {
// Check if required field is filled
if (data.required && !data.value) {
return 'This field is required';
}
// Validate email format
if (data.type === 'email' && data.value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(String(data.value))) {
return 'Invalid email';
}
}
// Validate age if provided
if (data.type === 'number' && data.value) {
const age = Number(data.value);
if (isNaN(age) || age < 18 || age > 120) {
return 'Age must be a number between 18 and 120';
}
}
return true; // No errors
},
maxErrors: 100, // Keep up to 100 validation errors
});
// Form fields state
const fieldData = ref<Array<FieldData>>([
{
id: 'name',
label: 'Name',
value: '',
type: 'text',
required: true,
touched: false,
},
{
id: 'email',
label: 'Email',
value: '',
type: 'email',
required: true,
touched: false,
},
{
id: 'age',
label: 'Age',
value: '',
type: 'number',
required: false,
touched: false,
},
]);
type DeskWithValidation = DeskWithContext<FieldData, FormContext> & {
getValidationErrors?: () => ValidationError[];
clearValidationErrors?: () => void;
count?: number;
};
// Create a desk with validation plugin
const { createDesk } = useCheckIn<FieldData, FormContext>();
const { desk } = createDesk(FORM_DESK_KEY, {
devTools: true,
debug: false,
plugins: [validationPlugin],
context: {
fieldData,
errorById: (id: string) => {
const allErrors = (desk as any)?.getValidationErrors?.() || [];
return allErrors.find((e: ValidationError) => e.id === id);
},
},
});
const validatedDesk = desk as DeskWithValidation;
// Computed properties for validation
const validationErrors = computed(() => {
return validatedDesk.getValidationErrors?.() || [];
});
// Check if form is valid (all fields filled and no validation errors)
const isFormValid = computed(() => {
// Check if all required fields have values
const allRequiredFilled = fieldData.value
.filter((f) => f.required)
.every((f) => String(f.value).trim() !== '');
// Check if there are NO validation errors at all
const noValidationErrors = validationErrors.value.length === 0;
return allRequiredFilled && noValidationErrors;
});
// Function to submit the form
const submitForm = () => {
if (isFormValid.value) {
const formData = fieldData.value.reduce(
(acc, field) => {
acc[field.id] = String(field.value);
return acc;
},
{} as Record<string, string>
);
alert('Form submitted successfully!\n\n' + JSON.stringify(formData, null, 2));
// Clear validation errors after successful submission
validatedDesk.clearValidationErrors?.();
} else {
const errorList = validationErrors.value
.map((e: ValidationError) => `- ${e.message}`)
.join('\n');
alert('The form contains errors. Please correct them:\n\n' + errorList);
}
};
// Function to reset the form
const resetForm = () => {
fieldData.value.forEach((field) => {
field.value = '';
field.touched = false;
});
// Clear validation errors when resetting
validatedDesk.clearValidationErrors?.();
};
</script>
<template>
<div>
<form class="flex flex-col gap-6" @submit.prevent="submitForm">
<FormField v-for="field in fieldData" :id="field.id" :key="field.id" />
<div class="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<Button :disabled="!isFormValid">
<UIcon name="i-heroicons-check" class="w-4 h-4" />
Submit
</Button>
<Button variant="outline" @click="resetForm">
<UIcon name="i-heroicons-arrow-path" class="w-4 h-4" />
Reset
</Button>
</div>
<div class="flex items-center gap-4 p-4 bg-card border border-border rounded-md">
<Badge
:variant="isFormValid ? 'default' : 'destructive'"
:class="{ 'px-3 py-1 text-sm': true, 'bg-green-500': isFormValid }"
>
{{ isFormValid ? '✓ Valid form' : '✗ Invalid form' }}
</Badge>
<span class="text-sm text-muted"> {{ validationErrors.length }} error(s) </span>
</div>
</form>
</div>
</template>
Individual form field with automatic validation:
<script setup lang="ts">
import { useCheckIn } from '#vue-airport/composables/useCheckIn';
import { type FieldData, FORM_DESK_KEY, type FormContext } from '.';
import { Input } from '@/components/ui/input';
/**
* Form Field Component
*
* Individual form field that automatically checks in to the form desk
* and watches value changes for validation.
*/
const props = defineProps<{
id: string;
}>();
// Check in to the desk with watchData
const { checkIn } = useCheckIn<FieldData, FormContext>();
const { desk } = checkIn(FORM_DESK_KEY, {
id: props.id,
autoCheckIn: true,
watchData: true,
data: (desk) => {
const field = desk.fieldData?.value?.find((f) => f.id === props.id);
return {
id: props.id,
label: field?.label || '',
value: field?.value || '',
type: field?.type || 'text',
required: field?.required,
touched: field?.touched || false,
};
},
});
// Get field data from context
const fieldData = computed(() => {
return desk?.fieldData?.value.find((f) => f.id === props.id);
});
const errorMessage = computed<string | undefined>(() => {
if (fieldData.value?.touched) {
return desk?.errorById(props.id)?.message;
}
return undefined;
});
// Function to update both the field value and hte local value
const updateFieldValue = (value: string | number) => {
const stringValue = String(value);
const error = desk?.errorById(props.id);
// Update the desk
desk?.update(props.id, { value: stringValue, touched: true, error });
// ALSO update the fieldData in context to keep it in sync
if (fieldData.value) {
fieldData.value.value = stringValue;
fieldData.value.touched = true;
}
};
</script>
<template>
<div class="flex flex-col gap-2">
<label :for="String(props.id)" class="font-medium text-gray-900 dark:text-gray-100">
{{ fieldData?.label }}
<span v-if="fieldData?.required" class="text-red-500">*</span>
</label>
<Input
:id="String(props.id)"
:model-value="fieldData?.value"
:type="fieldData?.type || 'text'"
:placeholder="`Enter ${fieldData?.label.toLowerCase()}`"
@update:model-value="updateFieldValue"
/>
<span v-if="errorMessage" class="text-red-500 text-sm">
{{ errorMessage }}
</span>
</div>
</template>
The validation plugin provides field-level validation:
const validationPlugin = createValidationPlugin<FieldData>({
validate: (data: FieldData) => {
// Return error message string, or true if valid
if (data.required && !data.value) {
return 'This field is required';
}
return true;
},
maxErrors: 100,
});
The plugin extends the desk with validation methods:
getValidationErrors(): Get all validation errorsgetValidationErrorsById(id): Get errors for a specific fieldclearValidationErrors(): Clear all errorshasValidationErrors: Boolean flag indicating if errors existvalidationErrorCount: Number of current validation errors (computed)hasValidationErrors: Boolean flag indicating if errors exist (computed)Fields are automatically validated when data changes thanks to watchData and the onBeforeUpdate hook:
watchData detects the change and calls desk.update()onBeforeUpdate hook validates the new dataThe form tracks its overall validity:
const isFormValid = computed(() =>
!hasErrors.value && desk.count > 0
);
autoCheckIn: true)onBeforeCheckIn hook (allows check-in even if invalid)watchData: true) detects value changesonBeforeUpdate hook on every keystroke (note this could be enhanced with debounce plugin for performance)This pattern is ideal for: