# Validate mixin

The validate mixin appends a reactive validation to the model data. It allows to dynamically validate data fields and provides validation error messages.

# Validation pattern

A validation pattern consists of two parts: a constant object with validation rules and a type of generated model validation property.

# Validation rule

Validation rule is a function that receives several parameters, including a value of the model data field, and returns a boolean result or an error message:

/**
 * A function type that checks a value from the data path and returns a result.
 *
 * @param value - checking data field
 * @param data - model data
 * @param path - field path
 * @returns - validation result
 */
type ValidationRule = (value: any, data: unknown, path: string[]) => boolean | string;

For example, let's write an age validation rule:

# Validation rules object

Rules should be placed into an object corresponding to the model data:

Rules of internal objects and arrays may be declared using $sub, $each, and $self reserved words.

  • $sub defines a rules set of the internal object fields
  • $each defines a rules set of each item of the internal array
  • $self defines a rule of the entire object or array

# Validation property type

A validation property type provides a type helper corresponding to the model data type. This type can be defined manually (e.g. for recursive types) and calculated automatically using PatternAssert.

The manual definition must be based on ValidationBase type:

import type { ValidationBase } from '@vueent/mix-models';

// ... definitions of the `Data` type and the `validations` object.

interface CredentialsValidations extends ValidationBase {
  readonly c: {
    firstName: ValidationBase;
    lastName: ValidationBase;
  };
}

interface PhoneValidations extends ValidationBase {
  readonly c: {
    value: ValidationBase;
  };
}

interface PhonesValidations extends ValidationBase {
  readonly c: PhoneValidations[];
}

interface Validations extends ValidationBase {
  readonly c: {
    age: ValidationBase;
    credentials: CredentialsValidations;
    phones: PhonesValidations;
  };
}

The calculated definifion uses a model data type and a validation rules object:

import type { PatternAssert } from '@vueent/mix-models';

// ... definitions of the `Data` type and the `validations` object.

type Validations = PatternAssert<typeof validations, Data>;

The two definitions above are equal.

# Model definition

The mixin provides a factory function that returns a mixin function. The following example presents a typical use of the validate mixin:

The mixin is most useful with TypeScript, because it allows a developer to use the validations autocompletion.

# Usage

A model instance with validate mixin has two equal properties validations and v. The validations property is of type ValidationBase, while the v property is of user-defined type Validations (see above):

const instance = create();

instance.v.c. // | Autocomplition
//               | age
//               | credentials
//               | phones

instance.v.c.phones.c[0].c. // | Autocomplition
//                             | value

# Nesting levels with flags, options, and methods

v/validations object is divided into levels according to the data model structure and is synchronously updated when it changes. Because any validated field contains many flags, options, and methods, each sublevel is accessible through a c or an untyped children property. Every validation object (including the root property v/validations) contains the following flags and methods:

  • c/children - children validations
  • anyChildDirty - indicating that at least one of children has the dirty flag
  • selfDirty - indicating that the current data field is dirty
  • dirty - indicating that the current data field or one of children is dirty
  • anyChildInvalid - indicating that at least on of children has the invalid flag
  • selfInvalid - indicating that validation of the current data field failed
  • invalid - indicating the validation of the current data field or one of children failed
  • message - validation error test
  • dirtyMessage - validation error text, which is specified only if the dirty flag is set
  • touch() - marks the current data field is dirty
  • reset() - resets the current validation state to default values
  • checkValue() - compares the specified value with the cached value of the current data field
  • destroy() - destroys the current validation instance and its children (should not be called manually)

The following example demonstrates a usage of the model, defined above:

const instance = create();

console.log(instance.v.c.age.dirty); // outputs: false

instance.data.age = 30;

console.log(instance.v.c.age.dirty); // outputs: false
console.log(instance.v.c.age.invalid); // outputs: false

instance.v.c.age.touch();

console.log(instance.v.c.age.dirty); // outputs: true
console.log(instance.v.c.age.invalid); // outputs: false
console.log(instance.v.c.age.message); // outputs: ''
console.log(instance.v.c.age.dirtyMessage); // outputs: ''

instance.data.age = -1;

console.log(instance.v.c.age.dirty); // outputs: true
console.log(instance.v.c.age.invalid); // outputs: true
console.log(instance.v.c.age.message); // outputs: "Age cannot be a negative value."
console.log(instance.v.c.age.dirtyMessage); // outputs: "Age cannot be a negative value."

// do not use push and splice, it breaks reactivity.
instance.data.phones = [...instance.data.phones, { value: '+155533344' }];

// some children are invalid.
console.log(instance.v.invalid); // outputs: true
console.log(instance.v.dirty); // outputs: true

// autotouch is not enabled.
console.log(instance.v.c.phones.c[0].c.value.dirty); // outputs: false
console.log(instance.v.c.phones.c[0].c.value.invalid); // outputs: true
console.log(instance.v.c.phones.c[0].c.value.dirtyMessage); // outputs: ""

instance.v.c.phones.c[0].c.value.touch();

console.log(instance.v.c.phones.c[0].c.value.dirty); // outputs: true
console.log(instance.v.c.phones.c[0].c.value.dirtyMessage); // outputs: "Invalid phone number format."

instance.data.phones[0].value = '+15553334411';

console.log(instance.v.c.phones.c[0].c.value.invalid); // outputs: false

instance.v.reset(); // reset validations

console.log(instance.v.dirty); // outputs: false

instance.destroy();

# Combination with the Save and the Rollback mixins

Validation properties will be reset when the model instance is saved or rolled back.

# v9s integration

Our side project v9s (opens new window) can be integrated with the validate mixin to make model definition more declarative. For example, let's rewrite the model from the page: