Skip to content
This repository has been archived by the owner on Jan 9, 2022. It is now read-only.

Documentation

JensDll edited this page Dec 10, 2021 · 63 revisions

Getting Started

Install vue3-form-validation with your favorite package manager:

yarn add vue3-form-validation
# or with npm
npm install vue3-form-validation

Configure the validation through createValidation and pass it to the app:

import { createValidation } from 'vue3-form-validation'

const validation = createValidation({
  defaultValidationBehavior: 'lazy',
  validationBehavior: {
    change: ({ force }) => !force,
    lazy: ({ touched }) => touched,
    submit: ({ submit, hasError }) => submit || hasError
  }
})

app.use(validation)

Start using useValidation in your components.

API Reference

At its core, this package exports one function useValidation, plus some type definitions for when using TypeScript.

import { useValidation } from 'vue3-form-validation'

const {
  form,
  submitting,
  validating,
  errors,
  hasError,
  validateFields,
  resetFields,
  add,
  remove
} = useValidation({})

useValidation takes the following parameters

  • formData {object} The structure of your formData

FormData has a structure similar to any other object you would write for v-model data binding, the only difference being that together with every property, you can provide rules to display validation errors. Let's look at an example:

const formData = {
  email: '',
  password: ''
}

The above can be converted to the following:

const formData = {
  email: {
    $value: '',
    $rules: [email => !email && 'Please enter your email address']
  },
  password: {
    $value: '',
    $rules: [
      password =>
        password.length > 7 || 'Password has to be longer than 7 characters'
    ]
  }
}

FormData can contain arrays and can be deeply nested. At the leaf level, the object should contain fields whose simplified type definition looks like the following:

type Field<T> = {
  $value: T | Ref<T>
  $rules?: Rule<T>[]
}

useValidation exposes the following state

  • form {object} A transformed formData object
  • submitting {Ref<boolean>} True during validation after calling validateFields when there were rules returning a Promise
  • validating {ComputedRef<boolean>} True while the form has any pending rules
  • errors {ComputedRef<string[]>} All current validation error messages
  • hasError {ComputedRef<boolean>} True if the form has any error

Form is a reactive object with identical structure as the formData input but with added metadata to every field. All objects with a $value property are converted to an object of the following type:

type TransformedField<T> = {
  $uid: number
  $value: T
  $errors: string[]
  $rawErrors: (string | null)[]
  $hasError: boolean
  $validating: boolean
  $touched: boolean
  $dirty: boolean
  $validate(options?: ValidateOptions): void
}

Given the data of the previous example, this would result in the structure below:

type Form = {
  email: TransformedField<string>
  password: TransformedField<string>
}

Listed as follows is a description of all the properties and their use case:

  • $uid {number} The unique id of the field. For dynamic forms, this can be used as the key attribute in v-for
  • $value {any} The current field's value. Intended to be used together with v-model
  • $errors {string[]} A list of validation error messages local to the field without null values
  • $rawErrors {(string | null)[]} The field's raw error messages one for each rule and null if there is no error. Allows you to check if there is a specific rule with an error instead of any
  • $hasError {boolean} True while the field has any errors
  • $validating {boolean} True while the field has any pending rules
  • $touched {boolean} True if the field is touched. In most cases, this value should be set together with the blur event. Either through $validate or manually
  • $dirty {boolean} True if the $value of the field has changed at least once
  • $validate {function} Validate the field

$validate([options])

  • options {object} Options to use for validation
    • setTouched {boolean} Set the field touched when called
      • Default
        true
    • force {boolean} Validate with the force flag set (see validation behavior)
      • Default
        true
  • Returns: {Promise<void>} A Promise that is pending until every asynchronous rule is done validating (or resolve directly for synchronous rules)

useValidation exposes the following methods

validateFields([options])

Validate all fields.

  • options {object} Options to use for validation
    • names {string[]} A list of field names to validate. Names are taken from the properties in the formData
      • Default
        undefined
        Meaning validate all
    • predicate {function} A filter function to determine which values to keep in the resulting formData. Used like Array.prototype.filter but for objects
      • Default
        () => true
        Meaning keep all
  • Returns: {Promise} A Promise containing the resulting formData
  • Throws: {ValidationError} An Error when there were any failing rules

