CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-vuelidate

Simple, lightweight model-based validation library for Vue.js applications

Pending
Overview
Eval results
Files

collections.mddocs/

Collections and Nested Validation

Advanced validation features for arrays, nested objects, and dynamic collections with support for tracking, conditional validation, and complex data structures.

Capabilities

Array Validation with $each

Validation of array elements using the special $each key for dynamic collections.

/**
 * Collection validation configuration for arrays
 */
interface CollectionValidation {
  $each: {
    [validationKey: string]: ValidationRule;
    $trackBy?: string | ((item: any) => string);
  };
}

/**
 * Validation state for collections includes $iter property
 */
interface CollectionValidationState extends ValidationState {
  /**
   * Iterator object providing access to individual element validations
   */
  readonly $iter: {
    [index: string]: ValidationState;
  };
}

Usage:

import { required, email, minLength } from 'vuelidate/lib/validators'

export default {
  data() {
    return {
      users: [
        { name: 'Alice', email: 'alice@example.com' },
        { name: 'Bob', email: 'bob@example.com' }
      ]
    }
  },
  
  validations: {
    users: {
      $each: {
        name: { required, minLength: minLength(2) },
        email: { required, email }
      }
    }
  },
  
  computed: {
    // Access validation for specific array elements
    firstUserErrors() {
      return this.$v.users.$iter[0]
    },
    
    // Check if any user has validation errors
    hasUserErrors() {
      return this.$v.users.$error
    }
  },
  
  methods: {
    // Add new user with automatic validation
    addUser() {
      this.users.push({ name: '', email: '' })
      // Validation automatically extends to new items
    },
    
    // Remove user (validation automatically adjusts)
    removeUser(index) {
      this.users.splice(index, 1)
    },
    
    // Validate all users
    validateAllUsers() {
      this.$v.users.$touch()
      return !this.$v.users.$invalid
    }
  }
}

Tracking with $trackBy

Stable tracking of array elements during reordering and modifications.

/**
 * Track array elements by a specific property or function
 */
interface TrackingOptions {
  $trackBy: string | ((item: any) => string);
}

Usage:

export default {
  data() {
    return {
      todos: [
        { id: 1, text: 'Learn Vuelidate', completed: false },
        { id: 2, text: 'Build awesome app', completed: false }
      ]
    }
  },
  
  validations: {
    todos: {
      $each: {
        // Track by ID to maintain validation state during reordering
        $trackBy: 'id',
        text: { required, minLength: minLength(3) }
      }
    }
  },
  
  methods: {
    // Reorder todos - validation state follows the tracked items
    moveTodoUp(index) {
      if (index > 0) {
        const item = this.todos.splice(index, 1)[0]
        this.todos.splice(index - 1, 0, item)
        // Validation state is preserved due to $trackBy
      }
    },
    
    // Dynamic tracking based on content
    validationsWithDynamicTracking() {
      return {
        items: {
          $each: {
            $trackBy: (item) => `${item.category}-${item.id}`,
            name: { required }
          }
        }
      }
    }
  }
}

Nested Object Validation

Validation of nested object structures with hierarchical validation state.

/**
 * Nested validation configuration
 */
interface NestedValidation {
  [propertyName: string]: {
    [validationKey: string]: ValidationRule | NestedValidation;
  };
}

Usage:

export default {
  data() {
    return {
      user: {
        personal: {
          firstName: '',
          lastName: '',
          birthDate: ''
        },
        contact: {
          email: '',
          phone: '',
          address: {
            street: '',
            city: '',
            postalCode: ''
          }
        }
      }
    }
  },
  
  validations: {
    user: {
      personal: {
        firstName: { required, minLength: minLength(2) },
        lastName: { required, minLength: minLength(2) },
        birthDate: { required }
      },
      contact: {
        email: { required, email },
        phone: { required },
        address: {
          street: { required },
          city: { required },
          postalCode: { required, minLength: minLength(5) }
        }
      }
    }
  },
  
  computed: {
    // Access nested validation states
    personalInfoValid() {
      return !this.$v.user.personal.$invalid
    },
    
    addressValid() {
      return !this.$v.user.contact.address.$invalid
    },
    
    contactSectionErrors() {
      const contact = this.$v.user.contact
      return {
        hasEmailError: contact.email.$error,
        hasPhoneError: contact.phone.$error,
        hasAddressError: contact.address.$error
      }
    }
  },
  
  methods: {
    // Validate specific sections
    validatePersonalInfo() {
      this.$v.user.personal.$touch()
      return !this.$v.user.personal.$invalid
    },
    
    validateContact() {
      this.$v.user.contact.$touch()
      return !this.$v.user.contact.$invalid
    }
  }
}

Group Validation

Reference-based validation for grouping related fields across different parts of the data structure.

/**
 * Group validation using array references
 */
interface GroupValidation {
  [groupKey: string]: string[] | string;
}

Usage:

