Examples

Form Validation

Form field validation with real-time feedback and error handling.

A complete form validation example demonstrating the validation plugin:

  • Custom validation plugin with field-level rules
  • Real-time validation feedback
  • Form submission with validation
  • Error cache management
  • Required field and email validation
Name *

Please enter your name.

Email *

Please enter your email.

Age

Please enter your age.

✗ Invalid form
0 error(s)

Project Structure

form/
├── index.ts           # Shared types and injection key
├── Form.vue           # Parent form component
└── FormField.vue      # Individual field component

Shared Types (index.ts)

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';

Parent Component (Form.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>

Child Component (FormField.vue)

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>

Key Concepts

Validation Plugin

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,
});

Validation Methods

The plugin extends the desk with validation methods:

  • getValidationErrors(): Get all validation errors
  • getValidationErrorsById(id): Get errors for a specific field
  • clearValidationErrors(): Clear all errors
  • hasValidationErrors: Boolean flag indicating if errors exist
  • validationErrorCount: Number of current validation errors (computed)
  • hasValidationErrors: Boolean flag indicating if errors exist (computed)

Real-time Validation

Fields are automatically validated when data changes thanks to watchData and the onBeforeUpdate hook:

  1. User types in a field
  2. Field's local value updates
  3. watchData detects the change and calls desk.update()
  4. Validation plugin's onBeforeUpdate hook validates the new data
  5. Validation errors are updated or cleared in real-time
  6. UI reflects current validation state immediately

Form Validation State

The form tracks its overall validity:

const isFormValid = computed(() => 
  !hasErrors.value && desk.count > 0
);

How It Works

  1. Validation plugin is created with custom validation logic
  2. Desk is created with the validation plugin in the parent component
  3. Fields check in automatically when mounted (autoCheckIn: true)
  4. Initial validation runs via onBeforeCheckIn hook (allows check-in even if invalid)
  5. Local value tracking in each field for independent state management
  6. Data watching (watchData: true) detects value changes
  7. Updates trigger validation via onBeforeUpdate hook on every keystroke (note this could be enhanced with debounce plugin for performance)
  8. Errors are updated in real-time and displayed per field
  9. Touched fields pattern prevents showing errors on pristine fields
  10. Form submission is enabled only when all validations pass

Usage

This pattern is ideal for:

  • Contact forms with validation
  • User registration forms
  • Profile editing forms
  • Any form requiring field-level validation
  • Complex forms with custom validation rules