predicate([iteratorResult])

  • iteratorResult {object}
    • key {string} The current key
    • value {any} The current value
    • path {string[]} The current path in the object
  • Returns: {any} Return anything or true if the value should be kept

resetFields([formData])

Reset all fields to their default value or pass an object to set specific values.

  • formData {object} FormData to set specific values. It has the same structure as the object passed to useValidation
    • Default
      undefined
      Meaning use the default values

Remarks

It will not create any new fields not present in the formData initially.

add(path, value)

Adds a new property to the formData.

  • path {(string | number)[]} A path where the new data is added
  • value {any} The data to add

Remarks

Objects with a $value property are transformed, as seen above.

remove(path)

Removes a property from the formData.

  • path {(string | number)[]} The path of the property to remove

Writing Rules

Rules are functions that should return a string when the validation fails. They can be written purely as a function or together with a key property in an object. They can also return a Promise when you have a rule that requires asynchronous code. Here is an example:

useValidation({
  name: {
    $value: '',
    $rules: [
      /**
       *  This is a simple rule.
       *  If name does not exist, return the error message.
       */
      name => !name && 'Please enter your name'
    ]
  },
  password: {
    $value: '',
    $rules: [
      /**
       *  This is a keyed rule.
       *  Keyed rules will be executed together. It will receive both
       *  password as well as confirmPassword as arguments
       *  because they share the same key 'pw'.
       *  If both do not match, return the error message.
       */
      {
        key: 'pw',
        rule: (password, confirmPassword) =>
          password !== confirmPassword && 'Passwords do not match'
      }
    ]
  },
  confirmPassword: {
    $value: '',
    $rules: [
      {
        key: 'pw',
        rule: (password, confirmPassword) =>
          password !== confirmPassword && 'Passwords do not match'
      }
    ]
  }
})

ℹ️ Keyed Rules will only be called after all connected fields have been touched. This will prevent overly aggressive error messages where the user did not have time yet to interact with all the validated inputs.

General-purpose rules can be written once and then imported as needed. This will reduce duplication for common validation use cases. Take the following rules:

const rules = {
  required: msg => x => !x && msg,
  min: min => msg => x => x.length >= min || msg,
  max: max => msg => x => x.length <= max || msg,
  minMax: (min, max) => msg => x => (min <= x.length && x.length <= max) || msg,
  email: msg => x => /\S+@\S+\.\S+/.test(x) || msg,
  equal:
    msg =>
    (...xs) =>
      xs.every(x => x === xs[0]) || msg
}

And then in a component:

useValidation({
  field: {
    $value: '',
    $rules: [
      rules.required('Error message'),
      rules.min(1)('Error message'),
      rules.max(15)('Error message'),
      rules.minMax(1, 15)('Error message'),
      rules.email('Error message'),
      rules.equal('Error message')
    ]
  }
})

Validation Behavior

Rules can be extended with validation behavior to customize how the validation should operate. This is done through ValidationBehaviorFunctions (VBF). By default, useValidation will try to execute every field's rule whenever the $value property changes. That is where VBFs come into play. The following diagram summarizes the validation steps:

               ┌──────────────────────────────────────────────┐
               │ The watcher for $value has detected a change │
               │ or the $validate method was called           │
               └──────────────────────┬───────────────────────┘
                                      │
                                      ▼
      ┌─────────────────────────────────────────────────────────────────┐
      │ Invoke the paired VBF of every rule,                            │
      │ passing some information about how the validation was triggered │
      └───────────────────────────────┬─────────────────────────────────┘
                                      │
                                      ▼
           ┌─────┐       ┌──────────────────────────┐       ┌────┐
           │ YES ├───────┤ Did the VBF return true? ├───────┤ NO │
           └──┬──┘       └──────────────────────────┘       └──┬─┘
              │                                                │
              ▼                                                ▼
┌─────────────────────────┐                             ┌────────────┐
│ Execute the paired rule │                             │ Do nothing │
└─────────────────────────┘                             └────────────┘

VBF(validationBehaviorInfo)

