CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-vee-validate

Painless forms for Vue.js with comprehensive validation, composition API, and component-based approaches.

Pending
Overview
Eval results
Files

vue-components.mddocs/

Vue Components

Renderless Vue components for template-based form development with integrated validation. These components provide declarative APIs for building forms while maintaining full control over rendering and styling.

Capabilities

Field Component

Renderless field component that provides validation and state management for individual form fields.

/**
 * Renderless field component for individual form fields
 * Provides validation, state management, and binding objects via slot props
 */
interface FieldProps {
  name: string;                                    // Field path/name (required)
  rules?: RuleExpression;                          // Validation rules
  as?: string | Component;                         // Render as specific element/component
  validateOnMount?: boolean;                       // Validate when field mounts
  validateOnBlur?: boolean;                        // Validate on blur events
  validateOnChange?: boolean;                      // Validate on change events
  validateOnInput?: boolean;                       // Validate on input events
  validateOnModelUpdate?: boolean;                 // Validate on v-model updates
  bails?: boolean;                                 // Stop validation on first error
  label?: string;                                  // Field label for error messages
  uncheckedValue?: any;                            // Value when checkbox/radio unchecked
  modelValue?: any;                                // v-model binding value
  keepValue?: boolean;                             // Preserve value on unmount
}

interface FieldSlotProps {
  field: FieldBindingObject;                       // Input binding object 
  componentField: ComponentFieldBindingObject;     // Component binding object
  value: any;                                      // Current field value
  meta: FieldMeta;                                 // Field metadata
  errors: string[];                                // Field errors array
  errorMessage: string | undefined;               // First error message
  validate: () => Promise<ValidationResult>;      // Manual validation trigger
  resetField: (state?: Partial<FieldState>) => void;  // Reset field state
  handleChange: (value: any) => void;              // Handle value changes
  handleBlur: () => void;                          // Handle blur events
  setValue: (value: any) => void;                  // Set field value
  setTouched: (touched: boolean) => void;          // Set touched state
  setErrors: (errors: string[]) => void;           // Set field errors
}

interface FieldBindingObject {
  name: string;
  onBlur: () => void;
  onChange: (e: Event) => void;
  onInput: (e: Event) => void;
  value: any;
}

interface ComponentFieldBindingObject {
  modelValue: any;
  'onUpdate:modelValue': (value: any) => void;
  onBlur: () => void;
}

Field Component Examples:

<template>
  <!-- Basic field with custom input -->
  <Field name="email" :rules="emailRules" v-slot="{ field, errorMessage, meta }">
    <input 
      v-bind="field"
      type="email" 
      placeholder="Enter your email"
      :class="{ 
        'error': !meta.valid && meta.touched,
        'success': meta.valid && meta.touched 
      }"
    />
    <span v-if="errorMessage" class="error-message">{{ errorMessage }}</span>
  </Field>

  <!-- Field rendered as specific element -->
  <Field name="message" as="textarea" rules="required" v-slot="{ field, errorMessage }">
    <label>Message</label>
    <!-- Field is rendered as textarea automatically -->
    <span v-if="errorMessage">{{ errorMessage }}</span>
  </Field>

  <!-- Field with component binding -->
  <Field name="category" rules="required" v-slot="{ componentField, errorMessage }">
    <CustomSelect v-bind="componentField" :options="categories" />
    <ErrorMessage name="category" />
  </Field>

  <!-- Checkbox field -->
  <Field 
    name="newsletter" 
    type="checkbox"
    :unchecked-value="false"
    :value="true"
    v-slot="{ field, value }"
  >
    <label>
      <input v-bind="field" type="checkbox" />
      Subscribe to newsletter ({{ value ? 'Yes' : 'No' }})
    </label>
  </Field>

  <!-- Field with custom validation -->
  <Field 
    name="username" 
    :rules="validateUsername"
    v-slot="{ field, errorMessage, meta, validate }"
  >
    <input v-bind="field" placeholder="Username" />
    <button @click="validate" :disabled="meta.pending">
      {{ meta.pending ? 'Validating...' : 'Check Availability' }}
    </button>
    <span v-if="errorMessage">{{ errorMessage }}</span>
  </Field>

  <!-- Field with advanced state management -->
  <Field 
    name="password" 
    rules="required|min:8"
    v-slot="{ field, meta, errors, setValue, setTouched }"
  >
    <input 
      v-bind="field" 
      type="password" 
      placeholder="Password"
      @focus="setTouched(true)"
    />
    
    <!-- Password strength indicator -->
    <div class="password-strength">
      <div 
        v-for="error in errors" 
        :key="error"
        class="strength-rule"
        :class="{ 'met': !errors.includes(error) }"
      >
        {{ error }}
      </div>
    </div>
    
    <button @click="setValue(generatePassword())">
      Generate Strong Password
    </button>
  </Field>
