de-formed-validations
Version:
Function-based modular validations
323 lines (303 loc) • 10.1 kB
text/typescript
import { ChangeEvent, useState, useEffect, useCallback } from 'react';
import {
ValidationSchema,
ValidationState,
CustomValidation,
ValidationFunction,
ValidationObject,
ResetValidationState,
ForceValidationState,
Validate,
ValidateCustom,
ValidateIfTrue,
ValidateOnBlur,
ValidateOnChange,
GetFieldValid,
GetAllErrors,
GetError,
ValidateAll,
} from './types';
import { compose, prop, all, isPropertyValid } from '../utilities';
import { map, reduce, converge, mergeRight, head } from 'ramda';
/**
* A hook that can be used to generate an object containing functions and
* properties pertaining to the validation state provided.
* @param validationSchema an object containing all the properties you want to validate
* @returns validationObject { forceValidationState, getError, getAllErrors, getFieldValid, isValid, resetValidationState, validate, validateAll, validateIfTrue, validateOnBlur, validateOnChange, validationState }
*/
export const useValidation = <S>(
validationSchema: ValidationSchema<S>,
): ValidationObject<S> => {
// -- Build Validation State Object -------------------------------------
const createValidationsState = (schema: ValidationSchema<S>) => {
return reduce(
(acc: ValidationState, key: keyof S) => ({
...acc,
[key]: {
isValid: true,
errors: [],
},
}),
{},
Object.keys(schema) as (keyof S)[],
);
};
// -- isValid and validationState ---------------------------------------
const [isValid, setIsValid] = useState<boolean>(true);
const [validationState, setValidationState] = useState<ValidationState>(
createValidationsState(validationSchema),
);
const [validationErrors, setValidationErros] = useState<string[]>([]);
/**
* Resets the validation state.
*/
const resetValidationState: ResetValidationState = (): void =>
compose(setValidationState, createValidationsState)(validationSchema);
/**
* Overrides the existing validation state with another.
* @param newValidationState ValidationState
*/
const forceValidationState: ForceValidationState = (
newValidationState: ValidationState,
): void => {
setValidationState(newValidationState);
};
/**
* Executes the value against all provided validation functions and updates
* the validation state.
* @param key string the name of the property being validated
* @param value any the value to be tested for validation
* @return true/false validation
*/
const runAllValidators = (
property: keyof S,
value: any,
state?: S,
): ValidationState => {
const localState = state ? state : ({} as S);
const runValidator = compose(
(func: ValidationFunction<S>) => func(value, localState),
prop('validation'),
);
const bools: boolean[] = map(
runValidator,
prop(property, validationSchema),
);
const allValidationsValid: boolean = all(bools);
const errors = bools.reduce((acc: string[], curr: boolean, idx: number) => {
const errorOf = compose(prop('errorMessage'), prop(idx), prop(property));
return curr ? acc : [...acc, errorOf(validationSchema)];
}, []);
return {
[property]: {
isValid: allValidationsValid,
errors: allValidationsValid ? [] : errors,
},
};
};
/**
* Executes a validation function on a value and updates the validation state.
* @param property string the name of the property being validated
* @param value any the value to be tested for validation
* @return boolean
*/
const validate: Validate<S> = (
property: keyof S,
value: unknown,
state?: S,
): boolean => {
if (property in validationSchema) {
const validations = runAllValidators(property, value, state);
const updated = mergeRight(validationState, validations);
setValidationState(updated);
return isPropertyValid(property, validations);
}
return true;
};
/**
* Takes a unique data set and runs them against the defined schema. Only use
* if you need to run validations on data where the validation props are
* unable to follow the names of the properties of an object. Will return a
* boolean and update validation state.
* @param customValidations CustomValidation[]
* @return boolean
*/
const validateCustom: ValidateCustom = (
customValidations: CustomValidation[],
): boolean => {
const zip = converge(runAllValidators, [
prop('key'),
prop('value'),
prop('state'),
]);
const state = reduce(
(acc: any, current: CustomValidation) => {
return mergeRight(acc, zip(current));
},
{},
customValidations,
);
setValidationState(state);
return allValid(state);
};
/**
* Updates the validation state if the validation succeeds.
* @param key string the name of the property being validated
* @param value any the value to be tested for validation
* @return boolean
*/
const validateIfTrue: ValidateIfTrue<S> = (
property: keyof S,
value: unknown,
state?: S,
): boolean => {
if (property in validationSchema) {
const validations = runAllValidators(property, value, state);
if (isPropertyValid(property, validations)) {
setValidationState(mergeRight(validationState, validations));
}
return isPropertyValid(property, validations);
}
return true;
};
/**
* Create a new onBlur function that calls validate on a property matching the
* name of the event whenever a blur event happens.
* @param state the data controlling the form
* @return function :: (event: any) => any
*/
const validateOnBlur: ValidateOnBlur<S> = (state: S) => (
event: ChangeEvent<HTMLInputElement>,
): void => {
const { value, name } = event.target;
validate(name as keyof S, value, state);
};
/**
* Create a new onChange function that calls validateIfTrue on a property
* matching the name of the event whenever a change event happens.
* @param onChange function to handle onChange events
* @param state the data controlling the form
* @return function :: (event: any) => any
*/
const validateOnChange: ValidateOnChange<S> = (
onChange: (event: any) => any,
state: S,
) => (event: any): unknown => {
const { value, name } = event.target;
validateIfTrue(name as keyof S, value, state);
return onChange(event);
};
/**
* Runs all validations against an object with all values and updates/returns
* isValid state.
* @param state any an object that contains all values to be validated
* @param props string[] property names to check (optional)
* @return boolean
*/
const validateAll: ValidateAll<S> = (
state: S,
props: (keyof S)[] = Object.keys(validationSchema) as (keyof S)[],
): boolean => {
const newState = reduce(
(acc: ValidationState, property: keyof S) => {
const r = runAllValidators(property, prop(property, state), state);
return mergeRight(acc, r);
},
{},
props,
);
const updated = mergeRight(validationState, newState);
setValidationState(updated);
return allValid(newState);
};
/**
* Get the current error stored for a property on the validation object.
* @param property the name of the property to retrieve
* @return string
*/
const getAllErrors: GetAllErrors<S> = (
property: keyof S,
vState: ValidationState = validationState,
): string[] => {
if (property in validationSchema) {
const val = compose(prop('errors'), prop(property));
return val(vState);
}
return [];
};
/**
* Get the current error stored for a property on the validation object.
* @param property the name of the property to retrieve
* @return string
*/
const getError: GetError<S> = (
property: keyof S,
vState: ValidationState = validationState,
): string => {
if (property in validationSchema) {
const val = compose(head, prop('errors'), prop(property));
return val(vState) ? val(vState) : '';
}
return '';
};
/**
* Get the current valid state stored for a property on the validation object.
* If the property does not exist on the validationSchema getFieldValid will
* return true by default.
* @param property the name of the property to retrieve
* @return boolean
*/
const getFieldValid: GetFieldValid<S> = (
property: keyof S,
vState: ValidationState = validationState,
): boolean => {
if (property in validationSchema) {
const val = compose(prop('isValid'), prop(property));
return val(vState);
}
return true;
};
// -- helper to determine if a new validation state is valid ------------
const allValid = (state: ValidationState): boolean => {
return reduce(
(acc: boolean, curr: keyof S) => {
return acc ? isPropertyValid(curr, state) : acc;
},
true,
Object.keys(state) as (keyof S)[],
);
};
// -- helper to build array of errors when validation state is invalid ---
const generateValidationErrors = (state: ValidationState): string[] => {
return reduce(
(acc: string[], curr: keyof S) => {
return getError(curr) ? [...acc, getError(curr) as string] : acc;
},
[],
Object.keys(state) as (keyof S)[],
);
};
// -- memoized functions to update state on change detection -------------
const updateIsValid = useCallback(allValid, [validationState]);
const updateErrors = useCallback(generateValidationErrors, [validationState]);
useEffect(() => {
setIsValid(updateIsValid(validationState));
setValidationErros(updateErrors(validationState));
}, [validationState, updateIsValid]);
return {
forceValidationState,
getAllErrors,
getError,
getFieldValid,
isValid,
resetValidationState,
validate,
validateAll,
validateCustom,
validateIfTrue,
validateOnBlur,
validateOnChange,
validationErrors,
validationState,
};
};