Used to customize the validation behavior.

  • validationBehaviorInfo {object}
    • hasError {boolean} True if the paired rule of this behavior has an error
    • touched {boolean} The touched state of the field
    • dirty {boolean} The dirty state of the field
    • force {boolean} True if the validation was triggered with the force flag (see $validate)
    • submit {boolean} True if the validation was triggered by validateFields
    • value {any} The $value of the field
  • Returns: {any} Return true if the validation should be triggered

Combining these options gives flexibility on how rules are executed. Its common to have shared validation behavior across different inputs. General purpose VBFs can be registered globally in createValidation:

const validation = createValidation({
  defaultValidationBehavior: 'lazier',
  validationBehavior: {
    change: ({ force }) => !force,
    lazy: ({ touched }) => touched,
    lazier: ({ force, touched, submit, hasError }) =>
      force || submit || (touched && hasError),
    submit: ({ submit, hasError }) => submit || hasError
  }
})

app.use(validation)

Every VBF is assigned a string, and a default validation behavior is given. In useValidation they can be passed alongside the rule:

useValidation({
  field: {
    $value: '',
    $rules: [
      // This will use the default validation behavior
      rules.required('Error message'),
      // Only call this on submit and afterwards if there is an error
      ['submit', rules.min(1)('Error message')],
      // Or pass a VBF directly instead of using a string
      [({ force }) => !force, rules.max(15)('Error message')]
    ]
  }
})

You can name the behaviors whatever you like. Below is a description of the examples above:

Change

The validation will trigger on change of $value and after calling validateFields.

Lazy

Do not validate until the field is touched and afterwards be aggressive.

Lazier

Like lazy, but the validation is only aggressive until the error is resolved. Afterwards, the behavior goes back to being lazy.

Submit

The validation is first triggered after calling validateFields and then only if there is an error.

Debounce the execution of a rule

Rules can be extended further by adding a debounce duration. It will delay invoking rule until after duration milliseconds have elapsed. Were the rule to be called again in that time frame, only the last attempt is considered. Take the following example:

useValidation({
  name: {
    $value: '',
    $rules: [
      [
        'change', // Will be validated frequently
        name => {
          // Query some API to see if the name is available
        },
        300 // A debounce duration of 300 ms
      ]
    ]
  }
})

Debouncing a rule is useful for reducing the frequency in which it is executed. It makes sense for async rules that require access to some external resource like an API to reduce the number of server calls but still give frequent feedback.

⚠️ Use debounce only for async rules.

TypeScript Support

For type inference in useValidation make sure to define the structure of your formData upfront and pass it as the generic parameter FormData. Fields have to be annotated by the Field type:

type FormData = {
  field: Field<string>
}

useValidation<FormData>({
  field: {
    $value: '',
    $rules: []
  }
})

Fields can have arbitrary properties. These are passed as the second generic argument to Field in object form:

type FormData = {
  field: Field<string, { extra: string; stuff: number }>
}

const { form } = useValidation<FormData>({
  field: {
    $value: '',
    $rules: [],
    extra: 'foo',
    stuff: 42
  }
})

form.field.extra // 'foo'
form.field.stuff // 42

Validation Behavior

To extend the default VBFs there is a special interface that needs to be declared. Somewhere in your project put the following in a .ts or .d.ts file:

declare module 'vue3-form-validation' {
  interface CustomValidationBehaviorFunctions {
    change: any
    lazy: any
    lazier: any
    submit: any
  }
}

The keys of the interface need to be implemented in createValidation:

createValidation({
  defaultValidationBehavior: 'lazier',
  validationBehavior: {
    change: ({ force }) => !force,
    lazy: ({ touched }) => touched,
    lazier: ({ force, touched, submit, hasError }) =>
      force || submit || (touched && hasError),
    submit: ({ submit, hasError }) => submit || hasError
  }
})

Caveats

The parameter type for inline VBFs can not be inferred:

type FormData = {
  field: Field<string>
}

useValidation<FormData>({
  field: {
    $value: '',
    $rules: [
      [
        // Annotation is necessary here
        (info: ValidationBehaviorInfo) => !info.force,
        () => 'Error message'
      ]
    ]
  }
})