add form-modal
This commit is contained in:
parent
5a3942f886
commit
e8a71242e0
392
dist/form.js
vendored
Normal file
392
dist/form.js
vendored
Normal file
@ -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
|
||||||
|
};
|
||||||
17
package.json
17
package.json
@ -10,7 +10,8 @@
|
|||||||
"./pagination": "./dist/pagination.js",
|
"./pagination": "./dist/pagination.js",
|
||||||
"./layout": "./dist/layout.js",
|
"./layout": "./dist/layout.js",
|
||||||
"./core": "./dist/core.js",
|
"./core": "./dist/core.js",
|
||||||
"./table": "./dist/table.js"
|
"./table": "./dist/table.js",
|
||||||
|
"./form": "./dist/form.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
@ -29,7 +30,9 @@
|
|||||||
"keycloak-js": "^26.0.0",
|
"keycloak-js": "^26.0.0",
|
||||||
"react": "^18.0.0 || ^19.0.0",
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
"react-dom": "^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": {
|
"peerDependenciesMeta": {
|
||||||
"@chakra-ui/react": {
|
"@chakra-ui/react": {
|
||||||
@ -64,6 +67,12 @@
|
|||||||
},
|
},
|
||||||
"react-icons": {
|
"react-icons": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-hook-form": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"chakra-react-select": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -79,7 +88,9 @@
|
|||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-icons": "^5.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": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|||||||
574
src/form/FormModal.jsx
Normal file
574
src/form/FormModal.jsx
Normal file
@ -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 (
|
||||||
|
<FormControl isInvalid={Boolean(errors[name])}>
|
||||||
|
{label && (
|
||||||
|
<FormLabel fontSize="sm" fontWeight="bold">
|
||||||
|
{label}
|
||||||
|
</FormLabel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<FormErrorMessage>{errors[name]?.message}</FormErrorMessage>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- fields -----
|
||||||
|
|
||||||
|
function InputField({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
validate,
|
||||||
|
rules,
|
||||||
|
showWhen,
|
||||||
|
placeholder,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const { register } = useFormModalContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWrapper name={name} label={label} showWhen={showWhen}>
|
||||||
|
<Input
|
||||||
|
borderRadius="xl"
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...register(name, getRules({ required, validate, rules }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FieldWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextareaField({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
validate,
|
||||||
|
rules,
|
||||||
|
showWhen,
|
||||||
|
placeholder,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const { register } = useFormModalContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWrapper name={name} label={label} showWhen={showWhen}>
|
||||||
|
<Textarea
|
||||||
|
borderRadius="xl"
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...register(name, getRules({ required, validate, rules }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FieldWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectField({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
validate,
|
||||||
|
rules,
|
||||||
|
showWhen,
|
||||||
|
placeholder = "انتخاب کنید",
|
||||||
|
options = [],
|
||||||
|
onChange,
|
||||||
|
isClearable = true,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const { control, setValue } = useFormModalContext();
|
||||||
|
const normalizedOptions = normalizeOptions(options);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWrapper name={name} label={label} showWhen={showWhen}>
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
rules={getRules({ required, validate, rules })}
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedOption =
|
||||||
|
normalizedOptions.find(
|
||||||
|
(option) => String(option.value) === String(field.value),
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChakraReactSelect
|
||||||
|
{...props}
|
||||||
|
value={selectedOption}
|
||||||
|
options={normalizedOptions}
|
||||||
|
placeholder={placeholder}
|
||||||
|
isClearable={isClearable}
|
||||||
|
onChange={(selected) => {
|
||||||
|
const value = selected?.value ?? "";
|
||||||
|
|
||||||
|
field.onChange(value);
|
||||||
|
|
||||||
|
onChange?.({
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FieldWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioField({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
validate,
|
||||||
|
rules,
|
||||||
|
showWhen,
|
||||||
|
options = [],
|
||||||
|
onChange,
|
||||||
|
}) {
|
||||||
|
const { control, setValue } = useFormModalContext();
|
||||||
|
const normalizedOptions = normalizeOptions(options);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWrapper name={name} label={label} showWhen={showWhen}>
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
rules={getRules({ required, validate, rules })}
|
||||||
|
render={({ field }) => (
|
||||||
|
<RadioGroup
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
|
||||||
|
onChange?.({
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack pt="8px" spacing={3}>
|
||||||
|
{normalizedOptions.map((option) => (
|
||||||
|
<Radio key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Radio>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FieldWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckField({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
validate,
|
||||||
|
rules,
|
||||||
|
showWhen,
|
||||||
|
children,
|
||||||
|
onChange,
|
||||||
|
}) {
|
||||||
|
const { control, setValue } = useFormModalContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWrapper name={name} label={label} showWhen={showWhen}>
|
||||||
|
<Controller
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
rules={getRules({ required, validate, rules })}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
isChecked={Boolean(field.value)}
|
||||||
|
onChange={(event) => {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
|
||||||
|
field.onChange(checked);
|
||||||
|
|
||||||
|
onChange?.({
|
||||||
|
value: checked,
|
||||||
|
setValue,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children ?? label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FieldWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilepickerField({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
validate,
|
||||||
|
rules,
|
||||||
|
showWhen,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const { register } = useFormModalContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWrapper name={name} label={label} showWhen={showWhen}>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
borderRadius="xl"
|
||||||
|
{...register(name, getRules({ required, validate, rules }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FieldWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DatepickerField({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
validate,
|
||||||
|
rules,
|
||||||
|
showWhen,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const { register } = useFormModalContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldWrapper name={name} label={label} showWhen={showWhen}>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
borderRadius="xl"
|
||||||
|
{...register(name, getRules({ required, validate, rules }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FieldWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Field = {
|
||||||
|
Input: InputField,
|
||||||
|
Select: SelectField,
|
||||||
|
Radio: RadioField,
|
||||||
|
Textarea: TextareaField,
|
||||||
|
Check: CheckField,
|
||||||
|
Filepicker: FilepickerField,
|
||||||
|
Datepicker: DatepickerField,
|
||||||
|
useForm: useFormModalContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----- form modal -----
|
||||||
|
|
||||||
|
export function FormModal({
|
||||||
|
isOpen,
|
||||||
|
close,
|
||||||
|
|
||||||
|
item = null,
|
||||||
|
entityName = "آیتم",
|
||||||
|
|
||||||
|
title,
|
||||||
|
createTitle,
|
||||||
|
editTitle,
|
||||||
|
|
||||||
|
submitText,
|
||||||
|
createSubmitText,
|
||||||
|
editSubmitText,
|
||||||
|
|
||||||
|
defaultValues = EMPTY_DEFAULT_VALUES,
|
||||||
|
getDefaultValues,
|
||||||
|
|
||||||
|
resetKey,
|
||||||
|
|
||||||
|
onSubmit,
|
||||||
|
transformValues,
|
||||||
|
|
||||||
|
children,
|
||||||
|
|
||||||
|
isLoading = false,
|
||||||
|
size = "lg",
|
||||||
|
columns = { base: 1, md: 2 },
|
||||||
|
}) {
|
||||||
|
const isEdit = Boolean(item);
|
||||||
|
const mode = isEdit ? "edit" : "create";
|
||||||
|
|
||||||
|
// ----- title -----
|
||||||
|
|
||||||
|
const resolvedTitle = useMemo(() => {
|
||||||
|
if (title) return title;
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
return (
|
||||||
|
editTitle ?? `${defaultActionLabels.edit.titlePrefix} ${entityName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
createTitle ?? `${defaultActionLabels.create.titlePrefix} ${entityName}`
|
||||||
|
);
|
||||||
|
}, [title, editTitle, createTitle, entityName, isEdit]);
|
||||||
|
|
||||||
|
// ----- submit text -----
|
||||||
|
|
||||||
|
const resolvedSubmitText = useMemo(() => {
|
||||||
|
if (submitText) return submitText;
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
return editSubmitText ?? defaultActionLabels.edit.submitText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createSubmitText ?? defaultActionLabels.create.submitText;
|
||||||
|
}, [submitText, editSubmitText, createSubmitText, isEdit]);
|
||||||
|
|
||||||
|
// ----- default values -----
|
||||||
|
|
||||||
|
const resolvedDefaultValues = useMemo(() => {
|
||||||
|
if (typeof getDefaultValues === "function") {
|
||||||
|
return getDefaultValues({
|
||||||
|
item,
|
||||||
|
mode,
|
||||||
|
isEdit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValues;
|
||||||
|
}, [getDefaultValues, defaultValues, item, mode, isEdit]);
|
||||||
|
|
||||||
|
const latestDefaultValuesRef = useRef(resolvedDefaultValues);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestDefaultValuesRef.current = resolvedDefaultValues;
|
||||||
|
}, [resolvedDefaultValues]);
|
||||||
|
|
||||||
|
// ----- reset key -----
|
||||||
|
|
||||||
|
const resolvedResetKey = useMemo(() => {
|
||||||
|
if (resetKey) return resetKey;
|
||||||
|
|
||||||
|
return getDefaultResetKey({
|
||||||
|
item,
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
}, [resetKey, item, mode]);
|
||||||
|
|
||||||
|
// ----- form -----
|
||||||
|
|
||||||
|
const methods = useForm({
|
||||||
|
defaultValues: resolvedDefaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting, errors },
|
||||||
|
} = methods;
|
||||||
|
|
||||||
|
// ----- reset on open -----
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
reset(latestDefaultValuesRef.current);
|
||||||
|
}, [isOpen, resolvedResetKey, reset]);
|
||||||
|
|
||||||
|
// ----- close -----
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
reset(latestDefaultValuesRef.current);
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----- submit -----
|
||||||
|
|
||||||
|
const handleFormSubmit = handleSubmit(async (values) => {
|
||||||
|
const finalValues =
|
||||||
|
typeof transformValues === "function"
|
||||||
|
? transformValues({
|
||||||
|
values,
|
||||||
|
item,
|
||||||
|
mode,
|
||||||
|
isEdit,
|
||||||
|
})
|
||||||
|
: values;
|
||||||
|
|
||||||
|
await onSubmit({
|
||||||
|
values: finalValues,
|
||||||
|
item,
|
||||||
|
mode,
|
||||||
|
isEdit,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----- context -----
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
register,
|
||||||
|
control,
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
errors,
|
||||||
|
}),
|
||||||
|
[register, control, setValue, watch, errors],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----- jsx -----
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={handleClose} isCentered size={size}>
|
||||||
|
<ModalOverlay backdropFilter="blur(6px)" />
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
as="form"
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
borderRadius="2xl"
|
||||||
|
overflow="visible"
|
||||||
|
>
|
||||||
|
<ModalHeader
|
||||||
|
bgGradient="linear(to-l, blue.600, facebook.500)"
|
||||||
|
color="white"
|
||||||
|
fontSize="md"
|
||||||
|
fontWeight="black"
|
||||||
|
>
|
||||||
|
{resolvedTitle}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalCloseButton color="white" />
|
||||||
|
|
||||||
|
<ModalBody py={5}>
|
||||||
|
<FormModalContext.Provider value={contextValue}>
|
||||||
|
<VStack spacing={4} alignItems="stretch">
|
||||||
|
<SimpleGrid columns={columns} spacing={4}>
|
||||||
|
{children}
|
||||||
|
</SimpleGrid>
|
||||||
|
</VStack>
|
||||||
|
</FormModalContext.Provider>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter gap={2} bg="gray.50">
|
||||||
|
<Button type="button" variant="ghost" onClick={handleClose}>
|
||||||
|
انصراف
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorScheme="blue"
|
||||||
|
borderRadius="xl"
|
||||||
|
isLoading={isSubmitting || isLoading}
|
||||||
|
>
|
||||||
|
{resolvedSubmitText}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
src/form/index.js
Normal file
2
src/form/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { FormModal, Field } from "./FormModal.jsx";
|
||||||
|
export { useFormModal } from "./useFormModal.jsx";
|
||||||
40
src/form/useFormModal.jsx
Normal file
40
src/form/useFormModal.jsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { useDisclosure } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
export function useFormModal() {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [item, setItem] = useState(null);
|
||||||
|
|
||||||
|
const openCreate = useCallback(() => {
|
||||||
|
setItem(null);
|
||||||
|
onOpen();
|
||||||
|
}, [onOpen]);
|
||||||
|
|
||||||
|
const openEdit = useCallback(
|
||||||
|
(selectedItem) => {
|
||||||
|
setItem(selectedItem);
|
||||||
|
onOpen();
|
||||||
|
},
|
||||||
|
[onOpen],
|
||||||
|
);
|
||||||
|
|
||||||
|
const close = useCallback(() => {
|
||||||
|
setItem(null);
|
||||||
|
onClose();
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const isEdit = Boolean(item);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
isOpen,
|
||||||
|
item,
|
||||||
|
isEdit,
|
||||||
|
isCreate: !isEdit,
|
||||||
|
openCreate,
|
||||||
|
openEdit,
|
||||||
|
close,
|
||||||
|
}),
|
||||||
|
[isOpen, item, isEdit, openCreate, openEdit, close],
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -19,6 +19,9 @@ const externalPackages = [
|
|||||||
"@tanstack/react-table",
|
"@tanstack/react-table",
|
||||||
|
|
||||||
"react-icons",
|
"react-icons",
|
||||||
|
|
||||||
|
"react-hook-form",
|
||||||
|
"chakra-react-select",
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -33,6 +36,7 @@ export default defineConfig({
|
|||||||
layout: path.resolve(process.cwd(), "src/layout/index.js"),
|
layout: path.resolve(process.cwd(), "src/layout/index.js"),
|
||||||
core: path.resolve(process.cwd(), "src/core/index.js"),
|
core: path.resolve(process.cwd(), "src/core/index.js"),
|
||||||
table: path.resolve(process.cwd(), "src/table/index.js"),
|
table: path.resolve(process.cwd(), "src/table/index.js"),
|
||||||
|
form: path.resolve(process.cwd(), "src/form/index.js"),
|
||||||
},
|
},
|
||||||
formats: ["es"],
|
formats: ["es"],
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user