# Validation
validationcomposable inspired by vuelidate (opens new window)
WARNING
Currently there's no exported validators.
# Parameters
import { useValidation } from "vue-composable";
const form = useValidation(options);
| Parameters | Type | Required | Default | Description | 
|---|---|---|---|---|
| options | Object |  true |  Validation input object | 
# State
The useValidation function exposes the following reactive state:
import { useValidation } from "vue-composable";
const form = useValidation(options);
| State | Type | Description | 
|---|---|---|
| form | Reactive<Options & Validation<Object>> |  Reactive form validation object | 
WARNING
The returned value is an reactive() object, do not deconstruct it.
# Input
The input object can be an ValidationObject or a nested dictionary of ValidationObject.
# ValidationObject
ValidationObject is composed by $value(ref<any>|any) and validators((o: ref<any>|any, ctx: object)=>Promise<boolean>|boolean).
// function validator
type ValidatorFunction<T, TContext = any> = (
  model: T,
  ctx: TContext
) => boolean | Promise<boolean>;
// validator object with message
type ValidatorObject<T> = {
  $validator: ValidatorFunction<T>;
  $message: RefTyped<string>;
};
// typed validationObject
type ValidationObject<T> = {
  $value: T | Ref<T>;
  $touch(): void;
  $reset(): void;
} & Record<string, ValidatorFunction<T> | ValidatorObject<T>>;
const validationUsername = useValidation({
  $value: ref(""),
  // required validator, the first argument, is `$value` unwrapped
  // second argument is the context, equivalent to `validationUsername`
  required(v, context) {
    return !!v;
    // or
    return !!context.$value;
  },
  // canBeTaken validator, returns a promise
  canBeTaken(v) {
    return api.get("username/check/" + v); // Promise
  },
  containsInvalidWords: {
    $validator(v) {
      return api.get("username/invalid/" + v);
    },
    $message: `This username contains improper words`,
    // custom properties
    $customProp: "custom",
  },
  // custom properties
  $placeholder: "Username", // it will be unchanged, because it starts with `$`
});
TIP
You can store any value you want, by using $ as the first letter of the property name.
{
  $value: ref(''),
  $myBag: { awesome: 'property'},
  required, // validator
}
# Return
It will return an reactive object.
interface ValidationValue<T> {
  $value: T;
  $dirty: boolean; // dirty is set to true when `$value` changes for the first time
  $anyInvalid: boolean; // any validation invalid
  $errors: any[]; // array of errors
  toObject(): T;
  $touch(): void;
  $reset(): void;
}
// validator
interface ValidatorResult {
  $error: any;
  $invalid: boolean;
}
interface ValidatorResultPromise {
  $pending: boolean;
  $promise: Promise<boolean> | null;
}
interface ValidatorResultMessage {
  $message: string;
}
On the example above, the result will be:
validationUsername.$value; // access to value,
validationUsername.$dirty; // was it modified
// validators
// common
validationUsername.required.$error;
validationUsername.required.$invalid; // true if the return from the validator is false
// promise
validationUsername.canBeTaken.$pending; // is promise still executing
validationUsername.canBeTaken.$promise; // access to the internal promise
// validator object
// contains the same properties has the previous and adds $message
validationUsername.containsInvalidWords.$message; // message
validationUsername.containsInvalidWords.$customProp; //custom prop
// custom properties
validationUsername.$placeholder; // custom prop
// retrieve value object
validationUsername.toObject(); // returns string
# NestedValidationObject
The validation composable allows you to have as many nested objects as you want, the only requirement is it ends on a ValidationObject;
interface ValidationGroupResult {
  $anyDirty: boolean;
  $errors: Array<any>;
  $anyInvalid: boolean;
}
const form = useValidation({
  settings: {
    email: {
      $value: ref(""),
    },
    // ...etc
  },
  personal: {
    name: {
      first: {
        $value: ref(""),
        // validators...
      },
      last: {
        $value: ref(""),
        // validators...
      },
    },
    // ...etc
  },
});
form.$anyDirty;
form.$anyInvalid;
form.$errors;
form.settings.$anyDirty;
form.settings.$anyInvalid;
form.settings.$errors;
form.personal.$anyDirty;
form.personal.$anyInvalid;
form.personal.$errors;
form.toObject(); // returns { settings: { email: '' }, personal: { name: { first: '', last: '' } } }
form.$touch(); // sets all the validations to `$dirty: true`
form.$reset(); // sets all the validations to `$dirty: false`
interface ValidationGroupResult {
  $anyDirty: boolean;
  $errors: Array<any>;
  $anyInvalid: boolean;
}
# Example
Form validation
# Code
<template>
  <div class="about">
    <h1>Form validation</h1>
    <form @submit="onSubmit">
      <input v-model="form.firstName.$value" placeholder="firstName" />
      <input v-model="form.lastName.$value" placeholder="lastName" />
      <input v-model="form.password.$value" placeholder="password" />
      <input v-model="form.samePassword.$value" placeholder="password2" />
      <p v-if="form.samePassword.$dirty && form.samePassword.match.$invalid">
        {{ form.samePassword.match.$message }}
      </p>
      <br />
      <input
        type="submit"
        v-model="submitText"
        :class="{
          invalid: form.$anyDirty && form.$anyInvalid,
          dirty: form.$anyDirty && !form.$anyInvalid,
          error: form.$errors.length > 0,
        }"
      />
    </form>
  </div>
</template>
<script>
import { defineComponent, ref, reactive, computed } from "@vue/composition-api";
import { useValidation } from "vue-composable";
const required = (x) => !!x;
export default defineComponent({
  setup() {
    const name = ref("");
    const surname = ref("");
    const password = ref("");
    const form = useValidation({
      firstName: {
        $value: name,
        required,
      },
      lastName: {
        $value: surname,
        required,
      },
      password: {
        $value: password,
        required: {
          $validator: required,
          $message: ref("password is required"),
        },
      },
      samePassword: {
        $value: ref(""),
        match: {
          $validator(x) {
            return x === password.value;
          },
          $message: "Password don't match",
        },
      },
    });
    const submitText = computed(() => {
      if (form.$anyDirty && form.$anyInvalid) {
        return "Invalid form";
      }
      if (!form.$anyDirty) {
        return "Please populate the form";
      }
      if (form.$errors.length > 0) {
        console.log(form.$errors);
        return "Error";
      }
      return "Submit";
    });
    const onSubmit = (e) => {
      e.preventDefault();
      if (form.$anyInvalid) {
        alert("invalid form");
      } else {
        const o = form.toObject();
        alert(`submit form "${JSON.stringify(o)}"`);
        console.log("submitted", o);
      }
    };
    return {
      onSubmit,
      submitText,
      form,
    };
  },
});
</script>
<style scoped>
.invalid {
  color: #e0e0e0;
  background: #282c34;
  border: none;
  outline: none;
  border-radius: 3px;
  padding: 0.3rem;
  margin: 0.5rem auto;
}
.dirty {
  color: #ffff92;
  background: #282c34;
  border: none;
  padding: 0.3rem;
  border-radius: 3px;
  margin: 0.5rem auto;
  outline: none;
}
.error {
  color: #fb686c;
  background: #282c34;
  padding: 0.3rem;
  margin: 0.5rem auto;
  border: none;
  outline: none;
  border-radius: 3px;
}
</style>