diff --git a/README.md b/README.md index 5a27ebf..3d22e11 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ An input state management for React. It comes with useful common validations if [MIT][license-url] -[size-shield]: https://img.shields.io/bundlephobia/minzip/aio-inputs/2.2.0?style=for-the-badge +[size-shield]: https://img.shields.io/bundlephobia/minzip/aio-inputs/3.0.0?style=for-the-badge [license-shield]: https://img.shields.io/github/license/klm-lab/inputs?style=for-the-badge diff --git a/examples/README.md b/examples/README.md index ff37314..332666c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,7 +39,7 @@ Tracking inputs [HERE][no-tracking-link] [MIT][license-url] -[size-shield]: https://img.shields.io/bundlephobia/minzip/aio-inputs/2.2.0?style=for-the-badge +[size-shield]: https://img.shields.io/bundlephobia/minzip/aio-inputs/3.0.0?style=for-the-badge [license-shield]: https://img.shields.io/github/license/klm-lab/inputs?style=for-the-badge diff --git a/package-lock.json b/package-lock.json index 96cd727..eead907 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "aio-inputs", - "version": "2.1.13", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aio-inputs", - "version": "2.1.13", + "version": "2.2.0", "license": "MIT", "dependencies": { - "aio-store": "^2.4.42" + "aio-store": "^2.4.43" }, "devDependencies": { "@rollup/plugin-commonjs": "^25.0.5", @@ -727,9 +727,9 @@ } }, "node_modules/aio-store": { - "version": "2.4.42", - "resolved": "https://registry.npmjs.org/aio-store/-/aio-store-2.4.42.tgz", - "integrity": "sha512-6zxmLd5uUV2J9tYEFhttak5FAluwT9x7OuTFnLnbeDKBnEDfzdy0TfgW6XOH+o/gKokctlUK/C7wf8gIGfLFQA==", + "version": "2.4.43", + "resolved": "https://registry.npmjs.org/aio-store/-/aio-store-2.4.43.tgz", + "integrity": "sha512-EbDOIqftM9jCvalokjKC0hzzd15atGk7HorNoHk9GT7Ewt4aKoNJqHPKYK4Rvnjcjijw80NMk5Q+B5F5t7Iwug==", "peerDependencies": { "react": "^18.2.0" }, diff --git a/package.json b/package.json index fab2eac..66233c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aio-inputs", - "version": "2.2.0", + "version": "3.0.0", "description": "An opinionated inputs state manager for react", "scripts": { "build": "npm ci && npm run build:noci", @@ -57,6 +57,9 @@ "rollup-plugin-copy": "^3.5.0", "typescript": "^5.2.2" }, + "types": "./index.d.ts", + "main": "./index.js", + "module": "./index.mjs", "exports": { ".": { "types": "./index.d.ts", @@ -74,7 +77,7 @@ "state" ], "dependencies": { - "aio-store": "^2.4.42" + "aio-store": "^2.4.43" }, "peerDependencies": { "react": "^18.2.0" diff --git a/rollup.config.js b/rollup.config.js index 01409d5..54bc838 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -11,7 +11,11 @@ module.exports = [ { file: "lib/index.js", format: "cjs", - plugins: [terser()] + plugins: [ + terser({ + compress: true + }) + ] }, // { // file: "lib/index.min.js", @@ -21,7 +25,13 @@ module.exports = [ { file: "lib/index.mjs", format: "esm", - plugins: [terser()] + plugins: [ + terser({ + ecma: 2020, + compress: true, + module: true + }) + ] } // { // file: "lib/index.esm.min.mjs", diff --git a/src/index.ts b/src/index.ts index 4428494..24bb723 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,17 @@ export { useInputs } from "./inputs"; export { trackInputs } from "./tracking"; +export { required } from "./inputs/validations/required"; +export { number, min, max } from "./inputs/validations/number"; +export { + minLength, + maxLength, + minLengthWithoutSpace, + maxLengthWithoutSpace +} from "./inputs/validations/length"; +export { email } from "./inputs/validations/email"; +export { regex } from "./inputs/validations/regex"; +export { startsWith, endsWith } from "./inputs/validations/string"; +export { copy } from "./inputs/validations/copy"; +export { match } from "./inputs/validations/match"; +export { custom } from "./inputs/validations/custom"; +export { asyncCustom } from "./inputs/validations/asyncCustom"; diff --git a/src/inputs/handlers/changes.ts b/src/inputs/handlers/changes.ts index 77ccef6..7b85b21 100644 --- a/src/inputs/handlers/changes.ts +++ b/src/inputs/handlers/changes.ts @@ -1,13 +1,13 @@ import type { Helper, Input, - InputConfig, InputStore, ObjectInputs, - ParsedFile + ParsedFile, + Unknown } from "../../types"; import { AsyncValidationParams } from "../../types"; -import { asyncValidation, validate } from "../../util/validation"; +import { validate } from "../validations"; import { validateState } from "../../util"; import { createFiles } from "./files"; import { createSelectFiles } from "./select"; @@ -16,112 +16,105 @@ import { createCheckboxValue } from "./checkbox"; const asyncCallback = ({ valid: asyncValid, em: asyncErrorMessage, - entry, + input, store, failed, helper }: AsyncValidationParams) => { // Clone inputs - const clone = store.get("entry"); - const ID = entry.id; + const entry = store.get("entry"); + const id = input.id; if (failed) { - clone[ID].validating = false; - clone[ID].asyncValidationFailed = true; - syncChanges(store, clone); + entry[id].validating = false; + entry[id].asyncValidationFailed = true; + syncChanges(store, entry); return; } // Revalidate input, maybe a change occurs before server response - const { valid, em } = validate(helper, clone, ID, clone[ID].value); + const { valid, em } = validate(helper, entry, id, entry[id].value); // Add server validation only actual data is valid - clone[ID].valid = valid && asyncValid; + entry[id].valid = valid && asyncValid; // Add server error message only actual data is valid else keep actual error Message - clone[ID].errorMessage = valid ? asyncErrorMessage : em; + entry[id].errorMessage = valid ? asyncErrorMessage : em; // Finish calling server - clone[ID].validating = false; + entry[id].validating = false; // Sync handlers - syncChanges(store, clone); + syncChanges(store, entry); }; const onChange = ( - input: Input, - element: HTMLInputElement | HTMLSelectElement, + element: (HTMLInputElement | HTMLSelectElement) & { ip: Input }, store: InputStore, - config: InputConfig, - isEvent: boolean, helper: Helper ) => { // Clone inputs - const clone = store.get("entry"); - const ID = input.id; + const entry = store.get("entry"); + const id = element.ip.id; + const input = element.ip; // Get the value based on type const value = input.type === "file" - ? createFiles( - (element as HTMLInputElement).files, - clone, - ID, - store, - config, - helper - ) + ? createFiles(element as Unknown, store, helper) : input.type === "select" ? input.multiple - ? createSelectFiles(isEvent, element as HTMLSelectElement, clone, ID) - : element.value !== "" && element.value !== clone[ID].placeholder + ? createSelectFiles(element as Unknown) + : element.value !== "" && element.value !== entry[id].placeholder ? element.value : "" : element.value; - const toValidate = - input.type === "checkbox" ? createCheckboxValue(clone, ID) : value; - - // Validate inputs - const { valid, em } = validate(helper, clone, ID, toValidate); // Handle type file if (input.type === "file") { - clone[ID].files = value as ParsedFile[]; + entry[id].files = value as ParsedFile[]; } else if (input.type === "radio") { // Check right radio input - for (const key in clone) { - if (clone[key].type === "radio" && clone[key].name === clone[ID].name) { - clone[key].checked = clone[key].value === value; - clone[key].props.checked = clone[key].value === value; - clone[key].valid = true; + for (const key in entry) { + if (entry[key].type === "radio" && entry[key].name === entry[id].name) { + entry[key].checked = entry[key].value === value; + entry[key].props.checked = entry[key].value === value; + entry[key].valid = true; } } } else if (input.type === "checkbox") { // Toggle the checkbox input - clone[ID].checked = !clone[ID].checked; - clone[ID].props.checked = !clone[ID].props.checked; + entry[id].checked = !entry[id].checked; + entry[id].props.checked = !entry[id].props.checked; } else { - clone[ID].value = value; - clone[ID].props.value = value; + entry[id].value = value; + entry[id].props.value = value; } + + const toValidate = + input.type === "checkbox" ? createCheckboxValue(entry, id) : value; + + const { valid, em } = validate(helper, entry, id, toValidate); + // Touched input - clone[ID].touched = true; + entry[id].touched = true; // Set valid to false if async is present else keep validation result - clone[ID].valid = (input.validation?.asyncCustom as unknown) ? false : valid; + entry[id].valid = (input.validation?.asyncCustom as unknown) ? false : valid; // Set errorMessage only if invalid if not keep the default errorMessage structure, Object or undefined - clone[ID].errorMessage = !valid ? em : em instanceof Object ? {} : undefined; - /* if it is valid then if async is true, we set validating to true otherwise false - * valid === false mean no need to call async, - * valid === true means we can call async if async is set to true by the user. - * - * validating prop is responsible to show async validation loading - * */ - // If all change are valid and async is there, we set valid to false else true - clone[ID].validating = valid ? !!input.validation?.asyncCustom : false; + entry[id].errorMessage = !valid ? em : em instanceof Object ? {} : undefined; + + entry[id].validating = valid ? !!input.validation?.asyncCustom : false; // asyncValidationFailed by default because call asyncCustom - clone[ID].asyncValidationFailed = false; + entry[id].asyncValidationFailed = false; // if valid and async is there, we call async validation valid && (input.validation?.asyncCustom as unknown) && - asyncValidation(store, helper, clone, ID, value, asyncCallback); + input.validation?.asyncCustom({ + store, + helper, + input, + target: id, + value, + asyncCallback + }); // we sync handlers - syncChanges(store, clone); + syncChanges(store, entry); }; const syncChanges = (store: InputStore, data: ObjectInputs) => { @@ -132,17 +125,15 @@ const syncChanges = (store: InputStore, data: ObjectInputs) => { }; export const inputChange = ( - value: any, - key: string, - entry: ObjectInputs, + value: Unknown, + input: Input, store: InputStore, - config: InputConfig, helper: Helper ) => { const isEvent = typeof value.preventDefault === "function"; - const element = {} as any; + const element = {} as Unknown; if (!isEvent) { - if (entry[key].type === "file") { + if (input.type === "file") { element.files = value; } else { element.value = value; @@ -152,5 +143,7 @@ export const inputChange = ( element.files = value.target.files || []; element.selectedOptions = value.target.selectedOptions || []; } - onChange(entry[key], element, store, config, isEvent, helper); + element.isEvent = isEvent; + element.ip = input; + onChange(element, store, helper); }; diff --git a/src/inputs/handlers/checkbox.ts b/src/inputs/handlers/checkbox.ts index 3058483..91d7bf7 100644 --- a/src/inputs/handlers/checkbox.ts +++ b/src/inputs/handlers/checkbox.ts @@ -1,21 +1,21 @@ import type { ObjectInputs } from "../../types"; export const createCheckboxValue = ( - clone: ObjectInputs, + entry: ObjectInputs, ID: string, userChange = true ) => { const selected = [] as string[]; - userChange && !clone[ID].checked && selected.push(clone[ID].value); - for (const key in clone) { + userChange && !entry[ID].checked && selected.push(entry[ID].value); + for (const key in entry) { if ( - clone[key].type === "checkbox" && + entry[key].type === "checkbox" && (userChange ? key !== ID : true) && - clone[key].name === clone[ID].name + entry[key].name === entry[ID].name ) { - clone[key].checked && selected.push(clone[key].value); + entry[key].checked && selected.push(entry[key].value); if (userChange) { - clone[key].valid = true; + entry[key].valid = true; } } } diff --git a/src/inputs/handlers/files.ts b/src/inputs/handlers/files.ts index 50f9ae9..c44b261 100644 --- a/src/inputs/handlers/files.ts +++ b/src/inputs/handlers/files.ts @@ -1,37 +1,33 @@ import type { Helper, - InitFileConfig, - InputConfig, + FileConfig, + Input, InputStore, - ObjectInputs, - ParsedFile + ParsedFile, + Unknown } from "../../types"; -import { validate } from "../../util/validation"; +import { validate } from "../validations"; import { validateState } from "../../util"; -import { KEY } from "../../util/helper"; export const createFiles = ( - files: FileList | null, - clone: ObjectInputs, - ID: string, + element: HTMLInputElement & { ip: Input }, store: InputStore, - config: InputConfig, helper: Helper ) => { - const entry = clone[ID]; - const parsed: ParsedFile[] = entry.mergeChanges ? [...entry.files] : []; + const id = element.ip.id; + const files = element.files; + const input = element.ip; + const parsed: ParsedFile[] = input.mergeChanges ? [...input.files] : []; // const dataTransfer = new DataTransfer(); - if (!entry.mergeChanges) { - entry.files.forEach((p) => URL.revokeObjectURL(p.url)); + if (!input.mergeChanges) { + input.files.forEach((p) => URL.revokeObjectURL(p.url)); } if (files) { for (let i = 0; i < files.length; i++) { parsed.push( parseFile( - clone, - ID, + id, store, - config, URL.createObjectURL(files[i]), false, files[i], @@ -45,16 +41,14 @@ export const createFiles = ( }; export const parseFile = ( - clone: ObjectInputs, - ID: string, + id: string, store: InputStore, - config: InputConfig, url: string, gettingFile: boolean, file: File, helper: Helper ): ParsedFile => { - const key = KEY.new; + const key = helper.key(); return { gettingFile, file, @@ -63,29 +57,30 @@ export const parseFile = ( fileUpdate: null, loaded: false, onLoad: () => { - !config.persistID && URL.revokeObjectURL(url); + !store.get("config").persistID && URL.revokeObjectURL(url); store.set((ref) => { - const index = ref.entry[ID].files.findIndex((f) => f.key === key); - ref.entry[ID].files[index].loaded = true; + const index = ref.entry[id].files.findIndex((f) => f.key === key); + ref.entry[id].files[index].loaded = true; }); }, - selfUpdate: (data: any) => { + selfUpdate: (data: Unknown) => { store.set((ref) => { - const files = ref.entry[ID].files; + const files = ref.entry[id].files; const index = files.findIndex((f) => f.key === key); files[index].fileUpdate = data; - ref.entry[ID].files = files; + ref.entry[id].files = files; }); }, selfRemove: () => { store.set((ref) => { - const files = ref.entry[ID].files; + const entry = store.get("entry"); + const files = ref.entry[id].files; const newFiles = files.filter((f) => f.key !== key); // Validate input - const { valid, em } = validate(helper, clone, ID, newFiles); - ref.entry[ID].files = newFiles; - ref.entry[ID].valid = valid; - ref.entry[ID].errorMessage = em; + const { valid, em } = validate(helper, entry, id, newFiles); + ref.entry[id].files = newFiles; + ref.entry[id].valid = valid; + ref.entry[id].errorMessage = em; // Validate form ref.isValid = validateState(ref.entry).isValid; }); @@ -101,77 +96,51 @@ const getFile = (url: string, blob: Blob) => { }; export const blobStringJob = ( - value: any, + value: Unknown, store: InputStore, - clone: ObjectInputs, - ID: string, - config: InputConfig, - fileConfig: InitFileConfig, + id: string, + fileConfig: FileConfig, index: number, valid: boolean, helper: Helper ) => { store.set((ref) => { - ref.entry[ID].files[index] = parseFile( - clone, - ID, + ref.entry[id].files[index] = parseFile( + id, store, - config, value, !!fileConfig.getBlob, // true is getBlob is present {} as File, helper ); - ref.entry[ID].valid = valid; + ref.entry[id].valid = valid; }); if (fileConfig.getBlob) { Promise.resolve(fileConfig.getBlob(value)).then((blob) => { store.set((ref) => { - ref.entry[ID].files[index].gettingFile = false; - ref.entry[ID].files[index].file = getFile(value, blob); + ref.entry[id].files[index].gettingFile = false; + ref.entry[id].files[index].file = getFile(value, blob); }); }); } }; export const retrieveBlob = ( - value: any, + value: Unknown, store: InputStore, - clone: ObjectInputs, - ID: string, - config: InputConfig, - fileConfig: InitFileConfig, + id: string, + fileConfig: FileConfig, valid: boolean, helper: Helper ) => { if (value instanceof Array) { value.forEach((v, index) => { - blobStringJob( - v, - store, - clone, - ID, - config, - fileConfig, - index, - valid, - helper - ); + blobStringJob(v, store, id, fileConfig, index, valid, helper); }); return; } if (typeof value === "string") { - blobStringJob( - value, - store, - clone, - ID, - config, - fileConfig, - 0, - valid, - helper - ); + blobStringJob(value, store, id, fileConfig, 0, valid, helper); return; } throw Error( diff --git a/src/inputs/handlers/select.ts b/src/inputs/handlers/select.ts index 6aae19c..f4feb58 100644 --- a/src/inputs/handlers/select.ts +++ b/src/inputs/handlers/select.ts @@ -1,27 +1,25 @@ -import type { ObjectInputs } from "../../types"; +import type { Input, Unknown } from "../../types"; export const createSelectFiles = ( - isEvent: boolean, - element: HTMLSelectElement, - clone: ObjectInputs, - ID: string + element: HTMLSelectElement & { ip: Input } ) => { - let selected = [] as string[]; - if (isEvent) { + const input = element.ip; + let selected: string[] = []; + if ((element as Unknown).isEvent) { const els = element.selectedOptions; for (let i = 0; i < els.length; i++) { els[i].value !== "" && - els[i].value !== clone[ID].placeholder && + els[i].value !== input.placeholder && selected.push(els[i].value); } } else { // Not need to clone, we keep the same reference safely - selected = clone[ID].value; + selected = input.value; if (selected.includes(element.value)) { - selected = selected.filter((v: any) => v !== element.value); + selected = selected.filter((v) => v !== element.value); } else { element.value !== "" && - element.value !== clone[ID].placeholder && + element.value !== input.placeholder && selected.push(element.value); } } diff --git a/src/inputs/index.ts b/src/inputs/index.ts index 10c4f3e..0ee66b8 100644 --- a/src/inputs/index.ts +++ b/src/inputs/index.ts @@ -3,9 +3,9 @@ import type { ComputeOnceOut, CreateArrayInputs, CreateObjectInputs, + FileConfig, ForEachCallback, Helper, - InitFileConfig, Input, InputConfig, InputStore, @@ -20,86 +20,76 @@ import type { import { commonProps, extractValues, - lockProps, - matchRules, - O, - parseValue, + syncRCAndValid, touchInput, transformToArray, validateState } from "../util"; import { useMemo } from "react"; -import { He, KEY, persist } from "../util/helper"; +import { He, O, persist } from "../util/helper"; import { createStore } from "aio-store/react"; import { retrieveBlob } from "./handlers/files"; import { inputChange } from "./handlers/changes"; -import { validate } from "../util/validation"; +import { validate } from "./validations"; const initValue = ( input: Input, value: Unknown, store: InputStore, - config: InputConfig, - fileConfig: InitFileConfig, + fileConfig: FileConfig, helper: Helper ) => { // Clone inputs - const clone = store.get("entry"); - const ID = input.id; + const entry = store.get("entry"); + const id = input.id; - const { valid } = validate(helper, clone, ID, value); + const { valid } = validate(helper, entry, id, value); - /* Handle type file. It is async, - * First, we send back an url - * */ if (input.type === "file") { - retrieveBlob(value, store, clone, ID, config, fileConfig, valid, helper); + retrieveBlob(value, store, id, fileConfig, valid, helper); return; } if (input.type === "radio") { // Check right radio input - clone[ID].checked = clone[ID].value === value; - clone[ID].props.checked = clone[ID].value === value; + entry[id].checked = entry[id].value === value; + entry[id].props.checked = entry[id].value === value; } else if (input.type === "checkbox") { // Toggle the checkbox input - const cbV = (value as Unknown[]).includes(clone[ID].value); - clone[ID].checked = cbV; - clone[ID].props.checked = cbV; + const cbV = (value as Unknown[]).includes(entry[id].value); + entry[id].checked = cbV; + entry[id].props.checked = cbV; } else { // Parse value if number - clone[ID].value = parseValue(input, value); - clone[ID].props.value = parseValue(input, value); + entry[id].value = value; + entry[id].props.value = value; } // Sync handlers store.set((ref) => { - ref.entry[ID] = clone[ID]; - ref.entry[ID].valid = valid; + ref.entry[id] = entry[id]; + ref.entry[id].valid = valid; }); }; -const populate = (state: any, type: StateType, config: InputConfig) => { - const final = {} as CreateObjectInputs; +const populate = (state: Unknown, type: StateType, config: InputConfig) => { + const final = {} as ObjectInputs; const helper = He(); for (const stateKey in state) { const parseKey = type === "object" ? stateKey : state[stateKey].id; - const key = KEY.new; - const v: Input = { + const key = helper.key(); + final[parseKey] = { ...commonProps(state[stateKey], stateKey), ...state[stateKey], ...(type === "object" ? { id: stateKey, key } : { key }) }; - v.props = lockProps(v); - final[parseKey] = v; - helper.s[parseKey] = { ...v }; } - const entry = helper.clean(matchRules(final, helper)) as ObjectInputs; - const isValid = validateState(entry).isValid; + const { entry, isValid } = syncRCAndValid(final, helper); return { entry, isValid, helper, initialValid: isValid, - asyncDelay: config.asyncDelay ?? 800 + config + //asyncDelay: config.asyncDelay ?? 800 }; }; @@ -121,21 +111,22 @@ const computeOnce = ( for (const key in entry) { // onChange ref.entry[key].onChange = (value) => - inputChange(value, key, entry, store, config, helper); + inputChange(value, entry[key], store, helper); // Props onChange ref.entry[key].props.onChange = (value) => - inputChange(value, key, entry, store, config, helper); - // initValue - ref.entry[key].initValue = (value, fileConfig: InitFileConfig = {}) => - initValue(entry[key], value, store, config, fileConfig, helper); + inputChange(value, entry[key], store, helper); // set - ref.entry[key].set = (prop, value) => { + ref.entry[key].set = (prop, value, fileConfig: FileConfig = {}) => { store.set((ref) => { - if (["extraData", "type"].includes(prop)) { + if (prop === "value") { + initValue(entry[key], value, store, fileConfig, helper); + } + if (prop === "type") { + ref.entry[key].type = value; + ref.entry[key].props.type = value; + } + if (prop === "extraData") { ref.entry[key][prop] = value; - if (prop === "type") { - ref.entry[key].props.type = value; - } } }); }; diff --git a/src/inputs/validations/asyncCustom.ts b/src/inputs/validations/asyncCustom.ts new file mode 100644 index 0000000..670b43b --- /dev/null +++ b/src/inputs/validations/asyncCustom.ts @@ -0,0 +1,52 @@ +import { + AsyncCallback, + AsyncValidateInput, + CustomAsyncValidationType, + InputStore, + Unknown +} from "../../types"; + +export const asyncCustom = ( + callback: CustomAsyncValidationType +): AsyncValidateInput => { + return ({ value, helper, target, input, store, asyncCallback }) => { + clearTimeout(helper.a[input.key]); + helper.a[input.key] = setTimeout( + () => { + // Save the time + const ST = helper.a[input.key]; + let eM: Unknown = null; + Promise.resolve(callback(value, (m: Unknown) => (eM = m))) + .then((value) => { + if (typeof value !== "boolean") { + throw TypeError("Your custom response is not a boolean"); + } + /* we check if time match the request id time + * If not, that means, another request has been sent. + * So we wait for that response + * */ + if (ST === helper.a[input.key]) { + (asyncCallback as AsyncCallback)({ + valid: value, + em: eM ?? helper.em[target], + input, + store: store as InputStore, + helper + }); + } + }) + .catch((error) => { + console.error(error); + (asyncCallback as AsyncCallback)({ + valid: false, + failed: true, + input, + store: store as InputStore, + helper + }); + }); + }, + store!.get("config.asyncDelay") ?? 800 + ); + }; +}; diff --git a/src/inputs/validations/copy.ts b/src/inputs/validations/copy.ts new file mode 100644 index 0000000..d4e37ce --- /dev/null +++ b/src/inputs/validations/copy.ts @@ -0,0 +1,23 @@ +import { ValidateInput, ValidationStateType } from "../../types"; +import { validate } from "./index"; + +export const INFINITE_MC = + "It seems that an ID is missing or we have infinite match here. Please make sure that the copied or matched input has an id and the last matched or copied input does not match or copy anyone"; + +export const copy = ( + inputId: string, + omittedRules?: (keyof ValidationStateType)[] +): ValidateInput => { + return ({ helper, entry, target, value, omittedRules: or }) => { + let v = { valid: true, em: helper.em[target] }; + try { + v = validate(helper, entry!, inputId, value, omittedRules ?? or); + } catch (_) { + throw Error(INFINITE_MC); + } + return { + valid: v.valid, + em: helper.em[target] ?? v.em + }; + }; +}; diff --git a/src/inputs/validations/custom.ts b/src/inputs/validations/custom.ts new file mode 100644 index 0000000..48a4d8c --- /dev/null +++ b/src/inputs/validations/custom.ts @@ -0,0 +1,12 @@ +import { CustomValidationType, Unknown, ValidateInput } from "../../types"; + +export const custom = (callback: CustomValidationType): ValidateInput => { + return ({ value, helper, target }) => { + let eM: Unknown = null; + const valid = callback(value, (m: Unknown) => (eM = m)); + if ((typeof valid as unknown) !== "boolean") { + throw TypeError("Your custom response is not a boolean"); + } + return { valid, em: eM ?? helper.em[target] }; + }; +}; diff --git a/src/inputs/validations/email.ts b/src/inputs/validations/email.ts new file mode 100644 index 0000000..6c410a5 --- /dev/null +++ b/src/inputs/validations/email.ts @@ -0,0 +1,13 @@ +import { Unknown, ValidateInput } from "../../types"; + +export const email = (em?: Unknown): ValidateInput => { + return ({ value }) => { + return { + valid: + /^(([^<>()\]\\.,;:\s@"]+(\.[^<>()\]\\.,;:\s@"]+)*)|(".+"))@(([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + value?.toLowerCase() + ), + em + }; + }; +}; diff --git a/src/inputs/validations/index.ts b/src/inputs/validations/index.ts new file mode 100644 index 0000000..43bafdd --- /dev/null +++ b/src/inputs/validations/index.ts @@ -0,0 +1,48 @@ +import type { + Helper, + Input, + ObjectInputs, + Unknown, + ValidationResult, + ValidationStateType +} from "../../types"; +import { O } from "../../util/helper"; + +const validate = ( + helper: Helper, + entry: ObjectInputs, + target: string, + value: Unknown, + omittedRules: (keyof ValidationStateType)[] = [] +): ValidationResult => { + const input: Input = entry[target]; + const rules: ValidationStateType = { ...input.validation } || {}; + const result = { + valid: true, + em: helper.em[target] + }; + for (let i = 0; i < omittedRules.length; i++) { + delete rules[omittedRules[i]]; + } + const rulesKeys = O.keys(rules); + for (let i = 0; i < rulesKeys.length; i++) { + if (rulesKeys[i] !== "asyncCustom") { + const v = (rules[rulesKeys[i] as keyof ValidationStateType] as Unknown)({ + entry, + helper, + input, + target, + value, + omittedRules + }); + result.valid = result.valid && v.valid; + result.em = v.em ?? result.em; + } + if (!result.valid) { + break; + } + } + return result; +}; + +export { validate }; diff --git a/src/inputs/validations/length.ts b/src/inputs/validations/length.ts new file mode 100644 index 0000000..69e3016 --- /dev/null +++ b/src/inputs/validations/length.ts @@ -0,0 +1,41 @@ +import { Unknown, ValidateInput } from "../../types"; + +export const minLength = (minLength: number, em?: Unknown): ValidateInput => { + return ({ value }) => { + return { valid: value?.length >= minLength, em }; + }; +}; + +export const maxLength = (maxLength: number, em?: Unknown): ValidateInput => { + return ({ value }) => { + return { valid: value?.length <= maxLength, em }; + }; +}; + +export const minLengthWithoutSpace = ( + minLengthWithoutSpace: number, + em?: Unknown +): ValidateInput => { + return ({ value }) => { + return { + valid: + value?.indexOf(" ") === -1 && + value?.trim().length >= minLengthWithoutSpace, + em + }; + }; +}; + +export const maxLengthWithoutSpace = ( + maxLengthWithoutSpace: number, + em?: Unknown +): ValidateInput => { + return ({ value }) => { + return { + valid: + value?.indexOf(" ") === -1 && + value?.trim().length <= maxLengthWithoutSpace, + em + }; + }; +}; diff --git a/src/inputs/validations/match.ts b/src/inputs/validations/match.ts new file mode 100644 index 0000000..71352ef --- /dev/null +++ b/src/inputs/validations/match.ts @@ -0,0 +1,26 @@ +import { ValidateInput, ValidateInputParams } from "../../types"; +import { validate } from "./index"; + +export const match = (inputId: string): ValidateInput => { + return ({ helper, entry, target, value }) => { + let v = { valid: true, em: helper.em[target] }; + if (!entry![inputId]) { + throw Error("Missing input id"); + } + if (!entry![inputId].validation.match) { + entry![inputId].validation.match = ({ + entry, + value: V + }: ValidateInputParams) => { + const valid = entry![target].value === V; + entry![target].valid = valid; + return { valid, em: helper.em[inputId] }; + }; + } + v = validate(helper, entry!, inputId, value, ["match"]); + v.em = v.valid ? helper.em[target] : v.em; + v.valid = v.valid && value === entry![inputId].value; + entry![inputId].valid = v.valid; + return { valid: v.valid, em: v.em }; + }; +}; diff --git a/src/inputs/validations/number.ts b/src/inputs/validations/number.ts new file mode 100644 index 0000000..be1f9bf --- /dev/null +++ b/src/inputs/validations/number.ts @@ -0,0 +1,19 @@ +import { Unknown, ValidateInput } from "../../types"; + +export const number = (em?: Unknown): ValidateInput => { + return ({ value }) => { + return { valid: !isNaN(value), em }; + }; +}; + +export const min = (min: number, em?: Unknown): ValidateInput => { + return ({ value }) => { + return { valid: Number(value) >= min, em }; + }; +}; + +export const max = (max: number, em?: Unknown): ValidateInput => { + return ({ value }) => { + return { valid: Number(value) <= max, em }; + }; +}; diff --git a/src/inputs/validations/regex.ts b/src/inputs/validations/regex.ts new file mode 100644 index 0000000..1829257 --- /dev/null +++ b/src/inputs/validations/regex.ts @@ -0,0 +1,10 @@ +import { Unknown, ValidateInput } from "../../types"; + +export const regex = (regex: RegExp & Unknown, em?: Unknown): ValidateInput => { + return ({ value }) => { + return { + valid: regex?.test(value), + em + }; + }; +}; diff --git a/src/inputs/validations/required.ts b/src/inputs/validations/required.ts new file mode 100644 index 0000000..28ef6ad --- /dev/null +++ b/src/inputs/validations/required.ts @@ -0,0 +1,21 @@ +import { InternalInput, Unknown, ValidateInput } from "../../types"; + +export const required = (em?: Unknown): ValidateInput => { + return ({ input, value }) => { + let valid: boolean = true; + if ( + ((input as InternalInput).type === "select" && + (input as InternalInput).multiple) || + (input as InternalInput).type === "file" || + (input as InternalInput).type === "checkbox" + ) { + valid = value !== null && value.length > 0; + } + valid = + valid && typeof value === "string" ? value.trim() !== "" : value !== null; + return { + valid, + em + }; + }; +}; diff --git a/src/inputs/validations/string.ts b/src/inputs/validations/string.ts new file mode 100644 index 0000000..ad4cc7b --- /dev/null +++ b/src/inputs/validations/string.ts @@ -0,0 +1,22 @@ +import { Unknown, ValidateInput } from "../../types"; + +export const startsWith = ( + startsWith: Unknown, + em?: Unknown +): ValidateInput => { + return ({ value }) => { + return { + valid: value.length > 0 && value.startsWith(startsWith), + em + }; + }; +}; + +export const endsWith = (endsWith: Unknown, em?: Unknown): ValidateInput => { + return ({ value }) => { + return { + valid: value.length > 0 && value.endsWith(endsWith), + em + }; + }; +}; diff --git a/src/tracking/index.ts b/src/tracking/index.ts index 1c28de4..61c25dd 100644 --- a/src/tracking/index.ts +++ b/src/tracking/index.ts @@ -7,7 +7,7 @@ import type { TrackUtil, Unknown } from "../types"; -import { O } from "../util"; +import { O } from "../util/helper"; const TRACKING_KEYS = O.freeze([ "getValues", @@ -25,7 +25,7 @@ const TRACKING_KEYS = O.freeze([ ]); export const trackInputs = (trackingID: S[]) => { - const track = {} as any; + const track = {} as Unknown; trackingID.forEach((a) => { track[a] = { ID: a @@ -43,7 +43,7 @@ export const trackInputs = (trackingID: S[]) => { }; track.toArray = () => { - const arr = [] as any; + const arr: ArrayInputs = []; for (const t in track) { if (!TRACKING_KEYS.includes(t) && track[t] && track[t].toArray) { arr.push(...track[t].toArray()); @@ -54,7 +54,7 @@ export const trackInputs = (trackingID: S[]) => { ["getValues", "toObject", "useValues"].forEach((func) => { track[func] = () => { - let result = {} as any; + let result = {} as Unknown; for (const t in track) { if (!TRACKING_KEYS.includes(t) && track[t] && track[t][func]) { result = { diff --git a/src/types/index.ts b/src/types/index.ts index f85e98c..3656d8a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,95 +27,49 @@ type HTMLInputTypeAttribute = type Unknown = any; interface CustomValidationType { - ( - value: Unknown, - setErrorMessage?: (message: ErrorMessageType) => void - ): boolean; + (value: Unknown, setErrorMessage: (message: Unknown) => void): boolean; } interface CustomAsyncValidationType { ( value: Unknown, - setErrorMessage?: (message: ErrorMessageType) => void + setErrorMessage: (message: Unknown) => void ): Promise; } -type MatchResultType = { - // matched keys - mk: string[]; - // Last matched - lm: string; - // validation - v?: ValidationStateType; -}; - -type MergeType = { - omit?: Set; - keyPath?: keyof ValidationStateType; +type ValidateInputParams = { + helper: Helper; + entry?: ObjectInputs; + input: Input; + target: string; + value: Unknown; + omittedRules?: (keyof ValidationStateType)[]; + store?: InputStore; + asyncCallback?: AsyncCallback; }; - -type StringOrMap = string | { value: string; message: ErrorMessageType }; - -type BooleanOrMap = boolean | { value?: boolean; message: ErrorMessageType }; - -// type CustomNumber = "only" | "not-allowed" | "allowed"; -// -// type CustomNumberOrMap = -// | CustomNumber -// | { value?: CustomNumber; message: ErrorMessageType }; - -type NumberOrMap = number | { value: number; message: ErrorMessageType }; - -type CopyKeyObjType = { value: string; omit: Set }; -type CopyType = { value: string; omit: (keyof ValidationStateType)[] }; +type ValidateInput = (params: ValidateInputParams) => ValidationResult; +type ValidationResult = { em: Unknown; valid: boolean }; +type AsyncValidateInput = (params: ValidateInputParams) => void; interface ValidationStateType { - required?: BooleanOrMap; - email?: BooleanOrMap; - number?: BooleanOrMap; - // negativeNumber?: CustomNumberOrMap; - // expoNumber?: CustomNumberOrMap; - // floatingNumber?: CustomNumberOrMap; - min?: NumberOrMap; - max?: NumberOrMap; - minLength?: NumberOrMap; - minLengthWithoutSpace?: NumberOrMap; - maxLength?: NumberOrMap; - maxLengthWithoutSpace?: NumberOrMap; - match?: string; - startsWith?: StringOrMap; - endsWith?: StringOrMap; - regex?: RegExp & Unknown; - copy?: string | CopyType; - custom?: CustomValidationType; - asyncCustom?: CustomAsyncValidationType; + required?: ValidateInput; + email?: ValidateInput; + number?: ValidateInput; + min?: ValidateInput; + max?: ValidateInput; + minLength?: ValidateInput; + minLengthWithoutSpace?: ValidateInput; + maxLength?: ValidateInput; + maxLengthWithoutSpace?: ValidateInput; + match?: ValidateInput; + startsWith?: ValidateInput; + endsWith?: ValidateInput; + regex?: ValidateInput; + copy?: ValidateInput; + custom?: ValidateInput; + asyncCustom?: ValidateInput; } -interface RequiredValidationStateType { - required: BooleanOrMap; - email: BooleanOrMap; - number: BooleanOrMap; - // negativeNumber: CustomNumberOrMap; - // expoNumber: CustomNumberOrMap; - // floatingNumber: CustomNumberOrMap; - min: NumberOrMap; - max: NumberOrMap; - minLength: NumberOrMap; - minLengthWithoutSpace: NumberOrMap; - maxLength: NumberOrMap; - maxLengthWithoutSpace: NumberOrMap; - match: string; - startsWith: StringOrMap; - endsWith: StringOrMap; - regex: RegExp & Unknown; - copy: string | CopyType; - asyncCustom: CustomAsyncValidationType; - custom: CustomValidationType; -} - -// type StringOrObj = string | { [k in string]: string }; -type ErrorMessageType = Unknown; - interface InternalInput { id?: string; accept?: string; @@ -173,25 +127,26 @@ interface Input extends InputProps { label: Unknown; files: ParsedFile[]; mergeChanges: boolean; - errorMessage: ErrorMessageType; - validation: RequiredValidationStateType; + errorMessage: Unknown; + validation: Required; validating: boolean; asyncValidationFailed: boolean; valid: boolean; touched: boolean; - initValue(value: Unknown, initFileConfig?: InitFileConfig): void; + // initValue(value: Unknown, initFileConfig?: FileConfig): void; - set

