From e8a71242e0c2527d20c6b12fc69976c94da40ac7 Mon Sep 17 00:00:00 2001 From: Mohamadzadeh <7796733@gmail.com> Date: Wed, 17 Jun 2026 13:26:04 +0330 Subject: [PATCH] add form-modal --- dist/form.js | 392 ++++++++++++++++++++++++++ package.json | 17 +- src/form/FormModal.jsx | 574 ++++++++++++++++++++++++++++++++++++++ src/form/index.js | 2 + src/form/useFormModal.jsx | 40 +++ vite.config.js | 4 + 6 files changed, 1026 insertions(+), 3 deletions(-) create mode 100644 dist/form.js create mode 100644 src/form/FormModal.jsx create mode 100644 src/form/index.js create mode 100644 src/form/useFormModal.jsx diff --git a/dist/form.js b/dist/form.js new file mode 100644 index 0000000..5642d36 --- /dev/null +++ b/dist/form.js @@ -0,0 +1,392 @@ +import { jsx as n, jsxs as y } from "react/jsx-runtime"; +import { Input as P, Checkbox as ee, Textarea as re, RadioGroup as te, HStack as ne, Radio as oe, Modal as le, ModalOverlay as ie, ModalContent as ce, ModalHeader as ue, ModalCloseButton as se, ModalBody as ae, VStack as de, SimpleGrid as fe, ModalFooter as pe, Button as G, FormControl as be, FormLabel as me, FormErrorMessage as ve, useDisclosure as xe } from "@chakra-ui/react"; +import { createContext as Fe, useContext as ge, useMemo as x, useRef as he, useEffect as H, useState as Se, useCallback as I } from "react"; +import { Controller as z, useForm as ke, useWatch as Re } from "react-hook-form"; +import { Select as Me } from "chakra-react-select"; +const Ce = {}, C = { + create: { + titlePrefix: "افزودن", + submitText: "ثبت" + }, + edit: { + titlePrefix: "ویرایش", + submitText: "ویرایش" + } +}, U = Fe(null); +function ye({ item: e, mode: r }) { + return e ? [r, e.rowId, e.entityId, e.id].filter(Boolean).join("-") : r; +} +function F({ required: e, validate: r, rules: t }) { + const o = { ...t ?? {} }; + return e && (o.required = typeof e == "string" ? e : "این فیلد الزامی است."), r && (o.validate = r), o; +} +function m() { + const e = ge(U); + if (!e) + throw new Error("Field باید داخل FormModal استفاده شود."); + return e; +} +function _(e = []) { + return e.map((r) => ({ + label: r.label ?? r.name, + value: String(r.value ?? r.id) + })); +} +function g({ name: e, label: r, showWhen: t, children: o }) { + var s; + const { errors: i, control: u } = m(), c = Re({ + control: u, + name: t == null ? void 0 : t.name + }); + return x(() => t ? typeof t == "function" ? t({ + value: c + }) : c === t.value : !0, [t, c]) ? /* @__PURE__ */ y(be, { isInvalid: !!i[e], children: [ + r && /* @__PURE__ */ n(me, { fontSize: "sm", fontWeight: "bold", children: r }), + o, + /* @__PURE__ */ n(ve, { children: (s = i[e]) == null ? void 0 : s.message }) + ] }) : null; +} +function Ve({ + name: e, + label: r, + required: t, + validate: o, + rules: i, + showWhen: u, + placeholder: c, + ...l +}) { + const { register: s } = m(); + return /* @__PURE__ */ n(g, { name: e, label: r, showWhen: u, children: /* @__PURE__ */ n( + P, + { + borderRadius: "xl", + placeholder: c, + ...s(e, F({ required: t, validate: o, rules: i })), + ...l + } + ) }); +} +function Ee({ + name: e, + label: r, + required: t, + validate: o, + rules: i, + showWhen: u, + placeholder: c, + ...l +}) { + const { register: s } = m(); + return /* @__PURE__ */ n(g, { name: e, label: r, showWhen: u, children: /* @__PURE__ */ n( + re, + { + borderRadius: "xl", + placeholder: c, + ...s(e, F({ required: t, validate: o, rules: i })), + ...l + } + ) }); +} +function Be({ + name: e, + label: r, + required: t, + validate: o, + rules: i, + showWhen: u, + placeholder: c = "انتخاب کنید", + options: l = [], + onChange: s, + isClearable: d = !0, + ...p +}) { + const { control: f, setValue: a } = m(), k = _(l); + return /* @__PURE__ */ n(g, { name: e, label: r, showWhen: u, children: /* @__PURE__ */ n( + z, + { + name: e, + control: f, + rules: F({ required: t, validate: o, rules: i }), + render: ({ field: S }) => { + const V = k.find( + (v) => String(v.value) === String(S.value) + ) ?? null; + return /* @__PURE__ */ n( + Me, + { + ...p, + value: V, + options: k, + placeholder: c, + isClearable: d, + onChange: (v) => { + const R = (v == null ? void 0 : v.value) ?? ""; + S.onChange(R), s == null || s({ + value: R, + setValue: a + }); + } + } + ); + } + } + ) }); +} +function Ie({ + name: e, + label: r, + required: t, + validate: o, + rules: i, + showWhen: u, + options: c = [], + onChange: l +}) { + const { control: s, setValue: d } = m(), p = _(c); + return /* @__PURE__ */ n(g, { name: e, label: r, showWhen: u, children: /* @__PURE__ */ n( + z, + { + name: e, + control: s, + rules: F({ required: t, validate: o, rules: i }), + render: ({ field: f }) => /* @__PURE__ */ n( + te, + { + value: f.value, + onChange: (a) => { + f.onChange(a), l == null || l({ + value: a, + setValue: d + }); + }, + children: /* @__PURE__ */ n(ne, { pt: "8px", spacing: 3, children: p.map((a) => /* @__PURE__ */ n(oe, { value: a.value, children: a.label }, a.value)) }) + } + ) + } + ) }); +} +function Pe({ + name: e, + label: r, + required: t, + validate: o, + rules: i, + showWhen: u, + children: c, + onChange: l +}) { + const { control: s, setValue: d } = m(); + return /* @__PURE__ */ n(g, { name: e, label: r, showWhen: u, children: /* @__PURE__ */ n( + z, + { + name: e, + control: s, + rules: F({ required: t, validate: o, rules: i }), + render: ({ field: p }) => /* @__PURE__ */ n( + ee, + { + isChecked: !!p.value, + onChange: (f) => { + const a = f.target.checked; + p.onChange(a), l == null || l({ + value: a, + setValue: d + }); + }, + children: c ?? r + } + ) + } + ) }); +} +function ze({ + name: e, + label: r, + required: t, + validate: o, + rules: i, + showWhen: u, + ...c +}) { + const { register: l } = m(); + return /* @__PURE__ */ n(g, { name: e, label: r, showWhen: u, children: /* @__PURE__ */ n( + P, + { + type: "file", + borderRadius: "xl", + ...l(e, F({ required: t, validate: o, rules: i })), + ...c + } + ) }); +} +function De({ + name: e, + label: r, + required: t, + validate: o, + rules: i, + showWhen: u, + ...c +}) { + const { register: l } = m(); + return /* @__PURE__ */ n(g, { name: e, label: r, showWhen: u, children: /* @__PURE__ */ n( + P, + { + type: "date", + borderRadius: "xl", + ...l(e, F({ required: t, validate: o, rules: i })), + ...c + } + ) }); +} +const Ae = { + Input: Ve, + Select: Be, + Radio: Ie, + Textarea: Ee, + Check: Pe, + Filepicker: ze, + Datepicker: De, + useForm: m +}; +function Ge({ + isOpen: e, + close: r, + item: t = null, + entityName: o = "آیتم", + title: i, + createTitle: u, + editTitle: c, + submitText: l, + createSubmitText: s, + editSubmitText: d, + defaultValues: p = Ce, + getDefaultValues: f, + resetKey: a, + onSubmit: k, + transformValues: S, + children: V, + isLoading: v = !1, + size: R = "lg", + columns: Y = { base: 1, md: 2 } +}) { + const b = !!t, h = b ? "edit" : "create", w = x(() => i || (b ? c ?? `${C.edit.titlePrefix} ${o}` : u ?? `${C.create.titlePrefix} ${o}`), [i, c, u, o, b]), J = x(() => l || (b ? d ?? C.edit.submitText : s ?? C.create.submitText), [l, d, s, b]), M = x(() => typeof f == "function" ? f({ + item: t, + mode: h, + isEdit: b + }) : p, [f, p, t, h, b]), E = he(M); + H(() => { + E.current = M; + }, [M]); + const K = x(() => a || ye({ + item: t, + mode: h + }), [a, t, h]), Q = ke({ + defaultValues: M + }), { + handleSubmit: X, + reset: B, + register: D, + control: L, + setValue: O, + watch: T, + formState: { isSubmitting: Z, errors: $ } + } = Q; + H(() => { + e && B(E.current); + }, [e, K, B]); + const j = () => { + B(E.current), r(); + }, q = X(async (A) => { + const W = typeof S == "function" ? S({ + values: A, + item: t, + mode: h, + isEdit: b + }) : A; + await k({ + values: W, + item: t, + mode: h, + isEdit: b + }); + }), N = x( + () => ({ + register: D, + control: L, + setValue: O, + watch: T, + errors: $ + }), + [D, L, O, T, $] + ); + return /* @__PURE__ */ y(le, { isOpen: e, onClose: j, isCentered: !0, size: R, children: [ + /* @__PURE__ */ n(ie, { backdropFilter: "blur(6px)" }), + /* @__PURE__ */ y( + ce, + { + as: "form", + onSubmit: q, + borderRadius: "2xl", + overflow: "visible", + children: [ + /* @__PURE__ */ n( + ue, + { + bgGradient: "linear(to-l, blue.600, facebook.500)", + color: "white", + fontSize: "md", + fontWeight: "black", + children: w + } + ), + /* @__PURE__ */ n(se, { color: "white" }), + /* @__PURE__ */ n(ae, { py: 5, children: /* @__PURE__ */ n(U.Provider, { value: N, children: /* @__PURE__ */ n(de, { spacing: 4, alignItems: "stretch", children: /* @__PURE__ */ n(fe, { columns: Y, spacing: 4, children: V }) }) }) }), + /* @__PURE__ */ y(pe, { gap: 2, bg: "gray.50", children: [ + /* @__PURE__ */ n(G, { type: "button", variant: "ghost", onClick: j, children: "انصراف" }), + /* @__PURE__ */ n( + G, + { + type: "submit", + colorScheme: "blue", + borderRadius: "xl", + isLoading: Z || v, + children: J + } + ) + ] }) + ] + } + ) + ] }); +} +function He() { + const { isOpen: e, onOpen: r, onClose: t } = xe(), [o, i] = Se(null), u = I(() => { + i(null), r(); + }, [r]), c = I( + (d) => { + i(d), r(); + }, + [r] + ), l = I(() => { + i(null), t(); + }, [t]), s = !!o; + return x( + () => ({ + isOpen: e, + item: o, + isEdit: s, + isCreate: !s, + openCreate: u, + openEdit: c, + close: l + }), + [e, o, s, u, c, l] + ); +} +export { + Ae as Field, + Ge as FormModal, + He as useFormModal +}; diff --git a/package.json b/package.json index 5f7afea..09ce615 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "./pagination": "./dist/pagination.js", "./layout": "./dist/layout.js", "./core": "./dist/core.js", - "./table": "./dist/table.js" + "./table": "./dist/table.js", + "./form": "./dist/form.js" }, "files": [ "dist" @@ -29,7 +30,9 @@ "keycloak-js": "^26.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", - "react-icons": "^5.0.0" + "react-icons": "^5.0.0", + "react-hook-form": "^7.0.0", + "chakra-react-select": "^5.0.0" }, "peerDependenciesMeta": { "@chakra-ui/react": { @@ -64,6 +67,12 @@ }, "react-icons": { "optional": true + }, + "react-hook-form": { + "optional": true + }, + "chakra-react-select": { + "optional": true } }, "devDependencies": { @@ -79,7 +88,9 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-icons": "^5.0.0", - "vite": "5.4.19" + "vite": "5.4.19", + "react-hook-form": "^7.0.0", + "chakra-react-select": "^5.0.0" }, "dependencies": {} } diff --git a/src/form/FormModal.jsx b/src/form/FormModal.jsx new file mode 100644 index 0000000..3bdd63e --- /dev/null +++ b/src/form/FormModal.jsx @@ -0,0 +1,574 @@ +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + HStack, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Radio, + RadioGroup, + Select, + SimpleGrid, + Textarea, + VStack, +} from "@chakra-ui/react"; +import { createContext, useContext, useEffect, useMemo, useRef } from "react"; +import { Controller, useForm, useWatch } from "react-hook-form"; +import { Select as ChakraReactSelect } from "chakra-react-select"; + +const EMPTY_DEFAULT_VALUES = {}; + +const defaultActionLabels = { + create: { + titlePrefix: "افزودن", + submitText: "ثبت", + }, + edit: { + titlePrefix: "ویرایش", + submitText: "ویرایش", + }, +}; + +const FormModalContext = createContext(null); + +// ----- utils ----- + +function getDefaultResetKey({ item, mode }) { + if (!item) return mode; + + return [mode, item.rowId, item.entityId, item.id].filter(Boolean).join("-"); +} + +function getRules({ required, validate, rules }) { + const finalRules = { ...(rules ?? {}) }; + + if (required) { + finalRules.required = + typeof required === "string" ? required : "این فیلد الزامی است."; + } + + if (validate) { + finalRules.validate = validate; + } + + return finalRules; +} + +function useFormModalContext() { + const context = useContext(FormModalContext); + + if (!context) { + throw new Error("Field باید داخل FormModal استفاده شود."); + } + + return context; +} + +function normalizeOptions(options = []) { + return options.map((option) => ({ + label: option.label ?? option.name, + value: String(option.value ?? option.id), + })); +} + +// ----- field wrapper ----- + +function FieldWrapper({ name, label, showWhen, children }) { + const { errors, control } = useFormModalContext(); + + const watchedValue = useWatch({ + control, + name: showWhen?.name, + }); + + const isVisible = useMemo(() => { + if (!showWhen) return true; + + if (typeof showWhen === "function") { + return showWhen({ + value: watchedValue, + }); + } + + return watchedValue === showWhen.value; + }, [showWhen, watchedValue]); + + if (!isVisible) return null; + + return ( + + {label && ( + + {label} + + )} + + {children} + + {errors[name]?.message} + + ); +} + +// ----- fields ----- + +function InputField({ + name, + label, + required, + validate, + rules, + showWhen, + placeholder, + ...props +}) { + const { register } = useFormModalContext(); + + return ( + + + + ); +} + +function TextareaField({ + name, + label, + required, + validate, + rules, + showWhen, + placeholder, + ...props +}) { + const { register } = useFormModalContext(); + + return ( + +