@refinedev/react-hook-form 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/iife/index.js +56 -0
- package/dist/iife/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/useForm/index.d.ts +24 -0
- package/dist/useForm/index.d.ts.map +1 -0
- package/dist/useModalForm/index.d.ts +35 -0
- package/dist/useModalForm/index.d.ts.map +1 -0
- package/dist/useStepsForm/index.d.ts +28 -0
- package/dist/useStepsForm/index.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/index.ts +11 -0
- package/src/useForm/index.ts +141 -0
- package/src/useModalForm/index.spec.ts +208 -0
- package/src/useModalForm/index.ts +241 -0
- package/src/useStepsForm/index.spec.ts +80 -0
- package/src/useStepsForm/index.ts +124 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
BaseKey,
|
|
4
|
+
BaseRecord,
|
|
5
|
+
FormWithSyncWithLocationParams,
|
|
6
|
+
HttpError,
|
|
7
|
+
useGo,
|
|
8
|
+
useModal,
|
|
9
|
+
useParsed,
|
|
10
|
+
useResource,
|
|
11
|
+
userFriendlyResourceName,
|
|
12
|
+
useTranslate,
|
|
13
|
+
useWarnAboutChange,
|
|
14
|
+
} from "@refinedev/core";
|
|
15
|
+
import { FieldValues } from "react-hook-form";
|
|
16
|
+
|
|
17
|
+
import { useForm, UseFormProps, UseFormReturnType } from "../useForm";
|
|
18
|
+
import React from "react";
|
|
19
|
+
|
|
20
|
+
export type UseModalFormReturnType<
|
|
21
|
+
TData extends BaseRecord = BaseRecord,
|
|
22
|
+
TError extends HttpError = HttpError,
|
|
23
|
+
TVariables extends FieldValues = FieldValues,
|
|
24
|
+
TContext extends object = {},
|
|
25
|
+
> = UseFormReturnType<TData, TError, TVariables, TContext> & {
|
|
26
|
+
modal: {
|
|
27
|
+
submit: (values: TVariables) => void;
|
|
28
|
+
close: () => void;
|
|
29
|
+
show: (id?: BaseKey) => void;
|
|
30
|
+
visible: boolean;
|
|
31
|
+
title: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type UseModalFormProps<
|
|
36
|
+
TData extends BaseRecord = BaseRecord,
|
|
37
|
+
TError extends HttpError = HttpError,
|
|
38
|
+
TVariables extends FieldValues = FieldValues,
|
|
39
|
+
TContext extends object = {},
|
|
40
|
+
> = UseFormProps<TData, TError, TVariables, TContext> & {
|
|
41
|
+
/**
|
|
42
|
+
* @description Configuration object for the modal.
|
|
43
|
+
* `defaultVisible`: Initial visibility state of the modal.
|
|
44
|
+
*
|
|
45
|
+
* `autoSubmitClose`: Whether the form should be submitted when the modal is closed.
|
|
46
|
+
*
|
|
47
|
+
* `autoResetForm`: Whether the form should be reset when the form is submitted.
|
|
48
|
+
* @type `{
|
|
49
|
+
defaultVisible?: boolean;
|
|
50
|
+
autoSubmitClose?: boolean;
|
|
51
|
+
autoResetForm?: boolean;
|
|
52
|
+
}`
|
|
53
|
+
* @default `defaultVisible = false` `autoSubmitClose = true` `autoResetForm = true`
|
|
54
|
+
*/
|
|
55
|
+
modalProps?: {
|
|
56
|
+
defaultVisible?: boolean;
|
|
57
|
+
autoSubmitClose?: boolean;
|
|
58
|
+
autoResetForm?: boolean;
|
|
59
|
+
};
|
|
60
|
+
} & FormWithSyncWithLocationParams;
|
|
61
|
+
|
|
62
|
+
export const useModalForm = <
|
|
63
|
+
TData extends BaseRecord = BaseRecord,
|
|
64
|
+
TError extends HttpError = HttpError,
|
|
65
|
+
TVariables extends FieldValues = FieldValues,
|
|
66
|
+
TContext extends object = {},
|
|
67
|
+
>({
|
|
68
|
+
modalProps,
|
|
69
|
+
refineCoreProps,
|
|
70
|
+
syncWithLocation,
|
|
71
|
+
...rest
|
|
72
|
+
}: UseModalFormProps<
|
|
73
|
+
TData,
|
|
74
|
+
TError,
|
|
75
|
+
TVariables,
|
|
76
|
+
TContext
|
|
77
|
+
> = {}): UseModalFormReturnType<TData, TError, TVariables, TContext> => {
|
|
78
|
+
const initiallySynced = React.useRef(false);
|
|
79
|
+
|
|
80
|
+
const translate = useTranslate();
|
|
81
|
+
|
|
82
|
+
const { resource: resourceProp, action: actionProp } =
|
|
83
|
+
refineCoreProps ?? {};
|
|
84
|
+
|
|
85
|
+
const { resource, action: actionFromParams } = useResource(resourceProp);
|
|
86
|
+
|
|
87
|
+
const parsed = useParsed();
|
|
88
|
+
const go = useGo();
|
|
89
|
+
|
|
90
|
+
const action = actionProp ?? actionFromParams ?? "";
|
|
91
|
+
|
|
92
|
+
const syncingId =
|
|
93
|
+
typeof syncWithLocation === "object" && syncWithLocation.syncId;
|
|
94
|
+
|
|
95
|
+
const syncWithLocationKey =
|
|
96
|
+
typeof syncWithLocation === "object" && "key" in syncWithLocation
|
|
97
|
+
? syncWithLocation.key
|
|
98
|
+
: resource && action && syncWithLocation
|
|
99
|
+
? `modal-${resource?.identifier ?? resource?.name}-${action}`
|
|
100
|
+
: undefined;
|
|
101
|
+
|
|
102
|
+
const {
|
|
103
|
+
defaultVisible = false,
|
|
104
|
+
autoSubmitClose = true,
|
|
105
|
+
autoResetForm = true,
|
|
106
|
+
} = modalProps ?? {};
|
|
107
|
+
|
|
108
|
+
const useHookFormResult = useForm<TData, TError, TVariables, TContext>({
|
|
109
|
+
refineCoreProps,
|
|
110
|
+
...rest,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const {
|
|
114
|
+
reset,
|
|
115
|
+
refineCore: { onFinish, id, setId },
|
|
116
|
+
saveButtonProps,
|
|
117
|
+
handleSubmit,
|
|
118
|
+
} = useHookFormResult;
|
|
119
|
+
|
|
120
|
+
const { visible, show, close } = useModal({
|
|
121
|
+
defaultVisible,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
React.useEffect(() => {
|
|
125
|
+
if (initiallySynced.current === false && syncWithLocationKey) {
|
|
126
|
+
const openStatus = parsed?.params?.[syncWithLocationKey]?.open;
|
|
127
|
+
if (typeof openStatus === "boolean") {
|
|
128
|
+
if (openStatus) {
|
|
129
|
+
show();
|
|
130
|
+
}
|
|
131
|
+
} else if (typeof openStatus === "string") {
|
|
132
|
+
if (openStatus === "true") {
|
|
133
|
+
show();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (syncingId) {
|
|
138
|
+
const idFromParams = parsed?.params?.[syncWithLocationKey]?.id;
|
|
139
|
+
if (idFromParams) {
|
|
140
|
+
setId?.(idFromParams);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
initiallySynced.current = true;
|
|
145
|
+
}
|
|
146
|
+
}, [syncWithLocationKey, parsed, syncingId, setId]);
|
|
147
|
+
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
if (initiallySynced.current === true) {
|
|
150
|
+
if (visible && syncWithLocationKey) {
|
|
151
|
+
go({
|
|
152
|
+
query: {
|
|
153
|
+
[syncWithLocationKey]: {
|
|
154
|
+
...parsed?.params?.[syncWithLocationKey],
|
|
155
|
+
open: true,
|
|
156
|
+
...(syncingId && id && { id }),
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
options: { keepQuery: true },
|
|
160
|
+
type: "replace",
|
|
161
|
+
});
|
|
162
|
+
} else if (syncWithLocationKey && !visible) {
|
|
163
|
+
go({
|
|
164
|
+
query: {
|
|
165
|
+
[syncWithLocationKey]: undefined,
|
|
166
|
+
},
|
|
167
|
+
options: { keepQuery: true },
|
|
168
|
+
type: "replace",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}, [id, visible, show, syncWithLocationKey, syncingId]);
|
|
173
|
+
|
|
174
|
+
const submit = async (values: TVariables) => {
|
|
175
|
+
await onFinish(values);
|
|
176
|
+
|
|
177
|
+
if (autoSubmitClose) {
|
|
178
|
+
close();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (autoResetForm) {
|
|
182
|
+
reset();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const { warnWhen, setWarnWhen } = useWarnAboutChange();
|
|
187
|
+
const handleClose = useCallback(() => {
|
|
188
|
+
if (warnWhen) {
|
|
189
|
+
const warnWhenConfirm = window.confirm(
|
|
190
|
+
translate(
|
|
191
|
+
"warnWhenUnsavedChanges",
|
|
192
|
+
"Are you sure you want to leave? You have unsaved changes.",
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (warnWhenConfirm) {
|
|
197
|
+
setWarnWhen(false);
|
|
198
|
+
} else {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
setId?.(undefined);
|
|
204
|
+
close();
|
|
205
|
+
}, [warnWhen]);
|
|
206
|
+
|
|
207
|
+
const handleShow = useCallback((id?: BaseKey) => {
|
|
208
|
+
setId?.(id);
|
|
209
|
+
|
|
210
|
+
show();
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const title = translate(
|
|
214
|
+
`${resource?.name}.titles.${actionProp}`,
|
|
215
|
+
undefined,
|
|
216
|
+
`${userFriendlyResourceName(
|
|
217
|
+
`${actionProp} ${
|
|
218
|
+
resource?.meta?.label ??
|
|
219
|
+
resource?.options?.label ??
|
|
220
|
+
resource?.label ??
|
|
221
|
+
resource?.name
|
|
222
|
+
}`,
|
|
223
|
+
"singular",
|
|
224
|
+
)}`,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
modal: {
|
|
229
|
+
submit,
|
|
230
|
+
close: handleClose,
|
|
231
|
+
show: handleShow,
|
|
232
|
+
visible,
|
|
233
|
+
title,
|
|
234
|
+
},
|
|
235
|
+
...useHookFormResult,
|
|
236
|
+
saveButtonProps: {
|
|
237
|
+
...saveButtonProps,
|
|
238
|
+
onClick: (e) => handleSubmit(submit)(e),
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { act } from "react-dom/test-utils";
|
|
3
|
+
|
|
4
|
+
import { TestWrapper } from "../../test";
|
|
5
|
+
|
|
6
|
+
import { useStepsForm } from "./";
|
|
7
|
+
|
|
8
|
+
describe("useStepsForm Hook", () => {
|
|
9
|
+
it("'defaultStep' props should set the initial value of 'currentStep'", async () => {
|
|
10
|
+
const { result } = renderHook(
|
|
11
|
+
() =>
|
|
12
|
+
useStepsForm({
|
|
13
|
+
stepsProps: {
|
|
14
|
+
defaultStep: 4,
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
{
|
|
18
|
+
wrapper: TestWrapper({}),
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(result.current.steps.currentStep).toBe(4);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("'goToStep' should update the current step state", async () => {
|
|
26
|
+
const { result } = renderHook(() => useStepsForm({}), {
|
|
27
|
+
wrapper: TestWrapper({}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await act(async () => {
|
|
31
|
+
result.current.steps.gotoStep(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(result.current.steps.currentStep).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("'currentStep' should be 0 when the 'goToSteps' params less than zero", async () => {
|
|
38
|
+
const { result } = renderHook(() => useStepsForm({}), {
|
|
39
|
+
wrapper: TestWrapper({}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await act(async () => {
|
|
43
|
+
result.current.steps.gotoStep(-7);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result.current.steps.currentStep).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("'currentStep' should be 0 when the 'goToSteps' params less than zero", async () => {
|
|
50
|
+
const { result } = renderHook(() => useStepsForm({}), {
|
|
51
|
+
wrapper: TestWrapper({}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await act(async () => {
|
|
55
|
+
result.current.steps.gotoStep(-7);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result.current.steps.currentStep).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("'currentStep' should not update when the 'goToSteps' params equal to the 'currentStep'", async () => {
|
|
62
|
+
const { result } = renderHook(
|
|
63
|
+
() =>
|
|
64
|
+
useStepsForm({
|
|
65
|
+
stepsProps: {
|
|
66
|
+
defaultStep: 2,
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
{
|
|
70
|
+
wrapper: TestWrapper({}),
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
await act(async () => {
|
|
75
|
+
result.current.steps.gotoStep(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.current.steps.currentStep).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { FieldValues } from "react-hook-form";
|
|
3
|
+
import { BaseRecord, HttpError } from "@refinedev/core";
|
|
4
|
+
|
|
5
|
+
import { useForm, UseFormProps, UseFormReturnType } from "../useForm";
|
|
6
|
+
|
|
7
|
+
export type UseStepsFormReturnType<
|
|
8
|
+
TData extends BaseRecord = BaseRecord,
|
|
9
|
+
TError extends HttpError = HttpError,
|
|
10
|
+
TVariables extends FieldValues = FieldValues,
|
|
11
|
+
TContext extends object = {},
|
|
12
|
+
> = UseFormReturnType<TData, TError, TVariables, TContext> & {
|
|
13
|
+
steps: {
|
|
14
|
+
currentStep: number;
|
|
15
|
+
gotoStep: (step: number) => void;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type UseStepsFormProps<
|
|
20
|
+
TData extends BaseRecord = BaseRecord,
|
|
21
|
+
TError extends HttpError = HttpError,
|
|
22
|
+
TVariables extends FieldValues = FieldValues,
|
|
23
|
+
TContext extends object = {},
|
|
24
|
+
> = UseFormProps<TData, TError, TVariables, TContext> & {
|
|
25
|
+
/**
|
|
26
|
+
* @description Configuration object for the steps.
|
|
27
|
+
* `defaultStep`: Allows you to set the initial step.
|
|
28
|
+
*
|
|
29
|
+
* `isBackValidate`: Whether to validation the current step when going back.
|
|
30
|
+
* @type `{
|
|
31
|
+
defaultStep?: number;
|
|
32
|
+
isBackValidate?: boolean;
|
|
33
|
+
}`
|
|
34
|
+
* @default `defaultStep = 0` `isBackValidate = false`
|
|
35
|
+
*/
|
|
36
|
+
stepsProps?: {
|
|
37
|
+
defaultStep?: number;
|
|
38
|
+
isBackValidate?: boolean;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const useStepsForm = <
|
|
43
|
+
TData extends BaseRecord = BaseRecord,
|
|
44
|
+
TError extends HttpError = HttpError,
|
|
45
|
+
TVariables extends FieldValues = FieldValues,
|
|
46
|
+
TContext extends object = {},
|
|
47
|
+
>({
|
|
48
|
+
stepsProps,
|
|
49
|
+
...rest
|
|
50
|
+
}: UseStepsFormProps<
|
|
51
|
+
TData,
|
|
52
|
+
TError,
|
|
53
|
+
TVariables,
|
|
54
|
+
TContext
|
|
55
|
+
> = {}): UseStepsFormReturnType<TData, TError, TVariables, TContext> => {
|
|
56
|
+
const { defaultStep = 0, isBackValidate = false } = stepsProps ?? {};
|
|
57
|
+
const [current, setCurrent] = useState(defaultStep);
|
|
58
|
+
|
|
59
|
+
const useHookFormResult = useForm({
|
|
60
|
+
...rest,
|
|
61
|
+
});
|
|
62
|
+
const {
|
|
63
|
+
trigger,
|
|
64
|
+
getValues,
|
|
65
|
+
reset,
|
|
66
|
+
formState: { dirtyFields },
|
|
67
|
+
refineCore: { queryResult },
|
|
68
|
+
} = useHookFormResult;
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (queryResult?.data) {
|
|
72
|
+
const fields: any = {};
|
|
73
|
+
const registeredFields = Object.keys(getValues());
|
|
74
|
+
Object.entries(queryResult?.data?.data).forEach(([key, value]) => {
|
|
75
|
+
if (registeredFields.includes(key)) {
|
|
76
|
+
if (dirtyFields[key]) {
|
|
77
|
+
fields[key] = getValues(key as any);
|
|
78
|
+
} else {
|
|
79
|
+
fields[key] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
reset(fields as any, {
|
|
85
|
+
keepDirty: true,
|
|
86
|
+
keepValues: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}, [queryResult?.data, current]);
|
|
90
|
+
|
|
91
|
+
const go = (step: number) => {
|
|
92
|
+
let targetStep = step;
|
|
93
|
+
|
|
94
|
+
if (step < 0) {
|
|
95
|
+
targetStep = 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setCurrent(targetStep);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const gotoStep = async (step: number) => {
|
|
102
|
+
if (step === current) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (step < current && !isBackValidate) {
|
|
107
|
+
go(step);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isValid = await trigger();
|
|
112
|
+
if (isValid) {
|
|
113
|
+
go(step);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...useHookFormResult,
|
|
119
|
+
steps: {
|
|
120
|
+
currentStep: current,
|
|
121
|
+
gotoStep,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
};
|