</template>

<script setup lang="ts">
import { Field, ErrorMessage } from 'vee-validate';

const emailRules = (value: string) => {
  if (!value) return 'Email is required';
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Email is invalid';
  return true;
};

const validateUsername = async (value: string) => {
  if (!value) return 'Username is required';
  if (value.length < 3) return 'Username too short';
  
  // Async validation
  const response = await fetch(`/api/check-username?username=${value}`);
  const { available } = await response.json();
  
  return available || 'Username is taken';
};

const categories = [
  { value: 'tech', label: 'Technology' },
  { value: 'design', label: 'Design' },
  { value: 'business', label: 'Business' }
];

const generatePassword = () => {
  return Math.random().toString(36).slice(-12) + 'A1!';
};
</script>

Form Component

Form wrapper component that provides validation context and handles form submission.

/**
 * Form wrapper component with validation context
 * Provides form state management and submission handling via slot props
 */
interface FormProps {
  as?: string | Component;                         // Render as specific element (default: 'form')
  validationSchema?: object;                       // Form validation schema
  initialValues?: object;                          // Initial field values
  initialErrors?: object;                          // Initial field errors
  initialTouched?: object;                         // Initial field touched states
  validateOnMount?: boolean;                       // Validate when form mounts
  onSubmit?: SubmissionHandler;                    // Form submission handler
  onInvalidSubmit?: InvalidSubmissionHandler;      // Invalid submission handler
  keepValues?: boolean;                            // Preserve values on unmount
  name?: string;                                   // Form identifier
}

interface FormSlotProps {
  // Form state
  values: Record<string, any>;                     // Current form values
  errors: Record<string, string>;                  // Current form errors
  meta: FormMeta;                                  // Form metadata
  isSubmitting: boolean;                           // Submission state
  isValidating: boolean;                           // Validation state
  submitCount: number;                             // Submission attempt count
  
  // Form methods
  handleSubmit: (e?: Event) => Promise<void>;      // Form submission handler
  handleReset: () => void;                         // Form reset handler
  validate: () => Promise<FormValidationResult>;   // Manual form validation
  validateField: (field: string) => Promise<ValidationResult>;  // Manual field validation
  
  // State mutations
  setFieldValue: (field: string, value: any) => void;        // Set field value
  setFieldError: (field: string, error: string) => void;     // Set field error
  setErrors: (errors: Record<string, string>) => void;       // Set multiple errors
  setValues: (values: Record<string, any>) => void;          // Set multiple values
  setTouched: (touched: Record<string, boolean>) => void;    // Set touched states
  resetForm: (state?: Partial<FormState>) => void;           // Reset form
  resetField: (field: string, state?: Partial<FieldState>) => void;  // Reset field
}

Form Component Examples:

