prasi-lib/comps/form/Form.tsx

231 lines
5.7 KiB
TypeScript
Executable File

import { Form as FForm } from "@/comps/ui/form";
import { Toaster } from "@/comps/ui/sonner";
import { useLocal } from "@/utils/use-local";
import { FC } from "react";
import { createPortal } from "react-dom";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { FormHook } from "./utils/utils";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { cn } from "@/utils";
export const Form: FC<{
on_init: (arg: { submit: any }) => any;
on_load: () => any;
on_submit: (arg: { form: any; error: any }) => Promise<any>;
body: any;
form: FormHook;
PassProp: any;
cache: () => any;
layout: "auto" | "1-col" | "2-col";
}> = ({
on_init,
on_load,
body,
form,
PassProp,
on_submit,
cache,
layout: _layout,
}) => {
const form_hook = useForm<any>({
defaultValues: {},
});
const local = useLocal({
el: null as any,
submit_timeout: null as any,
layout: "unknown" as "unknown" | "2-col" | "1-col",
init: false,
});
form.hook = form_hook;
if (!form.cache && typeof cache === "function") {
try {
form.cache = cache() || {};
} catch (e) {}
}
if (!form.cache) form.cache = {};
if (!form.validation) {
form.validation = {};
}
if (!form.label) {
form.label = {};
}
let layout = _layout || "auto";
if (layout !== "auto") local.layout = layout;
const submit = () => {
clearTimeout(local.submit_timeout);
local.submit_timeout = setTimeout(async () => {
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Saving ...
</>,
{
dismissible: true,
className: css`
background: #e4f7ff;
`,
}
);
const data = form.hook.getValues();
form.hook.clearErrors();
for (const [k, v] of Object.entries(form.validation)) {
if (v === "required") {
if (!data[k]) {
const error = {
type: "required",
message: `${form.label[k] || k} is required.`,
};
form.hook.formState.errors[k] = error;
form.hook.setError(k, error);
}
}
}
await on_submit({
form: data,
error: form.hook.formState.errors,
});
toast.dismiss();
if (Object.keys(form.hook.formState.errors).length > 0) {
toast.error(
<div className="c-flex c-text-red-600 c-items-center">
<AlertTriangle className="c-h-4 c-w-4 c-mr-1" />
Save Failed, please correct{" "}
{Object.keys(form.hook.formState.errors).length} errors.
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
} else {
toast.success(
<div className="c-flex c-text-blue-700 c-items-center">
<Check className="c-h-4 c-w-4 c-mr-1 " />
Data saved
</div>,
{
className: css`
background: #e4f5ff;
border: 2px solid blue;
`,
}
);
}
}, 300);
};
if (!local.init) {
local.init = true;
on_init({ submit });
const res = on_load();
const loaded = (values: any) => {
setTimeout(() => {
toast.dismiss();
});
if (!!values) {
for (const [k, v] of Object.entries(values)) {
form.hook.setValue(k, v);
}
}
local.render();
};
if (res instanceof Promise) {
setTimeout(() => {
if (!isEditor) {
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Loading data...
</>
);
}
res.then(loaded);
});
} else {
loaded(res);
}
}
form.submit = submit;
if (document.getElementsByClassName("prasi-toaster").length === 0) {
const elemDiv = document.createElement("div");
elemDiv.className = "prasi-toaster";
document.body.appendChild(elemDiv);
}
const toaster_el = document.getElementsByClassName("prasi-toaster")[0];
return (
<FForm {...form_hook}>
{toaster_el && createPortal(<Toaster cn={cn} />, toaster_el)}
<form
className={
"flex-1 flex flex-col w-full items-stretch relative overflow-auto"
}
ref={(el) => {
if (el) form.ref = el;
}}
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
submit();
}}
>
<div
ref={(el) => {
if (el && layout === "auto" && local.layout === "unknown") {
let cur: any = el;
let i = 0;
while (
cur.parentNode &&
cur.getBoundingClientRect().width === 0
) {
cur = cur.parentNode;
i++;
if (i > 10) {
break;
}
}
if (cur.getBoundingClientRect().width < 500) {
local.layout = "1-col";
} else {
local.layout = "2-col";
}
local.render();
}
}}
className={cx(
local.layout === "unknown" && "c-hidden",
local.layout === "2-col" &&
css`
> div {
flex-direction: row;
flex-wrap: wrap;
> div {
width: 50%;
}
}
`
)}
>
<PassProp submit={submit} data={form_hook.getValues()}>
{body}
</PassProp>
</div>
</form>
</FForm>
);
};