react-hooks-form-validator
Version:
One react hook for all your form validations
287 lines (267 loc) • 8.42 kB
JavaScript
import { useRef, useEffect, useCallback, useState, useMemo } from 'react';
import {
getDefaultValues,
getFieldError,
validateFieldConfig,
validatePerFieldSchema,
getDefaultErrors,
getFormValidity,
getFormErrors,
twoLevelCopy,
getFieldConfig,
} from './utilities';
export default function useNewForm(fieldConfig) {
const hasFieldConfigSet = useRef(false);
const fieldConfigRef = useRef(getFieldConfig(fieldConfig, hasFieldConfigSet));
const formValuesAsRef = useRef(getDefaultValues(fieldConfigRef.current));
const intialStateRef = useRef();
const [formValues, setFormValues] = useState(formValuesAsRef.current);
const [isFormTouched, setIsFormTouched] = useState(false);
const [formErrors, setFormErrors] = useState(() =>
getDefaultErrors(fieldConfigRef.current),
);
const setFieldValue = useCallback((fieldName, value) => {
formValuesAsRef.current[fieldName] = value;
setFormValues((currentValues) => ({
...currentValues,
[fieldName]: value,
}));
setIsFormTouched(true);
}, []);
const setFieldError = useCallback((fieldName, errorMsg) => {
setFormErrors((currentErrors) => {
if (
!currentErrors[fieldName] ||
currentErrors[fieldName].isValid !== !errorMsg ||
currentErrors[fieldName].errorMsg !== errorMsg
) {
return {
...currentErrors,
[fieldName]: {
isValid: !errorMsg,
errorMsg,
},
};
}
return currentErrors;
});
}, []);
const validateFieldAndSetErrorIfValueExists = useCallback(
async (fieldName, currentFormValues) => {
const errorMsg = await getFieldError(
currentFormValues,
fieldName,
fieldConfigRef.current,
);
setFormErrors((currentErrors) => {
if (
!currentErrors[fieldName] ||
currentErrors[fieldName].isValid !== !!errorMsg ||
currentErrors[fieldName].errorMsg !== errorMsg
) {
return {
...currentErrors,
[fieldName]: {
isValid: !errorMsg,
errorMsg: currentFormValues[fieldName] ? errorMsg : '',
},
};
}
return currentErrors;
});
},
[],
);
const validateFormFieldAndSetError = useCallback(
(fieldName) => {
const fieldError = getFieldError(
formValuesAsRef.current,
fieldName,
fieldConfigRef.current,
);
setFieldError(fieldName, fieldError);
},
[setFieldError],
);
const setFieldValueAndValidate = useCallback(
(fieldName, value) => {
setFieldValue(fieldName, value);
validateFormFieldAndSetError(fieldName);
},
[setFieldValue, validateFormFieldAndSetError],
);
const validateForm = useCallback(() => {
const [isValid, errors] = getFormErrors(
fieldConfigRef.current,
formValuesAsRef.current,
);
setFormErrors(errors);
return [isValid, errors];
}, []);
const updateFieldConfig = useCallback(
async (fieldName, fieldConfigUpdater) => {
const currentFieldConfig = fieldConfigRef.current[fieldName];
const newFieldConfig = fieldConfigUpdater(
currentFieldConfig,
formValuesAsRef.current,
);
const hasConfigChanged = Object.keys(newFieldConfig).some(
(name) => currentFieldConfig[name] !== newFieldConfig[name],
);
if (hasConfigChanged) {
const newMergedConfig = {
...currentFieldConfig,
...newFieldConfig,
};
validatePerFieldSchema(newMergedConfig);
fieldConfigRef.current[fieldName] = newMergedConfig;
validateFieldAndSetErrorIfValueExists(
fieldName,
formValuesAsRef.current,
);
}
},
[validateFieldAndSetErrorIfValueExists],
);
const addFieldConfig = useCallback(
(fieldName, fieldConfigOrUpdater) => {
const isFieldAlreadPresent = !!fieldConfigRef.current[fieldName];
if (isFieldAlreadPresent) {
const error = new Error(
`${fieldName} already exists. Please use ${fieldName}.updateConfig instead`,
);
throw error;
}
let newFieldConfig = fieldConfigOrUpdater;
if (typeof newFieldConfig === 'function') {
newFieldConfig = fieldConfigOrUpdater(formValuesAsRef.current);
}
validatePerFieldSchema(newFieldConfig);
fieldConfigRef.current[fieldName] = newFieldConfig;
setFieldValue(fieldName, newFieldConfig.defaultValue);
validateFieldAndSetErrorIfValueExists(fieldName, formValuesAsRef.current);
return fieldConfigRef.current;
},
[setFieldValue, validateFieldAndSetErrorIfValueExists],
);
const removeFieldConfig = useCallback((fieldName) => {
const isFieldAlreadPresent = !!fieldConfigRef.current[fieldName];
if (!isFieldAlreadPresent) {
// eslint-disable-next-line no-console
console.warn(
`${fieldName} doesn't exist. Please use addFieldConfig to add`,
);
return null;
}
delete fieldConfigRef.current[fieldName];
delete formValuesAsRef.current[fieldName];
setFormValues((currentValues) => {
const values = { ...currentValues };
delete values[fieldName];
return values;
});
setFormErrors((currentErrors) => {
const errors = { ...currentErrors };
delete errors[fieldName];
return errors;
});
return fieldConfigRef.current;
}, []);
const setFormErrorWithoutMessages = useCallback(() => {
const [, errorFields] = getFormErrors(
fieldConfigRef.current,
formValuesAsRef.current,
);
setFormErrors(() => {
const errorWithoutMsg = {};
Object.keys(errorFields).forEach((fieldName) => {
const errorState = errorFields[fieldName];
errorWithoutMsg[fieldName] = {
isValid: errorState.isValid,
};
});
return errorWithoutMsg;
});
}, []);
const resetForm = useCallback(() => {
fieldConfigRef.current = twoLevelCopy(intialStateRef.current.fieldConfig);
formValuesAsRef.current = { ...intialStateRef.current.values };
setFormErrorWithoutMessages();
setFormValues({ ...intialStateRef.current.values });
}, [setFormErrorWithoutMessages]);
useEffect(() => {
validateFieldConfig(fieldConfigRef.current);
setFormErrorWithoutMessages();
}, [setFormErrorWithoutMessages]);
useEffect(() => {
intialStateRef.current = {
values: getDefaultValues(fieldConfigRef.current),
fieldConfig: twoLevelCopy(fieldConfigRef.current),
};
}, []);
const fieldsReturnValue = useMemo(() => {
const fieldsIn = {};
Object.keys(fieldConfigRef.current).forEach((fieldName) => {
fieldsIn[fieldName] = {
updateConfig: (config) => updateFieldConfig(fieldName, config),
setValue: (value) => setFieldValueAndValidate(fieldName, value),
validate: () => validateFormFieldAndSetError(fieldName),
setError: (errorMsg) => setFieldError(fieldName, errorMsg),
setValueOnly: (value) => setFieldValue(fieldName, value),
get value() {
return formValues[fieldName];
},
get errorMsg() {
return (
(formErrors[fieldName] && formErrors[fieldName].errorMsg) || ''
);
},
get isValid() {
/**
*
* So by default, for that duration isValid is false
*/
return (
(formErrors[fieldName] && formErrors[fieldName].isValid) || false
);
},
};
});
return fieldsIn;
}, [
formErrors,
formValues,
setFieldError,
setFieldValue,
setFieldValueAndValidate,
updateFieldConfig,
validateFormFieldAndSetError,
]);
const formUpdater = useRef({
validateForm,
addFieldConfig,
removeFieldConfig,
reset: resetForm,
});
const formsReturnValue = useMemo(
() => ({
isTouched: isFormTouched,
isValid: getFormValidity(formErrors),
validate: validateForm,
addFieldConfig,
removeFieldConfig,
reset: resetForm,
config: fieldConfigRef.current,
updater: formUpdater.current,
}),
[
addFieldConfig,
formErrors,
isFormTouched,
removeFieldConfig,
resetForm,
validateForm,
],
);
return [fieldsReturnValue, formsReturnValue];
}