vuex-module-validatable-state

Simple Vuex module to handle form fields and validations.

vuex-module-validatable-state

You can build a view model for your form, which runs valdations easily. You just provide initial fields and validators to build the module, then map getters/actions to components.

Usage

Installation

$ npm i vuex-module-validatable-state

Register to core Vuex module

This module provides the function to return Vuex module as default. The function takes arguments:

  • Initial field set
  • Validators
A. Define directly
import validatableModule from "vuex-module-validatable-state";

const initialFields = {
  amount: null,
  description: "default text"
};

const validators = {
  amount: [
    ({ amount }) => amount === null ? "Require this" : false
  ],
  description: [
    ({ description }) => description.length > 15 ? "Should be shorter than 15" : false,
    ({ description, amount }) => description.indexOf(amount.toString())  ? "Should include amount" : false,
  ]
};

new Vuex.Store({
  modules: {
    myForm: {
      namespaced: true
      store,
      getters,
      actions,
      mutations,
      modules: {
        ...validatableModule(initialFields, validators) // <-- HERE
      }
    }
  }
});
B. Register to existing module
import { register } from "vuex-module-validatable-state";

const initialFields = {
  amount: null,
  description: "default text"
};

const validators = {
  amount: [
    ({ amount }) => amount === null ? "Require this" : false
  ],
  description: [
    ({ description }) => description.length > 15 ? "Should be shorter than 15" : false,
    ({ description, amount }) => description.indexOf(amount.toString())  ? "Should include amount" : false,
  ]
};

const store = new Vuex.Store({
  modules: {
    myForm: {
      namespaced: true
      store,
      getters,
      actions,
      mutations
    }
  }
});

register(store, "myForm", initialFields, validators);

Map to Components

Provided Getters

Getter name Returns
GetterTypes.ALL_FIELDS_VALID boolean whether all fields don't have error
GetterTypes.FIELD_VALUES All fields as { [fieldName]: value }
GetterTypes.FIELD_ERRORS All errors as { [fieldName]: errorMessage }
GetterTypes.FIELD_EDITABILITIES All editable flags as { [fieldName]: editability }
GetterTypes.FIELD_DIRTINESSES All dirtiness flags as { [fieldName]: dirtiness }
GetterTypes.ANY_FIELD_CHANGED boolean whether all fields are not dirty

Provided Actions

Import ActionTypes from the module.

Action name Runs
ActionTypes.SET_FIELD Set value for a field, then runs validation if enabled
ActionTypes.SET_FIELDS_BULK Set values for fields at once, then make all dirtiness flags false
ActionTypes.RESET_FIELDS Reset values on field with initial values
ActionTypes.ENABLE_ALL_VALIDATIONS Enable interactive validation and run validations for all fields
ActionTypes.VALIDATE_FIELD_VALUE Validate specific field
ActionTypes.VALIDATE_FIELDS Validate all fields
ActionTypes.SET_FIELDS_EDITABILITY Set editability flag for a field, disabled field is not updated nor validated
ActionTypes.SET_FIELDS_PRISTINE Make all dirtiness flags false

Validators

You can pass validators when you initialize the module.

const validators = {
  amount: [/* validators for filling error against to amount */],
  description: [/* validators for filling error against to description */]
}

Each validator can take all fields values to run validation:

const validators = {
  amount: [
    ({ amount, description }) => /* return false or errorMessage */
  ]
}

Optionally, can take getters on the store which calls this module:

const validators = {
  description: [
    ({ description }, getters) => getters.getterOnStore && validationLogicIfGetterOnStoreIsTruthy(description)
  ]
}

And you can request "interactive validation" which valites every time dispatch(ActionTypes.SET_FIELD) is called

const validators = {
  amount: [
    [({ amount }, getters) => /* validator logic */, { instant: true }]
  ]
}

Provided Typings

You can import handy type/interface definitions from the module.
The generic T in below expects fields type like:

interface FieldValues {
  amount: number;
  description: string;
}

getters[GetterTypes.FIELD_VALUES] returns values with following FieldValues interface.

<summary>See all typings</summary>

ValidatorTree<T>

As like ActionTree, MutationTree, you can receive type guards for Validators. By giving your fields' type for Generics, validator can get more guards for each fields:

image

SetFieldAction<T>

It's the type definition of the payload for dispatching ActionTypes.SET_FIELD, you can get type guard for your fields by giving Generics.

image

FieldValidationErrors<T>

Type for getters[GetterTypes.FIELD_ERRORS]

FieldEditabilities<T>

Type for getters[GetterTypes.FIELD_EDITABILITIES]

FieldDirtinesses<T>

Type for getters[GetterTypes.FIELD_DIRTINESSES]

Working Sample

Edit Sample: vuex-module-validatable-state

Registering to Vuex Store

const initialField = {
  amount: 0,
  description: null
};

const validators = {
  amount: [
    ({ amount }) => (!amount ? "Amount is required" : false),
    ({ amount }) => (amount <= 0 ? "Amount should be greater than 0" : false)
  ],
  description: [
    ({ amount, description }) =>
      amount > 1000 && !description
        ? "Description is required if amount is high"
        : false
  ]
};

const store = new Vuex.Store({
  modules: {
    ...theModule(initialField, validators)
  }
});

Mapping to Component

<template>
  <form>
    <div>
      <label for="amount">Amount (Required, Positive)</label>
      <input type="number" name="amount" v-model="amount">
      <span v-if="errors.amount">{{ errors.amount }}</span>
    </div>
    <div>
      <label for="description">Description (Required if amount is greater than 1000)</label>
      <textarea name="description" v-model="description"/>
      <span v-if="errors.description">{{ errors.description }}</span>
    </div>
    <button @click.prevent="submit">Validate and Submit</button>
  </form>
</template>

<script>
import { GetterTypes, ActionTypes } from "vuex-module-validatable-state";

export default {
  name: "App",
  computed: {
    amount: {
      get() {
        return this.$store.getters[GetterTypes.FIELD_VALUES].amount;
      },
      set(value) {
        this.$store.dispatch(ActionTypes.SET_FIELD_VALUE, {
          name: "amount",
          value
        });
      }
    },
    description: {
      get() {
        return this.$store.getters[GetterTypes.FIELD_VALUES].description;
      },
      set(value) {
        this.$store.dispatch(ActionTypes.SET_FIELD_VALUE, {
          name: "description",
          value
        });
      }
    },
    errors() {
      return this.$store.getters[GetterTypes.FIELD_ERRORS];
    }
  },
  methods: {
    submit() {
      this.$store.dispatch(ActionTypes.ENABLE_ALL_VALIDATIONS).then(() => {
        if (this.$store.getters[GetterTypes.ALL_FIELDS_VALID]) {
          alert("Form is valid, so now submitting!");
          this.$store.dispatch(ActionTypes.SET_FIELDS_PRISTINE);
        }
      });
    }
  }
};
</script>

GitHub