(prop: P, value: Input[P]): void; + set

( + prop: P, + value: Input[P], + fileConfig?: FileConfig + ): void; props: InputProps; extraData: Unknown; } -interface InitFileConfig { - // entryFormat?: "url" | "url[]"; - // proxyUrl?: string; - // useDefaultProxyUrl?: boolean; +interface FileConfig { getBlob?(url: string): Blob | Promise; } @@ -285,12 +240,13 @@ type InputStore = StoreType<{ isValid: boolean; helper: Helper; initialValid: boolean; - asyncDelay: number; + // asyncDelay: number; + config: InputConfig; }>; type AsyncValidationParams = { valid: boolean; - em?: ErrorMessageType; - entry: Input; + em?: Unknown; + input: Input; store: InputStore; failed?: boolean; helper: Helper; @@ -313,13 +269,9 @@ interface ComputeOnceOut { } interface Helper { - ok: { [k in string]: Set }; - s: CreateObjectInputs; - em: { [k in string]: ErrorMessageType | undefined }; - tm: { [k in string]: string[] }; + key(): string; + em: { [k in string]: Unknown }; a: { [k in string]: Unknown }; - - clean(s: CreateObjectInputs): CreateObjectInputs; } export type { @@ -333,13 +285,7 @@ export type { ValidationStateType, StateType, CustomValidationType, - MatchResultType, Form, - StringOrMap, - ErrorMessageType, - CopyKeyObjType, - MergeType, - CopyType, InputConfig, Input, IDTrackUtil, @@ -349,7 +295,7 @@ export type { ObjectInputs, ComputeOnceOut, ParsedFile, - InitFileConfig, + FileConfig, ForEachCallback, MapCallback, IsValid, @@ -357,5 +303,10 @@ export type { ArrayInputs, CreateArrayInputs, InputProps, - ValidateState + ValidateState, + ValidateInput, + AsyncValidateInput, + ValidateInputParams, + CustomAsyncValidationType, + ValidationResult }; diff --git a/src/util/helper.ts b/src/util/helper.ts index e56185d..da5ec8f 100644 --- a/src/util/helper.ts +++ b/src/util/helper.ts @@ -1,37 +1,24 @@ -import type { ComputeOnceOut, Helper, CreateObjectInputs } from "../types"; +import type { ComputeOnceOut, Helper } from "../types"; +let key = -1; const He = (): Helper => { - // omitted keys - const ok = {}; - //state - const s = {}; // error message const em = {}; - // tracking matching - const tm = {}; // async delay const a = {}; - const clean = (s: CreateObjectInputs) => { - for (const sKey in s) { - delete s[sKey].validation?.copy; - delete s[sKey].validation?.match; + return { + em, + a, + key: () => { + key++; + return `*_*_${key}`; } - return s; }; - return { ok, s, em, tm, a, clean }; }; const persist = {} as { [k in string]: ComputeOnceOut }; -const getKey = () => { - let i = -1; - return { - get new() { - i++; - return `*_*_${i}`; - } - }; -}; -const KEY = getKey(); -export { persist, He, KEY }; +const O = Object; + +export { persist, He, O }; diff --git a/src/util/index.ts b/src/util/index.ts index d1c5f58..28a9cd8 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,57 +1,33 @@ import type { - CreateObjectInputs, Helper, Input, InputProps, InputStore, InternalInput, - MatchResultType, - MergeType, ObjectInputs, ParsedFile, Unknown, - ValidateState, - ValidationStateType + ValidateState } from "../types"; -import { deepMatch, parseCopy, validate } from "./validation"; +import { validate } from "../inputs/validations"; import { createCheckboxValue } from "../inputs/handlers/checkbox"; import { radioIsChecked } from "../inputs/handlers/radio"; -const O = Object; +import { O } from "./helper"; -const parseValue = (input: Input, value: any) => - input.type === "number" || input.validation?.number +const parseValue = (input: Input, value: Unknown) => + input.type === "number" || (input.validation?.number as unknown) ? !isNaN(Number(value)) ? Number(value) : value : value; -const initValid = (entry: InternalInput) => { - const validation = entry.validation || {}; - const isValid = !O.keys(validation).length; - // return !["", 0, null, undefined].includes(entry.value); - // return isValid ? !!entry.checked : false; - return entry.checked ? true : isValid; -}; - -const lockProps = (entry: Input) => { - return { - id: entry.id, - name: entry.name, - type: entry.type, - value: entry.value, - checked: entry.checked, - multiple: entry.multiple, - placeholder: entry.placeholder - } as InputProps; -}; - // Spread common props const commonProps = (entry: InternalInput, id: string) => { const defaultID = entry.id ?? id; return { - id: defaultID, - name: entry.name ?? defaultID, - label: entry.label ?? entry.name ?? defaultID, + id, + name: defaultID, + label: entry.name ?? defaultID, type: entry.type ?? "text", value: entry.type === "select" && entry.multiple @@ -60,203 +36,19 @@ const commonProps = (entry: InternalInput, id: string) => { ? entry.label ?? defaultID : "", checked: false, - valid: initValid(entry), + multiple: false, + valid: entry.checked ? true : !O.keys(entry.validation ?? {}).length, touched: false, - placeholder: entry.placeholder ?? entry.name ?? defaultID, + placeholder: entry.name ?? defaultID, errorMessage: undefined, validating: false, extraData: null }; }; -const setTrackingMatching = ( - helper: Helper, - target: string, - matchKey: string[] -) => { - if (helper.tm[target]) { - return [...new Set([...helper.tm[target], ...matchKey])]; - } else { - return matchKey; - } -}; - -/* - * Loop validation and merge. - * Merge help preserve value that present in the state and not in the data - * */ -const merge = ( - state: ValidationStateType, - data: ValidationStateType, - mergeProps?: MergeType -) => { - const { omit, keyPath } = mergeProps || { - omit: new Set() - }; - for (const key in data) { - // We key as keyof ValidationStateType - // because we don't want to create an unnecessary variable because of typescript - - // We give priority to the data - if (keyPath === "copy") { - if ( - state[key as keyof ValidationStateType]?.constructor.name === - "Object" && - data[key as keyof ValidationStateType]?.constructor.name === "Object" - ) { - state[key as keyof ValidationStateType] = { - ...state[key as keyof ValidationStateType], - ...data[key as keyof ValidationStateType] - }; - } else { - state[key as keyof ValidationStateType] = - data[key as keyof ValidationStateType]; - } - } - // We give priority to the last matched result - if (keyPath === "match" && state[key as keyof ValidationStateType]) { - if ( - state[key as keyof ValidationStateType]?.constructor.name === - "Object" && - data[key as keyof ValidationStateType]?.constructor.name === "Object" - ) { - state[key as keyof ValidationStateType] = { - value: state[key as keyof ValidationStateType].value, - message: - data[key as keyof ValidationStateType].message ?? - state[key as keyof ValidationStateType].message - }; - } - if ( - state[key as keyof ValidationStateType]?.constructor.name !== - "Object" && - data[key as keyof ValidationStateType]?.constructor.name === "Object" - ) { - state[key as keyof ValidationStateType] = { - value: state[key as keyof ValidationStateType], - message: data[key as keyof ValidationStateType].message - }; - } - } - } - omit?.forEach((k: keyof ValidationStateType) => { - delete state[k]; - }); - - return state; -}; - -// Match and copy input validation -const mcv = ( - helper: Helper, - state: CreateObjectInputs, - stateKey: string, - matchOrCopyKey: string, - keyPath: keyof ValidationStateType -) => { - let matchResult = {} as MatchResultType; - /* - * We check if the matched input also match someone until find the last one who doesn't have a match property - * and use last not matched validation to match all input in the hierarchy. - * We add trackMatching for validation tool. See validate function in validation folder - * */ - try { - matchResult = deepMatch(helper, state, stateKey, matchOrCopyKey, keyPath); - } catch (_) { - throw Error( - "It seems that an ID is missing or we have infinite match here. Please make sure that the copied or matched input has an id and the last matched or copied input does not match or copy anyone" - ); - } - - /* - * Mmv is merge matched validation - * We merge resultValidation with all matched validation - * */ - let mmv: ValidationStateType = {}; - - matchResult.mk.forEach((v) => { - mmv = merge(mmv, helper.s[v].validation as ValidationStateType, { - keyPath - }); - - // We updated every matched input found with the appropriate validation - state[v].validation = { - ...merge( - { ...matchResult.v }, - state[v].validation as ValidationStateType, - { omit: helper.ok[v], keyPath } - ) - }; - - if (keyPath === "match") { - // Setting the trackMatching for every matched key - helper.tm[v] = setTrackingMatching(helper, v, [ - stateKey, - ...matchResult.mk.filter((value) => value !== v), - matchResult.lm - ]); - } - }); - - // We updated the current input with the appropriate validation - state[stateKey].validation = { - ...merge( - merge({ ...matchResult.v }, mmv, { - omit: helper.ok[stateKey], - keyPath - }), - state[stateKey].validation as ValidationStateType, - { - keyPath - } - ), - ...(keyPath === "match" - ? { - match: matchOrCopyKey - } - : { - copy: helper.s[stateKey].validation?.copy - }) - } as ValidationStateType; - - if (keyPath === "match") { - // Setting the trackMatching for current key - helper.tm[stateKey] = setTrackingMatching(helper, stateKey, [ - ...matchResult.mk, - matchResult.lm - ]); - // Setting the trackMatching for last matched - helper.tm[matchResult.lm] = setTrackingMatching(helper, matchResult.lm, [ - stateKey, - ...matchResult.mk - ]); - } - - // We save the error message because, the errorMessage is dynamic, and we need to fall back to the original if needed - helper.em[stateKey] = - state[stateKey].errorMessage ?? - state[matchOrCopyKey].errorMessage ?? - state[matchResult.lm].errorMessage; - - return state; -}; - -/** - * Mr is matching rules - * We check suspicious validation key and match key which is a typical scenario for password and confirm Password - * The validation system for matched values need them to both have the validation options. - * For example, a user enter - * { - * password: { - * validation: { - * minLength: 4 - * } - * }, - * confirmPassword: {validation: {match: "password"}} - * }. - * - */ -const matchRules = (state: CreateObjectInputs, helper: Helper) => { +// Sync checkbox and radio input and validate the form to get initial valid state +const syncRCAndValid = (entry: ObjectInputs, helper: Helper) => { + let isValid = true; const patch = { checkbox: { tab: [] @@ -266,76 +58,59 @@ const matchRules = (state: CreateObjectInputs, helper: Helper) => { } } as Unknown; - for (const stateKey in state) { - const validation = state[stateKey].validation; - const errorMessage = state[stateKey].errorMessage; - const type = state[stateKey].type; - - if (type === "checkbox" || type === "radio") { - patch[type].tab.push(stateKey); - if (!state[stateKey].valid && !patch[type].fv) { - patch[type][state[stateKey].name as string] = { - validation, - errorMessage + for (const stateKey in entry) { + const i = entry[stateKey]; + if (i.type === "checkbox" || i.type === "radio") { + patch[i.type].tab.push(stateKey); + if (!i.valid && !patch[i.type].fv) { + patch[i.type][i.name as string] = { + validation: i.validation, + errorMessage: i.errorMessage }; - // found validation - patch[type].fv = true; + // Found validation + patch[i.type].fv = true; } } // we save the error message - helper.em[stateKey] = state[stateKey].errorMessage; - // If an input want to copy validation from another input - const copyKey = state[stateKey].validation?.copy; - - if (copyKey) { - state = mcv( - helper, - state, - stateKey, - parseCopy(copyKey, "copy").value, - "copy" - ); - } - - /* We get the key to match with, we are trying to see - * for example if confirmPassword want to match validate - * from password - **/ - const matchKey = state[stateKey].validation?.match; - - /* we check if validate and key to match exist else we throw error - * For example, we check if state.confirmPassword.validate exists, and we check if matchKey - * state.confirmPassword.validate.match exists - */ - if (matchKey) { - state = mcv(helper, state, stateKey, matchKey, "match"); - } + helper.em[stateKey] = i.errorMessage; + // we add props + i.props = { + id: i.id, + name: i.name, + type: i.type, + value: i.value, + checked: i.checked, + multiple: i.multiple, + placeholder: i.placeholder + } as InputProps; + + // Validating form to get initial valid state + isValid = isValid && !i.validating && i.valid; } O.keys(patch).forEach((o) => { if (patch[o].fv) { patch[o].tab.forEach((id: string) => { // we get the name - const name = state[id].name as string; + const name = entry[id].name as string; // define errorMessage const errorMessage = - state[id].errorMessage ?? patch[o][name].errorMessage; + entry[id].errorMessage ?? patch[o][name].errorMessage; // we save the error message helper.em[id] = errorMessage; // define validation - state[id].validation = !state[id].valid - ? state[id].validation + entry[id].validation = !entry[id].valid + ? entry[id].validation : patch[o][name].validation; // set errorMessage - state[id].errorMessage = errorMessage; + entry[id].errorMessage = errorMessage; // set valid - state[id].valid = patch[o][name].validation ? false : state[id].valid; + entry[id].valid = patch[o][name].validation ? false : entry[id].valid; }); } }); - - return state; + return { entry, isValid }; }; const touchInput = (store: InputStore, helper: Helper) => { @@ -369,7 +144,6 @@ const touchInput = (store: InputStore, helper: Helper) => { }; // Validate the state -// Set form is valid const validateState = (data: ObjectInputs): ValidateState => { let isValid = true; let invalidKey = null; @@ -403,7 +177,7 @@ const cleanFiles = (files: ParsedFile[]) => { // E extract values from state const extractValues = (state: ObjectInputs) => { - const result = {} as { [k in string]: any }; + const result = {} as { [k in string]: Unknown }; for (const key in state) { const K = state[key].name; if (state[key].type === "radio") { @@ -432,11 +206,9 @@ const extractValues = (state: ObjectInputs) => { export { commonProps, validateState, - matchRules, + syncRCAndValid, transformToArray, extractValues, parseValue, - touchInput, - lockProps, - O + touchInput }; diff --git a/src/util/validation.ts b/src/util/validation.ts deleted file mode 100644 index 5375f16..0000000 --- a/src/util/validation.ts +++ /dev/null @@ -1,389 +0,0 @@ -import type { - AsyncCallback, - CopyKeyObjType, - CopyType, - ErrorMessageType, - InputStore, - MatchResultType, - CreateObjectInputs, - Input, - ObjectInputs, - ValidationStateType, - Unknown, - Helper -} from "../types"; - -const validateEmail = (email: string) => { - const re = - /^(([^<>()\]\\.,;:\s@"]+(\.[^<>()\]\\.,;:\s@"]+)*)|(".+"))@(([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - return re.test(String(email).toLowerCase()); -}; - -// Parse copy key -const parseCopy = ( - key: CopyType | undefined | string, - keyPath: keyof ValidationStateType -): CopyKeyObjType => { - if (keyPath === "match") { - return { - value: "", - omit: new Set() - }; - } - if (key?.constructor.name === "Object") { - return { - value: (key as CopyType).value, - omit: (key as CopyType).omit ? new Set((key as CopyType).omit) : new Set() - }; - } - return { - value: key as string, - omit: new Set() - }; -}; - -// Deep match -const deepMatch = ( - helper: Helper, - state: CreateObjectInputs, - stateKey: string, - matchKey: string, - keyPath: keyof ValidationStateType -) => { - const matchKeys: string[] = []; - let result = {} as MatchResultType; - helper.ok[stateKey] = parseCopy( - state[stateKey].validation?.copy, - keyPath - ).omit; - - const match = (matchKey: string, keyPath: keyof ValidationStateType) => { - // We get omitted path from user - const userOmit = parseCopy(state[stateKey].validation?.copy, keyPath).omit; - - if ( - // state[matchKey] exists - state[matchKey] && - // state[matchKey].validation exists - typeof state[matchKey].validation !== "undefined" && - // state[matchKey].validation contains a match or copy key - typeof state[matchKey].validation![keyPath] !== "undefined" && - // Input don't want the copied part - !userOmit.has("copy") - ) { - new Set([ - // We get omitted path from matchedKey and merge it with userOmit. - /* - * If an input omit a validation and match another input who omit another validation, - * then both omit same validations - * */ - ...parseCopy(state[matchKey].validation?.copy, keyPath).omit, - ...userOmit - ]).forEach((k) => { - // We save omitted path for the current key - helper.ok[stateKey].add(k); - }); - matchKeys.push(matchKey); - // Next round with a new matchKey - match(getValue(state[matchKey].validation![keyPath]), keyPath); - } else { - // By default, validation is the original validation of the last matched key - let validation: ValidationStateType | undefined = { - ...helper.s[matchKey].validation - }; - /* If we want the copied part or the matched part, we override the validation with - * the validation of the last matched found already updated while looping - */ - if (!userOmit.has("copy") && !userOmit.has("match")) { - validation = state[matchKey] ? state[matchKey].validation : {}; - } - result = { lm: matchKey, mk: matchKeys, v: validation }; - } - }; - - // We start the matching process - match(matchKey, keyPath); - return result; -}; - -const getErrorMessage = (helper: Helper, rule: any, target: string) => { - if (rule && rule?.constructor.name === "Object") { - return rule.message ?? helper.em[target]; - } - return helper.em[target]; -}; - -// Can be any rules -const getValue = (rule: any) => { - return rule?.constructor.name === "Object" ? rule.value : rule; -}; - -// V is validate -const validate = ( - helper: Helper, - state: ObjectInputs, - target: string, - value: Unknown -) => { - const entry: Input = state[target]; - const rules: ValidationStateType = entry.validation || {}; - let valid: boolean = true; - const em: ErrorMessageType | undefined = helper.em[target]; - // Required - if (typeof rules.required !== "undefined") { - if ( - (entry.type === "select" && entry.multiple) || - entry.type === "file" || - entry.type === "checkbox" - ) { - valid = value !== null && value.length > 0 && valid; - } - valid = - typeof value === "string" - ? value.trim() !== "" && valid - : value !== null && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.required, target) - }; - } - - // Number - if (typeof rules.number !== "undefined") { - valid = !isNaN(value) && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.number, target) - }; - } - - // Min - if (typeof rules.min !== "undefined") { - valid = Number(value) >= getValue(rules.min) && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.min, target) - }; - } - - // Max - if (typeof rules.max !== "undefined") { - valid = Number(value) <= getValue(rules.max) && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.max, target) - }; - } - - // Starts with - if (typeof rules?.startsWith !== "undefined" && typeof value === "string") { - valid = - value.length > 0 && value.startsWith(getValue(rules.startsWith)) && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.startsWith, target) - }; - } - - // MinLength - if (typeof rules.minLength !== "undefined") { - valid = value?.length >= getValue(rules.minLength) && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.minLength, target) - }; - } - - // MinLengthWithoutSpace - if ( - typeof rules.minLengthWithoutSpace !== "undefined" && - typeof value === "string" - ) { - valid = - value.indexOf(" ") === -1 && - value.trim().length >= getValue(rules.minLengthWithoutSpace) && - valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.minLengthWithoutSpace, target) - }; - } - - // MaxLength - if (typeof rules.maxLength !== "undefined") { - valid = value?.length <= getValue(rules.maxLength) && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.maxLength, target) - }; - } - - // MaxLengthWithoutSpace - if ( - typeof rules.maxLengthWithoutSpace !== "undefined" && - typeof value === "string" - ) { - valid = - value.indexOf(" ") === -1 && - value.trim().length <= getValue(rules.maxLengthWithoutSpace) && - valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.maxLengthWithoutSpace, target) - }; - } - - // Email - if (typeof rules.email !== "undefined" && typeof value === "string") { - valid = validateEmail(value) && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.email, target) - }; - } - - // Regex - if (typeof rules.regex !== "undefined") { - valid = getValue(rules.regex)?.test(value) && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.regex, target) - }; - } - - // ends with - if (typeof rules?.endsWith !== "undefined" && typeof value === "string") { - valid = - value.length > 0 && value.endsWith(getValue(rules.endsWith)) && valid; - } - if (!valid) { - return { - valid, - em: getErrorMessage(helper, rules.endsWith, target) - }; - } - - // if (typeof rules.__ !== "undefined") { - if (typeof helper.tm[target] !== "undefined") { - /* We get the match key here. - * For example, if we are typing in password, then matchKeys is confirmPassword - * f we are typing in confirmPassword then matchKeys is password and so on - */ - const matchKeys = helper.tm[target]; - - /* We save the current valid value which comes from top functions with validation rules.*/ - let currentInputValidStatus: boolean = valid; - /* - * We loop and check if typed value match all matched key value and break the loop. - * But before breaking loop, we override the currentInputValidStatus status with the new one - * */ - for (let i = 0; i < matchKeys.length; i++) { - const m = matchKeys[i]; - // We validate only if input is touched - if (state[m].touched) { - /* we override the current valid status only if currentInputValidStatus is true - * and if value === state[m].value where m in one of matched key in loop. - * If currentInputValidStatus === false, we revalidate current input find in the loop - * with currentInputValidStatus, create an error message and break - */ - currentInputValidStatus = - currentInputValidStatus && value === state[m].value; - // Revalidating current input found in the loop with currentInputValidStatus status - state[m].valid = currentInputValidStatus; - // currentInputValidStatus is false - if (!currentInputValidStatus) { - break; - } - } - } - if (!currentInputValidStatus) { - return { - valid: currentInputValidStatus, - em: getErrorMessage(helper, rules.match, target) - }; - } - } - if (!entry.validation?.asyncCustom && typeof rules.custom !== "undefined") { - let eM: ErrorMessageType | null = null; - valid = rules.custom(value, (m: ErrorMessageType) => (eM = m)); - if ((typeof valid as unknown) !== "boolean") { - throw TypeError("Your custom response is not a boolean"); - } - if (!valid) { - return { valid, em: eM ?? helper.em[target] }; - } - } - return { valid, em }; -}; - -// Async validation -const asyncValidation = ( - store: InputStore, - helper: Helper, - state: ObjectInputs, - target: string, - value: unknown, - callback: AsyncCallback -) => { - const entry: Input = state[target]; - clearTimeout(helper.a[entry.key]); - helper.a[entry.key] = setTimeout(() => { - // Save the time - const ST = helper.a[entry.key]; - const rules: Required = entry.validation; - let eM: ErrorMessageType | null = null; - Promise.resolve(rules.asyncCustom(value, (m: ErrorMessageType) => (eM = m))) - .then((value) => { - if (typeof value !== "boolean") { - throw TypeError("Your custom response is not a boolean"); - } - /* we check if time match the request id time - * If not, that means, another request has been sent. - * So we wait for that response - * */ - if (ST === helper.a[entry.key]) { - callback({ - valid: value, - em: eM ?? helper.em[target], - entry, - store, - helper - }); - } - }) - .catch((error) => { - console.error(error); - callback({ - valid: false, - failed: true, - entry, - store, - helper - }); - }); - }, store.get("asyncDelay")); -}; - -export { validate, deepMatch, asyncValidation, getValue, parseCopy }; diff --git a/tsconfig.json b/tsconfig.json index 5c180f3..b91a577 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "moduleResolution": "node", - "target": "ES2018", + "target": "ES2020", "esModuleInterop": true, "module": "ESNext", "lib": [