formoose
Version:
Schema-based run time validation engine, made to integrate back and front-end validations using Mongoose like schemas.
456 lines (417 loc) • 17 kB
JavaScript
import { useState, useCallback, useEffect } from 'react';
class FormooseError extends Error {
constructor(message, field, translatedMessageKey, translatedMessageInterpolators) {
super(message);
Object.setPrototypeOf(this, FormooseError.prototype);
this.name = 'FormooseError';
this.message = message || 'Unxpected validation error detected by Formoose.';
this.field = field;
this.translatedMessageKey = translatedMessageKey;
this.translatedMessageInterpolators = translatedMessageInterpolators;
}
}
class ErrorHandler {
static throw(message, fieldName, translatedMessageKey, translatedMessageInterpolators = {}) {
throw new FormooseError(message, fieldName, translatedMessageKey, translatedMessageInterpolators);
}
}
const checkIfBelongsToSchema = (props, schema) => {
let errorFound = '';
props.map((propToCheck) => {
const doesNotHasPropertyOnSchema = !Object.prototype.hasOwnProperty.call(schema, propToCheck);
if (doesNotHasPropertyOnSchema) {
errorFound = `${propToCheck} not found on Schema.`;
}
return errorFound;
});
if (errorFound !== '') {
ErrorHandler.throw(errorFound);
}
return errorFound === '';
};
var ErrorCodes;
(function (ErrorCodes) {
ErrorCodes["alphabetical-chars-only"] = "alphabetical-chars-only";
ErrorCodes["is-not-a-boolean"] = "is-not-a-boolean";
ErrorCodes["is-empty"] = "is-empty";
ErrorCodes["is-not-a-number"] = "is-not-a-number";
ErrorCodes["does-not-match-enum"] = "does-not-match-enum";
ErrorCodes["chars-limit-exceeded"] = "chars-limit-exceeded";
ErrorCodes["chars-lenght-is-too-short"] = "chars-lenght-is-too-short";
ErrorCodes["is-not-an-array"] = "is-not-an-array";
ErrorCodes["failed-custom-validate"] = "failed-custom-validate";
})(ErrorCodes || (ErrorCodes = {}));
function isString(fieldValue, fieldName) {
if (typeof fieldValue !== 'string') {
ErrorHandler.throw(`String expected, type sent: ${typeof fieldValue} - on field: ${fieldName}`, fieldName, ErrorCodes['alphabetical-chars-only'], { fieldName, fieldValue });
}
return true;
}
function isArray(fieldValue, fieldName) {
if (!Array.isArray(fieldValue)) {
ErrorHandler.throw(`Array expected, type sent: ${typeof fieldValue} - on field: ${fieldName}`, fieldName, ErrorCodes['is-not-an-array'], { fieldName, fieldValue });
}
return true;
}
function isEmpty(fieldValue, fieldName) {
if (fieldValue === undefined || fieldValue === null || fieldValue === '' || fieldValue.length < 1) {
ErrorHandler.throw(`Required field "${fieldName}" is empty, sent type '${typeof fieldValue}' with value: '${fieldValue}' - on field: ${fieldName}`, fieldName, ErrorCodes['is-empty'], { fieldName, fieldValue });
}
return true;
}
function isNumber(fieldValue, fieldName) {
if (typeof fieldValue !== 'number') {
ErrorHandler.throw(`Number expected, type sent: ${typeof fieldValue} - on field: ${fieldName}`, fieldName, ErrorCodes['is-not-a-number'], { fieldName, fieldValue });
}
return true;
}
function isBoolean(fieldValue, fieldName) {
if (typeof fieldValue !== 'boolean') {
ErrorHandler.throw(`Expected type boolean, sent '${typeof fieldValue}' - on field: ${fieldName}`, fieldName, ErrorCodes['is-not-a-boolean'], { fieldName, fieldValue });
}
return true;
}
function matchValidate(fieldValue, schemaItem, fieldName) {
const { validate } = schemaItem;
if (!(validate === null || validate === void 0 ? void 0 : validate.validator(fieldValue))) {
const defaultMessage = `Custom Validation failed on field: ${fieldName}`;
ErrorHandler.throw((validate === null || validate === void 0 ? void 0 : validate.message) || defaultMessage, fieldName, (validate === null || validate === void 0 ? void 0 : validate.message) || ErrorCodes['failed-custom-validate'], { fieldName, fieldValue });
}
return true;
}
function matchEnum(fieldValue, enumObject, fieldName) {
const existsInEnum = Object.values(enumObject).indexOf(fieldValue) > -1;
if (!existsInEnum) {
ErrorHandler.throw(`The value '${fieldValue}' isn't listed in the possible enumerable list '${JSON.stringify(enumObject)}' - on field: ${fieldName}`, fieldName, ErrorCodes['does-not-match-enum'], { fieldName, fieldValue });
}
return true;
}
function maxLength(fieldValue, size, fieldName) {
const stringHelper = String(fieldValue);
if (isString(fieldValue, fieldName) && stringHelper.length > size) {
ErrorHandler.throw(`String exceed the size of ${size} characters with ${stringHelper.length} - on field: ${fieldName}`, fieldName, ErrorCodes['chars-limit-exceeded'], { fieldValue, size, length: fieldValue.length, fieldName });
}
return true;
}
function minLength(fieldValue, size, fieldName) {
const stringHelper = String(fieldValue);
if (isString(fieldValue, fieldName) && stringHelper.length < size) {
ErrorHandler.throw(`String with length of ${stringHelper.length}, expected at least ${size} characters - on field: ${fieldName}`, fieldName, ErrorCodes['chars-lenght-is-too-short'], { fieldValue, size, length: fieldValue.length, fieldName });
}
return true;
}
const getTypeValidator = (primitiveTypeName) => {
const types = {
Array: isArray,
Boolean: isBoolean,
Number: isNumber,
String: isString,
default: () => null
};
return types[primitiveTypeName] || types.default;
};
const TypeChecker = (dataChecker) => {
const { fieldValue, schemaItem, fieldName } = dataChecker;
getTypeValidator(schemaItem.type)(fieldValue, fieldName);
};
const RequiredChecker = (dataChecker) => {
const { fieldValue, schemaItem, fieldName } = dataChecker;
if (schemaItem === null || schemaItem === void 0 ? void 0 : schemaItem.required) {
isEmpty(fieldValue, fieldName);
}
};
const MinLengthChecker = (dataChecker) => {
const { fieldValue, schemaItem, fieldName } = dataChecker;
if (schemaItem === null || schemaItem === void 0 ? void 0 : schemaItem.min) {
minLength(fieldValue, schemaItem.min, fieldName);
}
};
const MaxLengthChecker = (dataChecker) => {
const { fieldValue, schemaItem, fieldName } = dataChecker;
if (schemaItem === null || schemaItem === void 0 ? void 0 : schemaItem.max) {
maxLength(fieldValue, schemaItem.max, fieldName);
}
};
const CustomValidate = (dataChecker) => {
const { fieldValue, schemaItem, fieldName } = dataChecker;
if (schemaItem === null || schemaItem === void 0 ? void 0 : schemaItem.validate) {
matchValidate(fieldValue, schemaItem, fieldName);
}
};
const EnumChecker = (dataChecker) => {
const { fieldValue, schemaItem, fieldName } = dataChecker;
if (schemaItem === null || schemaItem === void 0 ? void 0 : schemaItem.enum) {
matchEnum(fieldValue, schemaItem.enum, fieldName);
}
};
const EnsureSchema = (dataChecker) => {
const { required } = dataChecker.schemaItem;
const { fieldValue } = dataChecker;
const validationNotNeeded = !required && (fieldValue === undefined || fieldValue === '' || fieldValue === null);
if (validationNotNeeded) {
return null;
}
for (const prop in dataChecker.schemaItem) {
if (Object.prototype.hasOwnProperty.call(dataChecker === null || dataChecker === void 0 ? void 0 : dataChecker.schemaItem, prop)) {
if (prop === 'type') {
TypeChecker(dataChecker);
}
if (prop === 'required') {
RequiredChecker(dataChecker);
}
if (prop === 'min') {
MinLengthChecker(dataChecker);
}
if (prop === 'max') {
MaxLengthChecker(dataChecker);
}
if (prop === 'validate') {
CustomValidate(dataChecker);
}
if (prop === 'enum') {
EnumChecker(dataChecker);
}
}
}
return true;
};
const Validate = (model, fieldNames, schema) => new Promise((resolve) => {
checkIfBelongsToSchema(fieldNames, schema);
for (let i = 0; i < fieldNames.length; i++) {
EnsureSchema({
fieldName: fieldNames[i],
schemaItem: schema[fieldNames[i]],
fieldValue: model[fieldNames[i]]
});
}
resolve(true);
});
class Formoose {
constructor() {
throw new Error('Instancing Formoose Class is deperacted. Please use the new hook useFormoose.');
}
}
function cleanError(stateSetter, field) {
stateSetter((state) => ({
...state,
[field]: {
...state[field],
error: null,
message: null
}
}));
}
function convertModelToForm(model) {
const objectAdapter = {};
Object.keys(model).forEach((key) => {
objectAdapter[key] = {
error: null,
message: null,
value: model[key]
};
});
return objectAdapter;
}
function setMessage(stateSetter, field, message) {
stateSetter((state) => ({
...state,
[field]: {
...state[field],
message
}
}));
}
function setError(stateSetter, fieldName, error, t) {
stateSetter((state) => ({
...state,
[fieldName]: {
...state[fieldName],
error,
message: t(error === null || error === void 0 ? void 0 : error.translatedMessageKey, error === null || error === void 0 ? void 0 : error.translatedMessageInterpolators)
}
}));
}
function setValue(stateSetter, field, value) {
stateSetter((formState) => {
return {
...formState,
[field]: {
...formState[field],
value
}
};
});
}
function handleFieldChange(event, fieldName, stateSetter) {
const { target } = event;
const { type } = target;
let fieldValue = null;
switch (type) {
case 'radio':
fieldValue = target.value;
break;
case 'checkbox':
fieldValue = target.checked;
break;
default:
fieldValue = target.value;
break;
}
setValue(stateSetter, fieldName, fieldValue);
}
function getModel(formState, fieldName) {
return {
[fieldName]: formState[fieldName].value
};
}
function validateAllFieldsSync(schema, formData, stateSetter, t) {
return new Promise((resolve, reject) => {
const fakeFormData = Object.assign({}, formData);
const fakeSetFormData = (fakeState) => {
Object.assign(fakeFormData, fakeState(fakeFormData));
return fakeFormData;
};
const promiseList = Object.keys(formData).map((field) => Validate(getModel(formData, field), [field], schema)
.then(() => {
cleanError(fakeSetFormData, field);
})
.catch((error) => {
setError(fakeSetFormData, field, error, t);
throw error;
}));
Promise.all(promiseList)
.then(() => {
stateSetter(fakeFormData);
resolve(true);
})
.catch((e) => {
console.error(e);
stateSetter(fakeFormData);
reject({
formData: fakeFormData,
message: 'Invalid Form',
result: false
});
});
});
}
const validateOneField = (fieldName, schema, formData, stateSetter, t) => {
return new Promise((resolve) => {
Validate(getModel(formData, fieldName), [fieldName], schema)
.then(() => {
cleanError(stateSetter, fieldName);
resolve(true);
})
.catch((error) => {
console.error(error);
setError(stateSetter, fieldName, error, t);
resolve(false);
});
});
};
function getMaxLength(fieldName, schema) {
var _a;
return (_a = schema[fieldName]) === null || _a === void 0 ? void 0 : _a.max;
}
function updateFormDataValues(formData, model) {
const objectAdapter = {};
Object.keys(model).map((key) => {
objectAdapter[key] = { value: model[key] };
});
Object.assign(formData, objectAdapter);
}
function getSimpleObject(formData, fieldsToSkip = null, getEmptyValuesToo = false) {
const objectAdapter = {};
const allFieldNames = Object.keys(formData);
allFieldNames.map((fieldName) => {
const fieldHasAnyValue = formData[fieldName].value !== '' && formData[fieldName].value !== undefined;
if (getEmptyValuesToo || fieldHasAnyValue) {
objectAdapter[fieldName] = formData[fieldName].value;
}
});
if (fieldsToSkip) {
fieldsToSkip.forEach((fieldName) => {
delete objectAdapter[fieldName];
});
}
return objectAdapter;
}
const mountFormState = (schema) => {
const formData = {};
Object.keys(schema).map((key) => {
return (formData[key] = {
error: undefined,
message: undefined,
value: ''
});
});
return formData;
};
var SupportedTypesEnum;
(function (SupportedTypesEnum) {
SupportedTypesEnum[SupportedTypesEnum["Array"] = 0] = "Array";
SupportedTypesEnum[SupportedTypesEnum["Boolean"] = 1] = "Boolean";
SupportedTypesEnum[SupportedTypesEnum["Number"] = 2] = "Number";
SupportedTypesEnum[SupportedTypesEnum["String"] = 3] = "String";
})(SupportedTypesEnum || (SupportedTypesEnum = {}));
let validateAfterAllTimer = null;
const getFieldName = (fieldNameOrEvent) => {
var _a;
if (typeof fieldNameOrEvent === 'string') {
return fieldNameOrEvent;
}
return (_a = fieldNameOrEvent === null || fieldNameOrEvent === void 0 ? void 0 : fieldNameOrEvent.target) === null || _a === void 0 ? void 0 : _a.name;
};
function useFormoose(schema, t) {
const [formData, setFormData] = useState(mountFormState(schema));
const [lastChangedField, setLastChangedField] = useState();
const tools = {
cleanError: (fieldNameOrEvent) => {
cleanError(setFormData, getFieldName(fieldNameOrEvent));
},
convertModelToForm,
getMaxLength: (fieldName) => getMaxLength(fieldName, schema),
getModel: (fieldName) => getModel(formData, fieldName),
getSimpleObject: (fieldsToSkip, getEmptyValuesToo) => getSimpleObject(formData, fieldsToSkip, getEmptyValuesToo),
handleFieldChange: (event) => {
var _a;
const fieldName = (_a = event === null || event === void 0 ? void 0 : event.target) === null || _a === void 0 ? void 0 : _a.name;
handleFieldChange(event, fieldName, setFormData);
setLastChangedField({ fieldName, fieldValue: event.target.value });
},
mountFormState: () => mountFormState(schema),
setError: (fieldName, error) => setError(setFormData, fieldName, error, t),
setMessage: (fieldName, message) => setMessage(setFormData, fieldName, message),
setValue: (fieldName, fieldValue) => setValue(setFormData, fieldName, fieldValue),
updateFormDataValues: (updatedModel) => updateFormDataValues(formData, updatedModel),
validateAllFieldsSync: () => validateAllFieldsSync(schema, formData, setFormData, t),
validateOneField: async (fieldNameOrEvent) => {
return await validateOneField(getFieldName(fieldNameOrEvent), schema, formData, setFormData, t);
}
};
const setProps = useCallback((fieldName) => {
return {
name: fieldName,
value: formData[fieldName].value,
onChange: tools.handleFieldChange,
onFocus: tools.cleanError,
onBlur: tools.validateOneField
};
}, [tools]);
useEffect(() => {
clearTimeout(validateAfterAllTimer);
if (lastChangedField === null || lastChangedField === void 0 ? void 0 : lastChangedField.fieldName) {
validateAfterAllTimer = setTimeout(() => {
tools.validateOneField(lastChangedField.fieldName);
}, 500);
}
}, [lastChangedField]);
return {
formData,
setFormData,
setProps,
...tools
};
}
export { CustomValidate, EnsureSchema, EnumChecker, ErrorCodes, ErrorHandler, Formoose, FormooseError, MaxLengthChecker, MinLengthChecker, RequiredChecker, SupportedTypesEnum, TypeChecker, Validate, checkIfBelongsToSchema, cleanError, convertModelToForm, getMaxLength, getModel, getSimpleObject, getTypeValidator, handleFieldChange, isArray, isBoolean, isEmpty, isNumber, isString, matchEnum, matchValidate, maxLength, minLength, mountFormState, setError, setMessage, setValue, updateFormDataValues, useFormoose, validateAllFieldsSync, validateOneField };
//# sourceMappingURL=index.esm.mjs.map