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 { useLocal } from "@/utils/use-local";
import { FC, useEffect } from "react"; import { FC, useEffect, useRef } from "react";
import { FMInternal, FMProps } from "./typings"; import { FMInternal, FMProps } from "./typings";
import { formReload } from "./utils/reload"; import { formReload } from "./utils/reload";
import { formInit } from "./utils/init"; import { formInit } from "./utils/init";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import get from "lodash.get";
export const Form: FC<FMProps> = (props) => { export const Form: FC<FMProps> = (props) => {
const { PassProp, body } = props; const { PassProp, body } = props;
@ -15,6 +16,9 @@ export const Form: FC<FMProps> = (props) => {
formReload(fm); formReload(fm);
}, },
fields: {}, fields: {},
events: {
on_change(name: string, new_value: any) {},
},
submit: null as any, submit: null as any,
error: {} as any, error: {} as any,
internal: { internal: {
@ -25,6 +29,33 @@ export const Form: FC<FMProps> = (props) => {
}, },
}, },
props: {} as any, 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(() => { useEffect(() => {
@ -41,10 +72,45 @@ export const Form: FC<FMProps> = (props) => {
} }
const toaster_el = document.getElementsByClassName("prasi-toaster")[0]; 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 ( 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)} {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 { FieldProp } from "../typings";
import { createField } from "../utils/create-field"; import { useField } from "../utils/use-field";
import { Label } from "./Label"; import { Label } from "./Label";
export const Field: FC<FieldProp> = (arg) => { 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; if (field.status === "init") return null;
const mode = field.label_mode;
return ( return (
<div <label
className={cx( className={cx(
"field", "field",
mode === "horizontal" && "", "c-flex",
mode === "vertical" && "" 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} />} {mode !== "hidden" && <Label field={field} fm={fm} />}
</div> </label>
); );
}; };

View File

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

View File

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

View File

@ -1,10 +1,27 @@
import { FMLocal } from "../typings"; import { FMLocal } from "../typings";
export const formError = (fm: FMLocal) => { export const formError = (fm: FMLocal) => {
const error = {} as FMLocal["error"]; const error = {
error.list = []; _internal: {},
error.clear = () => {}; get list() {
error.set = () => {}; const res = Object.entries(this._internal).map(([name, error]) => {
error.get = () => {}; 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; return error;
}; };

View File

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