This commit is contained in:
rizky 2024-04-11 15:57:28 -07:00
parent 70a4422600
commit 7ac64362da
7 changed files with 170 additions and 30 deletions

View File

@ -1,10 +1,11 @@
import { useLocal } from "@/utils/use-local";
import { FC, useEffect } from "react";
import { FC, useEffect, useRef } from "react";
import { FMInternal, FMProps } from "./typings";
import { formReload } from "./utils/reload";
import { formInit } from "./utils/init";
import { createPortal } from "react-dom";
import { Toaster } from "sonner";
import get from "lodash.get";
export const Form: FC<FMProps> = (props) => {
const { PassProp, body } = props;
@ -15,6 +16,9 @@ export const Form: FC<FMProps> = (props) => {
formReload(fm);
},
fields: {},
events: {
on_change(name: string, new_value: any) {},
},
submit: null as any,
error: {} as any,
internal: {
@ -25,6 +29,33 @@ export const Form: FC<FMProps> = (props) => {
},
},
props: {} as any,
size: {
width: 0,
height: 0,
field: "full",
},
});
const ref = useRef({
el: null as null | HTMLFormElement,
rob: new ResizeObserver(([e]) => {
fm.size.height = e.contentRect.height;
fm.size.width = e.contentRect.width;
if (fm.status === "ready") fm.status = "resizing";
if (fm.props.layout === "auto") {
if (fm.size.width > 650) {
fm.size.field = "half";
} else {
fm.size.field = "full";
}
} else {
if (fm.props.layout === "1-col") fm.size.field = "full";
if (fm.props.layout === "2-col") fm.size.field = "half";
}
fm.render();
}),
});
useEffect(() => {
@ -41,10 +72,45 @@ export const Form: FC<FMProps> = (props) => {
}
const toaster_el = document.getElementsByClassName("prasi-toaster")[0];
const childs = get(
body,
"props.meta.item.component.props.body.content.childs"
) as any[];
if (fm.status === "resizing") return null;
return (
<>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
fm.submit();
}}
ref={(el) => {
if (!ref.current.el && el) {
ref.current.el = el;
ref.current.rob.observe(el);
}
}}
className={cx(
"form c-flex-1 c-w-full c-h-full c-relative c-overflow-auto"
)}
>
{toaster_el && createPortal(<Toaster cn={cx} />, toaster_el)}
{fm.status !== "init" && <PassProp fm={fm}>{body}</PassProp>}
</>
<div
className={cx(
"form-inner c-flex c-flex-1 c-flex-wrap c-items-start c-content-start c-absolute c-inset-0"
)}
>
{fm.status !== "init" &&
childs.map((child, idx) => {
return (
<PassProp fm={fm} key={idx}>
{child}
</PassProp>
);
})}
</div>
</form>
);
};

View File

@ -1,23 +1,49 @@
import { FC } from "react";
import { FC, useEffect } from "react";
import { FieldProp } from "../typings";
import { createField } from "../utils/create-field";
import { useField } from "../utils/use-field";
import { Label } from "./Label";
export const Field: FC<FieldProp> = (arg) => {
const field = createField(arg);
const { fm } = arg;
const field = useField(arg);
const mode = fm.props.label_mode;
const w = field.width;
useEffect(() => {
if (field.required && typeof field.required_msg === "function") {
const error_msg = field.required_msg(field.name);
const error_list = fm.error
.get(field.name)
.filter((e) => e !== error_msg);
if (fm.data[field.name]) {
fm.error.set(field.name, [error_msg, ...error_list]);
} else {
fm.error.set(field.name, error_list);
}
}
fm.events.on_change(field.name, fm.data[field.name]);
}, [fm.data[field.name]]);
if (field.status === "init") return null;
const mode = field.label_mode;
return (
<div
<label
className={cx(
"field",
mode === "horizontal" && "",
mode === "vertical" && ""
"c-flex",
w === "auto" && fm.size.field === "full" && "c-w-full",
w === "auto" && fm.size.field === "half" && "c-w-1/2",
w === "full" && "c-w-full",
w === "½" && "c-w-1/2",
w === "⅓" && "c-w-1/3",
w === "¼" && "c-w-1/4",
mode === "horizontal" && "c-flex-row",
mode === "vertical" && "c-flex-col"
)}
>
{mode !== "hidden" && <Label field={field} />}
</div>
{mode !== "hidden" && <Label field={field} fm={fm} />}
</label>
);
};

View File

@ -1,3 +1,4 @@
import { FC } from "react";
import { FMLocal, FieldLocal } from "../typings";
export const FieldInput: FC<{ field: FieldLocal; fm: FMLocal }> = ({

View File

@ -1,6 +1,21 @@
import { FC } from "react";
import { FieldLocal } from "../typings";
import { FMLocal, FieldLocal } from "../typings";
export const Label: FC<{ field: FieldLocal }> = ({ field }) => {
return <div className={cx("label")}>{field.label}</div>;
export const Label: FC<{ field: FieldLocal; fm: FMLocal }> = ({
field,
fm,
}) => {
return (
<div
className={cx(
"label",
fm.props.label_mode === "horizontal" &&
css`
width: ${fm.props.label_width}px;
`
)}
>
{field.label}
</div>
);
};

View File

@ -16,13 +16,14 @@ export type FMProps = {
layout: "auto" | "1-col" | "2-col";
meta: any;
item: any;
label_mode: "vertical" | "horizontal" | "hidden";
label_width: number;
};
export type FieldProp = {
name: string;
label: string;
desc?: string;
label_mode: "vertical" | "horizontal" | "hidden";
fm: FMLocal;
type:
| "text"
@ -39,6 +40,7 @@ export type FieldProp = {
| "master-link"
| "custom";
required: "y" | "n";
required_msg: (name: string) => string;
options: FieldOptions;
on_change: (arg: { value: any }) => void | Promise<void>;
PassProp: any;
@ -51,19 +53,23 @@ export type FieldProp = {
rel_table: string;
rel_fields: string[];
rel_query: () => any;
width: "auto" | "full" | "½" | "⅓" | "¼";
};
export type FMInternal = {
status: "init" | "loading" | "saving" | "ready";
status: "init" | "resizing" | "loading" | "saving" | "ready";
data: any;
reload: () => Promise<void>;
submit: () => Promise<void>;
events: {
on_change: (name: string, new_value: any) => void;
};
fields: Record<string, FieldLocal>;
error: {
list: { name: string; error: string }[];
set: (name: string, error: string) => void;
get: (name: string, error: string) => void;
clear: () => void;
readonly list: { name: string; error: string[] }[];
set: (name: string, error: string[]) => void;
get: (name: string) => string[];
clear: (name?: string) => void;
};
internal: {
reload: {
@ -73,6 +79,11 @@ export type FMInternal = {
};
};
props: Exclude<FMProps, "body" | "PassProp">;
size: {
width: number;
height: number;
field: "full" | "half";
};
};
export type FMLocal = FMInternal & { render: () => void };
@ -84,7 +95,9 @@ export type FieldInternal = {
desc: FieldProp["desc"];
prefix: FieldProp["prefix"];
suffix: FieldProp["suffix"];
label_mode: FieldProp["label_mode"];
width: FieldProp["width"];
required: boolean;
required_msg: FieldProp["required_msg"];
Child: () => ReactNode;
};
export type FieldLocal = FieldInternal & { render: () => void };

View File

@ -1,10 +1,27 @@
import { FMLocal } from "../typings";
export const formError = (fm: FMLocal) => {
const error = {} as FMLocal["error"];
error.list = [];
error.clear = () => {};
error.set = () => {};
error.get = () => {};
const error = {
_internal: {},
get list() {
const res = Object.entries(this._internal).map(([name, error]) => {
return { name, error };
});
return res;
},
clear(name) {
if (name) delete this._internal[name];
else this._internal = {};
},
get(name) {
return this._internal[name];
},
set(name, error) {
this._internal[name] = error;
},
} as FMLocal["error"] & {
_internal: Record<string, string[]>;
};
return error;
};

View File

@ -2,7 +2,7 @@ import { useLocal } from "@/utils/use-local";
import { FMLocal, FieldInternal, FieldProp } from "../typings";
import { useEffect } from "react";
export const createField = (arg: FieldProp) => {
export const useField = (arg: FieldProp) => {
const field = useLocal<FieldInternal>({
status: "init",
name: arg.name,
@ -11,7 +11,9 @@ export const createField = (arg: FieldProp) => {
desc: arg.desc,
prefix: arg.prefix,
suffix: arg.suffix,
label_mode: arg.label_mode,
width: arg.width,
required: arg.required === "y",
required_msg: arg.required_msg,
Child: () => {
return <arg.PassProp>{arg.child}</arg.PassProp>;
},