UNPKG

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