diff --git a/comps/form/Form.tsx b/comps/form/Form.tsx index e29ff82..60ebfd9 100755 --- a/comps/form/Form.tsx +++ b/comps/form/Form.tsx @@ -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 = (props) => { const { PassProp, body } = props; @@ -15,6 +16,9 @@ export const Form: FC = (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 = (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 = (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 ( - <> +
{ + 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_el)} - {fm.status !== "init" && {body}} - +
+ {fm.status !== "init" && + childs.map((child, idx) => { + return ( + + {child} + + ); + })} +
+ ); }; diff --git a/comps/form/field/Field.tsx b/comps/form/field/Field.tsx index 1dc9d11..1bd431d 100755 --- a/comps/form/field/Field.tsx +++ b/comps/form/field/Field.tsx @@ -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 = (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 ( -
- {mode !== "hidden" &&
+ {mode !== "hidden" && ); }; diff --git a/comps/form/field/Input.tsx b/comps/form/field/Input.tsx index eefd824..83231ab 100755 --- a/comps/form/field/Input.tsx +++ b/comps/form/field/Input.tsx @@ -1,3 +1,4 @@ +import { FC } from "react"; import { FMLocal, FieldLocal } from "../typings"; export const FieldInput: FC<{ field: FieldLocal; fm: FMLocal }> = ({ diff --git a/comps/form/field/Label.tsx b/comps/form/field/Label.tsx index cabd843..810cb5f 100755 --- a/comps/form/field/Label.tsx +++ b/comps/form/field/Label.tsx @@ -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
{field.label}
; +export const Label: FC<{ field: FieldLocal; fm: FMLocal }> = ({ + field, + fm, +}) => { + return ( +
+ {field.label} +
+ ); }; diff --git a/comps/form/typings.ts b/comps/form/typings.ts index 6b46cfb..f06f5e2 100755 --- a/comps/form/typings.ts +++ b/comps/form/typings.ts @@ -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; 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; submit: () => Promise; + events: { + on_change: (name: string, new_value: any) => void; + }; fields: Record; 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; + 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 }; diff --git a/comps/form/utils/error.ts b/comps/form/utils/error.ts index 287507a..cfaea25 100755 --- a/comps/form/utils/error.ts +++ b/comps/form/utils/error.ts @@ -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; + }; return error; }; diff --git a/comps/form/utils/create-field.tsx b/comps/form/utils/use-field.tsx similarity index 81% rename from comps/form/utils/create-field.tsx rename to comps/form/utils/use-field.tsx index 05482bd..ca3975a 100755 --- a/comps/form/utils/create-field.tsx +++ b/comps/form/utils/use-field.tsx @@ -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({ 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.child}; },