export default {
  data() {
    return {
      billingAddress: {
        street: '',
        city: '',
        country: ''
      },
      shippingAddress: {
        street: '',
        city: '', 
        country: ''
      },
      sameAsShipping: false
    }
  },
  
  validations: {
    // Individual field validations
    billingAddress: {
      street: { required },
      city: { required },
      country: { required }
    },
    shippingAddress: {
      street: { required },
      city: { required },
      country: { required }
    },
    
    // Group validation - validates related fields together
    addressGroup: [
      'billingAddress.street',
      'billingAddress.city', 
      'billingAddress.country',
      // Conditionally include shipping address
      ...(this.sameAsShipping ? [] : [
        'shippingAddress.street',
        'shippingAddress.city',
        'shippingAddress.country'
      ])
    ]
  },
  
  computed: {
    allAddressFieldsValid() {
      return !this.$v.addressGroup.$invalid
    }
  },
  
  watch: {
    sameAsShipping(useShipping) {
      if (useShipping) {
        // Copy billing to shipping
        Object.assign(this.shippingAddress, this.billingAddress)
      }
      // Re-evaluate validations due to dynamic group composition
      this.$v.$touch()
    }
  }
}

Dynamic Collections

Advanced patterns for dynamic validation with changing data structures.

Usage:

export default {
  data() {
    return {
      formFields: [
        { type: 'text', name: 'username', value: '', required: true },
        { type: 'email', name: 'email', value: '', required: true },
        { type: 'number', name: 'age', value: '', required: false }
      ]
    }
  },
  
  // Dynamic validations based on field configuration
  validations() {
    const validations = {
      formFields: {
        $each: {
          $trackBy: 'name',
          value: {}
        }
      }
    }
    
    // Build validation rules based on field types
    this.formFields.forEach((field, index) => {
      const fieldValidations = {}
      
      if (field.required) {
        fieldValidations.required = required
      }
      
      switch (field.type) {
        case 'email':
          fieldValidations.email = email
          break
        case 'number':
          fieldValidations.numeric = numeric
          break
        case 'text':
          if (field.minLength) {
            fieldValidations.minLength = minLength(field.minLength)
          }
          break
      }
      
      validations.formFields.$each.value = fieldValidations
    })
    
    return validations
  },
  
  computed: {
    dynamicFormErrors() {
      const errors = []
      
      this.formFields.forEach((field, index) => {
        const validation = this.$v.formFields.$iter[index]
        if (validation && validation.value.$error) {
          errors.push({
            field: field.name,
            errors: this.getFieldErrors(validation.value, field)
          })
        }
      })
      
      return errors
    }
  },
  
  methods: {
    addField(fieldConfig) {
      this.formFields.push({
        type: fieldConfig.type,
        name: fieldConfig.name,
        value: '',
        required: fieldConfig.required || false
      })
    },
    
    removeField(index) {
      this.formFields.splice(index, 1)
    },
    
    getFieldErrors(validation, fieldConfig) {
      const errors = []
      
      if (fieldConfig.required && !validation.required) {
        errors.push(`${fieldConfig.name} is required`)
      }
      
      if (fieldConfig.type === 'email' && !validation.email) {
        errors.push(`${fieldConfig.name} must be a valid email`)
      }
      
      if (fieldConfig.type === 'number' && !validation.numeric) {
        errors.push(`${fieldConfig.name} must be a number`)
      }
      
      return errors
    }
  }
}

Complex Nested Arrays

Validation of arrays containing nested objects with their own collections.

Usage:

export default {
  data() {
    return {
      departments: [
        {
          name: 'Engineering',
          manager: { name: 'John Doe', email: 'john@company.com' },
          employees: [
            { name: 'Alice', position: 'Senior Developer' },
            { name: 'Bob', position: 'Junior Developer' }
          ]
        }
      ]
    }
  },
  
  validations: {
    departments: {
      $each: {
        $trackBy: 'name',
        name: { required, minLength: minLength(2) },
        manager: {
          name: { required },
          email: { required, email }
        },
        employees: {
          $each: {
            $trackBy: 'name',
            name: { required, minLength: minLength(2) },
            position: { required }
          }
        }
      }
    }
  },
  
  computed: {
    departmentSummary() {
      return this.departments.map((dept, deptIndex) => {
        const deptValidation = this.$v.departments.$iter[deptIndex]
        
        return {
          name: dept.name,
          isValid: !deptValidation.$error,
          managerValid: !deptValidation.manager.$error,
          employeeCount: dept.employees.length,
          validEmployees: dept.employees.filter((emp, empIndex) => 
            !deptValidation.employees.$iter[empIndex].$error
          ).length
        }
      })
    }
  },
  
  methods: {
    addDepartment() {
      this.departments.push({
        name: '',
        manager: { name: '', email: '' },
        employees: []
      })
    },
    
    addEmployee(deptIndex) {
      this.departments[deptIndex].employees.push({
        name: '',
        position: ''
      })
    },
    
    validateDepartment(index) {
      const deptValidation = this.$v.departments.$iter[index]
      deptValidation.$touch()
      return !deptValidation.$invalid
    }
  }
}

Install with Tessl CLI

npx tessl i tessl/npm-vuelidate

docs

collections.md

custom-validators.md

index.md

integration.md

validation-state.md

validators.md

tile.json