Last active
June 5, 2024 19:06
-
-
Save ecesar88/6de39affb4d82cd374baa2526503fb38 to your computer and use it in GitHub Desktop.
useForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import getValidations from "./validations"; | |
import useForm from "src/hooks/useForm"; | |
const { | |
handleSubmit, | |
registerKey, | |
clearErrors, | |
clearForm, | |
validations: { validateSingleSchema, schema: validationSchema }, | |
formState: { errors, payload, setPayload }, | |
} = useForm<IFormData>({ | |
validations: { | |
enable: true, | |
getValidations, | |
}, | |
initialState, | |
}); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState } from "react"; | |
export interface IValidate { | |
description?: string; | |
errorMessage?: string; | |
name: string; | |
validate: () => boolean; | |
} | |
export interface IValidationSchema { | |
keyName: string; | |
validations: IValidate[]; | |
} | |
export interface IUseForm<T> { | |
initialState?: T; | |
validations?: { | |
enable: boolean; | |
getValidations: (payload: T) => IValidationSchema[]; | |
}; | |
} | |
export interface IErrors { | |
error: string; | |
description: string; | |
errorMessage: string; | |
validations: Omit<IValidate, "validate">[]; | |
} | |
export interface IUseFormReturn<T> { | |
handleSubmit: ( | |
evt: React.FormEvent<HTMLFormElement>, | |
callback: (data: T) => void | |
) => Record<string, IErrors>; | |
registerKey: (key: keyof T, value: any) => void; | |
clearErrors: () => void; | |
clearForm: () => void; | |
validations: { | |
validateAll: (validationSchema: IValidationSchema[]) => boolean; | |
validateSingleSchema: (schema: IValidationSchema) => boolean; | |
schema: IValidationSchema[]; | |
}; | |
formState: { | |
payload: T; | |
setPayload: React.Dispatch<React.SetStateAction<T>>; | |
errors: Record<string, IErrors>; | |
}; | |
} | |
function useForm<T>({ | |
validations: { enable: enableValidations, getValidations }, | |
initialState, | |
}: IUseForm<T>): IUseFormReturn<T> { | |
const [payload, setPayload] = useState<T>(initialState); | |
const [errors, setErrors] = useState<Record<string, IErrors>>({}); | |
const validationSchema = getValidations(payload); | |
const copyAndRemoveTheValidateFn = (itemKey: string) => { | |
// Deep copy the schema to remove the validation function from the errors | |
const schemaCopy: IValidationSchema[] = JSON.parse( | |
JSON.stringify(validationSchema) | |
); | |
// Remove the validation function from the errors that are going to be shown on submit | |
schemaCopy.forEach((schema) => | |
schema.validations.forEach((validation) => delete validation?.validate) | |
); | |
return schemaCopy?.find((key) => key.keyName === itemKey).validations; | |
}; | |
const firstValidationThatDidntPass = (schema: IValidationSchema) => | |
schema?.validations?.find((validation) => !validation?.validate()); | |
const validateAll = (validationSchema: IValidationSchema[]): boolean => { | |
// There is no neeed to validate if there is no validations to run | |
if (!validationSchema?.length) { | |
throw new Error( | |
"A validation schema is empty. It should be an array: IValidationSchema[] and have at least one validation (On validateAll)" | |
); | |
} | |
// Clear all the errors first, before validating anything | |
setErrors({}); | |
// Run all the validations for a specific schema | |
const results = validationSchema.map((singleSchema) => | |
singleSchema?.validations?.map((v) => Boolean(v.validate())) | |
); | |
// If all validations didn't pass | |
if ( | |
!results.every((validations) => | |
validations.every((validation) => validation === true) | |
) | |
) { | |
const errorsObject = {}; | |
// Get only the validations that didn't pass | |
const checkErrors = validationSchema?.filter((schema) => | |
schema?.validations?.some((validation) => !validation.validate()) | |
); | |
checkErrors.forEach((error) => { | |
const validationError = firstValidationThatDidntPass(error); | |
errorsObject[error?.keyName] = { | |
error: validationError?.name ?? "", | |
description: validationError?.description ?? "", | |
errorMessage: validationError?.errorMessage ?? "", | |
validations: copyAndRemoveTheValidateFn(error?.keyName), | |
}; | |
}); | |
setErrors(errorsObject); | |
return false; | |
} | |
return true; | |
}; | |
const validateSingleSchema = (schema: IValidationSchema) => { | |
if (!Object.keys(schema)?.length) { | |
throw new Error( | |
"A validation schema is empty. It should be an object of type IValidationSchema and have at least one validation (On validateSingleSchema)" | |
); | |
} | |
// Get an array of the return on the validations, e.g: [true, false, true] | |
const validationsArray = schema?.validations.map((validation) => | |
validation.validate() | |
); | |
// Check if everything in that array is equal to "true", i.e: passes all the validations | |
const allTheValidationsPassed = validationsArray.every( | |
(validation) => validation === true | |
); | |
// Remove the error from the state if the all validations passed or include the error if them didn't | |
setErrors((prev) => { | |
const validationError = firstValidationThatDidntPass(schema); | |
// Add the validation error | |
const addValidationError = { | |
...prev, | |
[schema?.keyName]: { | |
error: validationError?.name ?? "", | |
description: validationError?.description ?? "", | |
errorMessage: validationError?.errorMessage ?? "", | |
validations: copyAndRemoveTheValidateFn(schema?.keyName), | |
}, | |
}; | |
// Remove the error | |
const { [schema?.keyName]: validation, ...rest } = prev; | |
return allTheValidationsPassed ? rest : addValidationError; | |
}); | |
return allTheValidationsPassed; | |
}; | |
// Clear form | |
const clearForm = () => { | |
clearErrors(); | |
setPayload({} as T); | |
}; | |
// Clear all errors | |
const clearErrors = () => { | |
setErrors({}); | |
}; | |
// Set a key in the payload | |
const registerKey = (key: keyof T, value: any) => { | |
setPayload((prev) => ({ | |
...prev, | |
[key]: value, | |
})); | |
}; | |
const handleSubmit = ( | |
evt: React.FormEvent<HTMLFormElement>, | |
callback: (data: T) => void | |
) => { | |
evt?.persist(); | |
evt?.preventDefault(); | |
if (enableValidations && !validateAll?.(validationSchema)) { | |
return errors; | |
} | |
callback?.(payload); | |
}; | |
return { | |
registerKey, | |
validations: { | |
validateSingleSchema, | |
validateAll, | |
schema: validationSchema, | |
}, | |
handleSubmit, | |
clearErrors, | |
clearForm, | |
formState: { | |
payload, | |
setPayload, | |
errors, | |
}, | |
}; | |
} | |
export default useForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { isBefore, isAfter, isValid } from "date-fns"; | |
import { IFormData } from "src/types/form"; | |
import { IValidationSchema } from "src/types/useForm"; | |
import { isDateValid, parseDate, datePtBrToISO8601 } from "src/utils/functions"; | |
const getValidations = (payload: IFormData): IValidationSchema[] => [ | |
{ | |
keyName: "exit-entrance", | |
validations: [ | |
{ | |
name: "at-least-one-must-be-selected", | |
description: "At least one must be selected", | |
errorMessage: "Selecione pelo menos uma das alternativas", | |
validate: () => payload?.entrance || payload?.exit, | |
}, | |
], | |
}, | |
{ | |
keyName: "startDate", | |
validations: [ | |
{ | |
name: "needs-to-be-filled", | |
description: "Needs to be filled", | |
errorMessage: "Campo obrigatório", | |
validate: () => !!payload?.startDate?.length, | |
}, | |
{ | |
name: "assert-date-is-valid", | |
description: "Is date valid", | |
errorMessage: "Data inválida", | |
validate: () => { | |
return ( | |
payload?.startDate?.length === 10 && isDateValid(payload?.startDate) | |
); | |
}, | |
}, | |
{ | |
name: "startDate>endDate", | |
description: "startDate can't be after endDate", | |
errorMessage: "Data inicial não pode ser maior que data final", | |
validate: () => { | |
if ( | |
!isDateValid(payload?.startDate) || | |
!isDateValid(payload?.endDate) | |
) { | |
return true; | |
} | |
if ( | |
(isDateValid(payload?.startDate) || | |
isDateValid(payload?.endDate)) && | |
payload?.startDate === payload?.endDate | |
) { | |
return true; | |
} | |
const parsedStartDate = parseDate( | |
datePtBrToISO8601(payload?.startDate) | |
) as Date; | |
const parsedEndDate = parseDate(datePtBrToISO8601(payload?.endDate)); | |
return isBefore(parsedStartDate, parsedEndDate); | |
}, | |
}, | |
], | |
}, | |
{ | |
keyName: "endDate", | |
validations: [ | |
{ | |
name: "needs-to-be-filled", | |
description: "Needs to be filled", | |
errorMessage: "Campo obrigatório", | |
validate: () => !!payload?.endDate?.length, | |
}, | |
{ | |
name: "assert-date-is-valid", | |
description: "Is date valid", | |
errorMessage: "Data inválida", | |
validate: () => | |
payload?.endDate?.length === 10 && isDateValid(payload?.endDate), | |
}, | |
{ | |
name: "endDate<startDate", | |
description: "endDate can't be before startDate", | |
errorMessage: "Data final não pode ser menor que data inicial", | |
validate: () => { | |
if ( | |
!isDateValid(payload?.startDate) || | |
!isDateValid(payload?.endDate) | |
) { | |
return true; | |
} | |
if ( | |
(isDateValid(payload?.startDate) || | |
isDateValid(payload?.endDate)) && | |
payload?.startDate === payload?.endDate | |
) { | |
return true; | |
} | |
const parsedStartDate = parseDate( | |
datePtBrToISO8601(payload?.startDate) | |
); | |
const parsedEndDate = parseDate(datePtBrToISO8601(payload?.endDate)); | |
return isAfter(parsedEndDate, parsedStartDate); | |
}, | |
}, | |
{ | |
name: "endDate>today", | |
description: "endDate can't be greater than today", | |
errorMessage: "Data final não pode ser maior que o dia de hoje", | |
validate: () => { | |
if (!isDateValid(payload?.endDate)) return true; | |
const parsedEndDate = parseDate(datePtBrToISO8601(payload?.endDate)); | |
return !isAfter(parsedEndDate, new Date()); | |
}, | |
}, | |
], | |
}, | |
]; | |
export default getValidations; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment