Created
June 19, 2024 13:30
-
-
Save g4rcez/ad864e92d19c68aa184e484bad2b5e54 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 { formToJson } from "brouther"; | |
import React, { useCallback, useEffect, useRef, useState } from "react"; | |
import { AllPaths, Is, setPath } from "sidekicker"; | |
import { z, ZodArray, ZodNumber } from "zod"; | |
import { formReset, InputProps } from "~/components"; | |
export const convertPath = (path: string) => path.replace("[", ".").replace("]", "").split("."); | |
export const getSchemaShape = <T extends z.ZodObject<any>>(name: string, schema: T) => | |
convertPath(name).reduce((acc, el) => { | |
if (el === "") return acc; | |
const shape = acc.shape[el] || acc; | |
return shape instanceof ZodArray ? shape.element : shape; | |
}, schema); | |
const getValueByType = (e: HTMLInputElement) => { | |
if (e.type === "checkbox") return e.checked; | |
if (e.type === "number") return e.valueAsNumber; | |
return e.value; | |
}; | |
type CustomOnInvalid = (args: { form: HTMLFormElement; errors: Record<string, string> }) => any; | |
type CustomOnSubmit<T> = (args: { | |
json: T; | |
form: HTMLFormElement; | |
reset: () => void; | |
event: React.FormEvent<HTMLFormElement>; | |
}) => any; | |
export const useForm = <T extends z.ZodObject<any>>(schema: T) => { | |
const [errors, setErrors] = useState<Record<string, string | undefined> | null>(null); | |
const ref = useRef<Record<string, { element: HTMLElement; schema: z.ZodType }>>({}); | |
const input = <Props extends InputProps>(name: AllPaths<z.infer<T>>, props?: Props): Props => { | |
const validator = getSchemaShape(name, schema); | |
return { | |
...props, | |
name, | |
id: name, | |
type: validator instanceof ZodNumber ? "number" : props?.type ?? "text", | |
error: errors?.[name], | |
ref: (e: HTMLInputElement) => { | |
if (e === null) return; | |
ref.current[name] = { element: e, schema: validator }; | |
} | |
} as any; | |
}; | |
useEffect(() => { | |
const events = Object.values(ref.current).map((input) => { | |
const validation = input.schema.safeParse(getValueByType(input.element as any)); | |
const onBlurField = (e: any) => { | |
const validation = input.schema.safeParse(getValueByType(e.target)); | |
const html = input.element as HTMLInputElement; | |
const name = html.name; | |
if (validation.success) { | |
html.setCustomValidity(""); | |
return setErrors((prev) => { | |
const { [name]: removed, ...rest } = prev || {}; | |
return rest === null || Is.empty(rest) ? null : rest; | |
}); | |
} | |
const errorMessage = validation.error.issues[0].message; | |
html.setCustomValidity(errorMessage); | |
setErrors((prev) => ({ ...prev, [name]: errorMessage })); | |
}; | |
input.element.addEventListener("blur", onBlurField); | |
return { | |
input, | |
hasInitialError: !validation.success, | |
unsubscribe: () => input.element.removeEventListener("blur", onBlurField) | |
}; | |
}); | |
const hasErrors = events.some((x) => x.hasInitialError); | |
if (hasErrors) setErrors({}); | |
return () => { | |
events.forEach((item) => { | |
item.unsubscribe(); | |
}); | |
}; | |
}, []); | |
const onInvalid = useCallback( | |
(exec?: CustomOnInvalid) => (event: React.FormEvent<HTMLFormElement>) => { | |
event.preventDefault(); | |
const form = event.currentTarget; | |
const validationErrors = Object.values(ref.current).reduce((acc, input) => { | |
const field = input.element as HTMLInputElement; | |
const validation = input.schema.safeParse(getValueByType(field)); | |
if (validation.success) return acc; | |
const errorMessage = validation.error.issues[0].message; | |
field.setAttribute("data-initialized", "true"); | |
return { ...acc, [field.name]: errorMessage }; | |
}, {}); | |
setErrors(validationErrors); | |
exec?.({ form, errors: validationErrors }); | |
}, | |
[] | |
); | |
const onSubmit = useCallback( | |
(exec: CustomOnSubmit<z.infer<T>>) => (event: React.FormEvent<HTMLFormElement>) => { | |
event.preventDefault(); | |
const form = event.currentTarget; | |
let json = formToJson(form); | |
Array.from(form.elements).forEach((field) => { | |
if (field.tagName === "INPUT") { | |
const input = field as HTMLInputElement; | |
json = setPath(json, input.name, getValueByType(input)); | |
} | |
}); | |
exec({ form, json, event, reset: () => formReset(form) }); | |
}, | |
[] | |
); | |
return { input, onSubmit, errors, onInvalid, disabled: errors !== null }; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment