Skip to content

Instantly share code, notes, and snippets.

@g4rcez
Created June 19, 2024 13:30
Show Gist options
  • Save g4rcez/ad864e92d19c68aa184e484bad2b5e54 to your computer and use it in GitHub Desktop.
Save g4rcez/ad864e92d19c68aa184e484bad2b5e54 to your computer and use it in GitHub Desktop.
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