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",
|
||||
"./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": {}
|
||||
}
|
||||
|
||||
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",
|
||||
|
||||
"react-icons",
|
||||
|
||||
"react-hook-form",
|
||||
"chakra-react-select",
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
@ -33,6 +36,7 @@ export default defineConfig({
|
||||
layout: path.resolve(process.cwd(), "src/layout/index.js"),
|
||||
core: path.resolve(process.cwd(), "src/core/index.js"),
|
||||
table: path.resolve(process.cwd(), "src/table/index.js"),
|
||||
form: path.resolve(process.cwd(), "src/form/index.js"),
|
||||
},
|
||||
formats: ["es"],
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user