# Validation

validation composable 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>