<template>
  <!-- Basic form with validation schema -->
  <Form 
    :validationSchema="schema" 
    :initial-values="initialValues"
    @submit="onSubmit"
    v-slot="{ errors, meta, isSubmitting }"
  >
    <Field name="name" v-slot="{ field, errorMessage }">
      <input v-bind="field" placeholder="Full Name" />
      <span v-if="errorMessage">{{ errorMessage }}</span>
    </Field>

    <Field name="email" v-slot="{ field, errorMessage }">
      <input v-bind="field" type="email" placeholder="Email" />
      <span v-if="errorMessage">{{ errorMessage }}</span>
    </Field>

    <button 
      type="submit" 
      :disabled="!meta.valid || isSubmitting"
    >
      {{ isSubmitting ? 'Submitting...' : 'Submit' }}
    </button>

    <!-- Form-level error display -->
    <div v-if="Object.keys(errors).length > 0" class="form-errors">
      <h4>Please fix the following errors:</h4>
      <ul>
        <li v-for="(error, field) in errors" :key="field">
          {{ field }}: {{ error }}
        </li>
      </ul>
    </div>
  </Form>

  <!-- Advanced form with manual submission -->
  <Form 
    :validation-schema="userSchema"
    v-slot="{ 
      values, 
      errors, 
      meta, 
      handleSubmit, 
      setFieldValue, 
      setErrors,
      resetForm,
      isSubmitting 
    }"
  >
    <Field name="username" v-slot="{ field, errorMessage }">
      <input v-bind="field" placeholder="Username" />
      <span v-if="errorMessage">{{ errorMessage }}</span>
    </Field>

    <Field name="email" v-slot="{ field, errorMessage }">
      <input v-bind="field" type="email" placeholder="Email" />
      <span v-if="errorMessage">{{ errorMessage }}</span>
    </Field>

    <!-- Manual submission buttons -->
    <button @click="handleSubmit(submitUser)" :disabled="!meta.valid">
      Create User
    </button>
    
    <button @click="handleSubmit(saveDraft)" type="button">
      Save as Draft
    </button>

    <button @click="resetForm" type="button">
      Reset Form
    </button>

    <!-- Programmatic field updates -->
    <button @click="setFieldValue('username', generateUsername())">
      Generate Username
    </button>

    <!-- Form progress -->
    <div class="form-progress">
      Progress: {{ Math.round((Object.keys(values).filter(key => values[key]).length / Object.keys(schema).length) * 100) }}%
    </div>
  </Form>

  <!-- Form with custom validation handling -->
  <Form 
    @submit="handleFormSubmit"
    @invalid-submit="handleInvalidSubmit"
    v-slot="{ meta, submitCount }"
  >
    <Field name="data" rules="required" v-slot="{ field, errorMessage }">
      <input v-bind="field" placeholder="Enter data" />
      <span v-if="errorMessage">{{ errorMessage }}</span>
    </Field>

    <button type="submit">Submit</button>
    
    <div v-if="submitCount > 0">
      Attempts: {{ submitCount }}
    </div>
  </Form>
</template>

<script setup lang="ts">
import { Form, Field } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  name: yup.string().required('Name is required'),
  email: yup.string().email('Invalid email').required('Email is required')
});

const userSchema = yup.object({
  username: yup.string().min(3).required(),
  email: yup.string().email().required()
});

const initialValues = {
  name: '',
  email: ''
};

const onSubmit = async (values: any) => {
  console.log('Form submitted:', values);
  
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  alert('Form submitted successfully!');
};

const submitUser = async (values: any, { setErrors }: any) => {
  try {
    const response = await fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(values)
    });
    
    if (!response.ok) {
      const errors = await response.json();
      setErrors(errors);
      return;
    }
    
    alert('User created successfully!');
  } catch (error) {
    setErrors({ '': 'Network error occurred' });
  }
};

const saveDraft = async (values: any) => {
  await fetch('/api/drafts', {
    method: 'POST',
    body: JSON.stringify(values)
  });
  
  alert('Draft saved!');
};

const generateUsername = () => {
  return 'user_' + Math.random().toString(36).substr(2, 9);
};

const handleFormSubmit = (values: any) => {
  console.log('Valid form submitted:', values);
};

const handleInvalidSubmit = ({ errors, values }: any) => {
  console.log('Invalid form submission:', { errors, values });
  alert('Please fix form errors before submitting');
};
</script>

FieldArray Component

Component for managing dynamic arrays of form fields with built-in manipulation methods.

/**
 * Dynamic array field management component
 * Provides array manipulation methods via slot props
 */
interface FieldArrayProps {
  name: string;                                    // Array field path (required)
}

interface FieldArraySlotProps {
  fields: FieldEntry[];                            // Array of field entries
  push: (value: any) => void;                      // Add item to end
  remove: (index: number) => void;                 // Remove item by index
  swap: (indexA: number, indexB: number) => void;  // Swap two items
  insert: (index: number, value: any) => void;     // Insert item at index
  replace: (newArray: any[]) => void;              // Replace entire array
  update: (index: number, value: any) => void;     // Update item at index
  prepend: (value: any) => void;                   // Add item to beginning
  move: (oldIndex: number, newIndex: number) => void;  // Move item to new position
}

interface FieldEntry {
  value: any;                                      // Entry value
  key: string | number;                            // Unique key for tracking
  isFirst: boolean;                                // True if first entry
  isLast: boolean;                                 // True if last entry
}

FieldArray Component Examples:

<template>
  <!-- Simple array of strings -->
  <FieldArray name="tags" v-slot="{ fields, push, remove }">
    <div v-for="(entry, index) in fields" :key="entry.key" class="tag-item">
      <Field :name="`tags[${index}]`" v-slot="{ field, errorMessage }">
        <input v-bind="field" placeholder="Enter tag" />
        <span v-if="errorMessage">{{ errorMessage }}</span>
      </Field>
      
      <button @click="remove(index)" type="button">Remove</button>
    </div>
    
    <button @click="push('')" type="button">Add Tag</button>
  </FieldArray>

  <!-- Complex array of objects -->
  <FieldArray name="users" v-slot="{ fields, push, remove, swap, move }">
    <div v-for="(entry, index) in fields" :key="entry.key" class="user-item">
      <div class="user-fields">
        <Field :name="`users[${index}].name`" v-slot="{ field, errorMessage }">
          <input v-bind="field" placeholder="Name" />
          <span v-if="errorMessage">{{ errorMessage }}</span>
        </Field>

        <Field :name="`users[${index}].email`" v-slot="{ field, errorMessage }">
          <input v-bind="field" type="email" placeholder="Email" />
          <span v-if="errorMessage">{{ errorMessage }}</span>
        </Field>

        <Field :name="`users[${index}].role`" v-slot="{ field }">
          <select v-bind="field">
            <option value="">Select Role</option>
            <option value="admin">Admin</option>
            <option value="user">User</option>
            <option value="guest">Guest</option>
          </select>
        </Field>
      </div>

      <div class="user-actions">
        <button @click="remove(index)" type="button">Remove</button>
        
        <button 
          @click="move(index, index - 1)" 
          :disabled="entry.isFirst"
          type="button"
        >
          Move Up
        </button>
        
        <button 
          @click="move(index, index + 1)" 
          :disabled="entry.isLast"
          type="button"
        >
          Move Down
        </button>
      </div>
    </div>

    <button @click="push(defaultUser)" type="button">Add User</button>
    
    <button @click="push(defaultUser, 0)" type="button">Add User at Top</button>
  </FieldArray>

  <!-- Advanced field array with drag and drop -->
  <FieldArray 
    name="sortableItems" 
    v-slot="{ fields, remove, swap, update }"
  >
    <draggable 
      v-model="fields" 
      @end="handleDragEnd"
      item-key="key"
    >
      <template #item="{ element: entry, index }">
        <div class="sortable-item">
          <div class="drag-handle">⋮⋮</div>
          
          <Field :name="`sortableItems[${index}].title`" v-slot="{ field }">
            <input v-bind="field" placeholder="Item title" />
          </Field>

          <Field :name="`sortableItems[${index}].description`" v-slot="{ field }">
            <textarea v-bind="field" placeholder="Description"></textarea>
          </Field>

          <button @click="remove(index)" type="button">×</button>
        </div>
      </template>
    </draggable>
  </FieldArray>
</template>

<script setup lang="ts">
import { FieldArray, Field } from 'vee-validate';
import draggable from 'vuedraggable';

const defaultUser = {
  name: '',
  email: '',
  role: ''
};

const handleDragEnd = (event: any) => {
  // Draggable automatically updates the fields array
  console.log('Items reordered:', event);
};
</script>

ErrorMessage Component

Conditional error message display component that only renders when a field has an error.

/**
 * Conditional error message display component
 * Only renders when specified field has an error message
 */
interface ErrorMessageProps {
  name: string;                                    // Field path to show error for (required)
  as?: string;                                     // Render as specific element
}

interface ErrorMessageSlotProps {
  message: string | undefined;                     // Error message or undefined
}

ErrorMessage Component Examples:

<template>
  <!-- Basic error message display -->
  <Field name="email" rules="required|email" v-slot="{ field }">
    <input v-bind="field" type="email" placeholder="Email" />
  </Field>
  <ErrorMessage name="email" />

  <!-- Custom error message styling -->
  <Field name="password" rules="required|min:8" v-slot="{ field }">
    <input v-bind="field" type="password" placeholder="Password" />
  </Field>
  <ErrorMessage name="password" as="div" class="error-text" />

  <!-- Error message with custom slot -->
  <Field name="username" rules="required" v-slot="{ field }">
    <input v-bind="field" placeholder="Username" />
  </Field>
  <ErrorMessage name="username" v-slot="{ message }">
    <div v-if="message" class="error-container">
      <icon name="warning" />
      <span>{{ message }}</span>
    </div>
  </ErrorMessage>

  <!-- Multiple error messages for different fields -->
  <div class="form-group">
    <Field name="firstName" rules="required" v-slot="{ field }">
      <input v-bind="field" placeholder="First Name" />
    </Field>
    <ErrorMessage name="firstName" />
  </div>

  <div class="form-group">
    <Field name="lastName" rules="required" v-slot="{ field }">
      <input v-bind="field" placeholder="Last Name" />
    </Field>
    <ErrorMessage name="lastName" />
  </div>

  <!-- Error message for nested fields -->
  <Field name="address.street" rules="required" v-slot="{ field }">
    <input v-bind="field" placeholder="Street Address" />
  </Field>
  <ErrorMessage name="address.street" />

  <Field name="address.city" rules="required" v-slot="{ field }">
    <input v-bind="field" placeholder="City" />
  </Field>
  <ErrorMessage name="address.city" />

  <!-- Conditional error message display -->
  <Field name="phone" :rules="phoneRules" v-slot="{ field, meta }">
    <input v-bind="field" placeholder="Phone Number" />
  </Field>
  <ErrorMessage 
    name="phone" 
    v-slot="{ message }"
  >
    <div v-if="message && showPhoneError" class="phone-error">
      {{ message }}
      <button @click="showPhoneError = false">Dismiss</button>
    </div>
  </ErrorMessage>
</template>

<script setup lang="ts">
import { Field, ErrorMessage } from 'vee-validate';
import { ref } from 'vue';

const showPhoneError = ref(true);

const phoneRules = (value: string) => {
  if (!value) return 'Phone number is required';
  if (!/^\d{10}$/.test(value)) return 'Phone number must be 10 digits';
  return true;
};
</script>

<style scoped>
.error-text {
  color: red;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

.error-container {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: red;
  font-size: 0.875rem;
}

.form-group {
  margin-bottom: 1rem;
}

.phone-error {
  background: #fee;
  border: 1px solid #fcc;
  padding: 0.5rem;
  border-radius: 4px;
  color: #c00;
}
</style>

Component Integration Patterns

Form with Mixed Field Types

Combining different field types in a comprehensive form.

<template>
  <Form 
    :validation-schema="schema"
    :initial-values="initialValues"
    @submit="onSubmit"
    v-slot="{ meta, isSubmitting }"
  >
    <!-- Text input -->
    <Field name="name" v-slot="{ field, errorMessage }">
      <label>Full Name</label>
      <input v-bind="field" type="text" />
      <ErrorMessage name="name" />
    </Field>

    <!-- Email input with custom validation -->
    <Field name="email" v-slot="{ field, errorMessage, meta }">
      <label>Email Address</label>
      <input 
        v-bind="field" 
        type="email" 
        :class="{ valid: meta.valid && meta.touched }"
      />
      <ErrorMessage name="email" />
    </Field>

    <!-- Select dropdown -->
    <Field name="country" v-slot="{ field }">
      <label>Country</label>
      <select v-bind="field">
        <option value="">Select Country</option>
        <option value="us">United States</option>
        <option value="ca">Canada</option>
        <option value="uk">United Kingdom</option>
      </select>
      <ErrorMessage name="country" />
    </Field>

    <!-- Checkbox -->
    <Field 
      name="agreeToTerms" 
      type="checkbox" 
      :value="true"
      v-slot="{ field }"
    >
      <label>
        <input v-bind="field" type="checkbox" />
        I agree to the terms and conditions
      </label>
      <ErrorMessage name="agreeToTerms" />
    </Field>

    <!-- Dynamic field array -->
    <FieldArray name="hobbies" v-slot="{ fields, push, remove }">
      <label>Hobbies</label>
      <div v-for="(entry, index) in fields" :key="entry.key">
        <Field :name="`hobbies[${index}]`" v-slot="{ field }">
          <input v-bind="field" placeholder="Enter hobby" />
        </Field>
        <button @click="remove(index)" type="button">Remove</button>
      </div>
      <button @click="push('')" type="button">Add Hobby</button>
    </FieldArray>

    <!-- Submit button -->
    <button 
      type="submit" 
      :disabled="!meta.valid || isSubmitting"
    >
      {{ isSubmitting ? 'Submitting...' : 'Submit' }}
    </button>
  </Form>
</template>

Install with Tessl CLI

npx tessl i tessl/npm-vee-validate

docs

configuration-rules.md

core-validation.md

field-management.md

form-actions.md

form-management.md

index.md

state-access.md

vue-components.md

tile.json