Skip to content

Instantly share code, notes, and snippets.

@ecesar88
Last active June 5, 2024 19:06
Show Gist options
  • Save ecesar88/6de39affb4d82cd374baa2526503fb38 to your computer and use it in GitHub Desktop.
Save ecesar88/6de39affb4d82cd374baa2526503fb38 to your computer and use it in GitHub Desktop.
useForm.tsx
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,
});
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;
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