@react-fabric/form 1.0.0-beta.9 → 1.0.2
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/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
- package/css/_colorPicker.css +95 -0
- package/{src/internal/Field.module.css → css/_field.css} +94 -33
- package/{src/input/File.module.css → css/_file.css} +21 -6
- package/css/styles.css +27 -0
- package/dist/cjs/builder/FileField.js +107 -78
- package/dist/cjs/builder/FormField.js +60 -39
- package/dist/cjs/builder/SchemaEditor.js +72 -0
- package/dist/cjs/builder/components/AddSchemaFields.js +64 -0
- package/dist/cjs/builder/components/SchemaArrayForm.js +146 -0
- package/dist/cjs/builder/components/SchemaField.js +423 -0
- package/dist/cjs/form/Controller.js +66 -19
- package/dist/cjs/form/Form.js +97 -39
- package/dist/cjs/form/useForm.js +32 -0
- package/dist/cjs/hooks/useFileUploader.js +74 -45
- package/dist/cjs/hooks/useFormBuilder.js +85 -31
- package/dist/cjs/i18n/{ar.json.js → ar/form.json.js} +5 -4
- package/dist/cjs/i18n/{en.json.js → en/form.json.js} +5 -5
- package/dist/cjs/i18n/index.js +4 -4
- package/dist/cjs/index.js +41 -2
- package/dist/cjs/input/ArrayInput.js +259 -135
- package/dist/cjs/input/AvatarInput.js +99 -63
- package/dist/cjs/input/Checkbox.js +24 -25
- package/dist/cjs/input/ColorInput.js +138 -81
- package/dist/cjs/input/ColorPicker.js +78 -95
- package/dist/cjs/input/DateInput.js +192 -0
- package/dist/cjs/input/Field.js +21 -20
- package/dist/cjs/input/File.js +85 -80
- package/dist/cjs/input/Hidden.js +27 -13
- package/dist/cjs/input/Input.js +49 -48
- package/dist/cjs/input/Number.js +46 -44
- package/dist/cjs/input/Password.js +51 -49
- package/dist/cjs/input/Radio.js +26 -16
- package/dist/cjs/input/RadioGroup.js +26 -11
- package/dist/cjs/input/{Range.js → RangeSlider.js} +61 -66
- package/dist/cjs/input/Search.js +58 -64
- package/dist/cjs/input/Slider.js +57 -66
- package/dist/cjs/input/Switch.js +29 -27
- package/dist/cjs/input/Textarea.js +65 -53
- package/dist/cjs/internal/ErrorIcon.js +9 -9
- package/dist/cjs/internal/FieldWrapper.js +23 -13
- package/dist/cjs/internal/InputWrapper.js +85 -87
- package/dist/cjs/ranger/index.js +7 -7
- package/dist/cjs/ranger/utils.js +9 -4
- package/dist/cjs/select/AutoComplete.js +93 -76
- package/dist/cjs/select/DualList.js +235 -154
- package/dist/cjs/select/List.js +122 -89
- package/dist/cjs/select/Options.js +58 -54
- package/dist/cjs/select/Select.js +119 -89
- package/dist/cjs/select/helpers.js +5 -5
- package/dist/cjs/select/useSelect.js +83 -58
- package/dist/cjs/types/schema.js +43 -0
- package/dist/cjs/utils/eventHandlers.js +16 -6
- package/dist/cjs/utils/getColor.js +6 -6
- package/dist/cjs/utils/getProps.js +49 -0
- package/dist/esm/builder/FileField.js +109 -80
- package/dist/esm/builder/FormField.js +60 -39
- package/dist/esm/builder/SchemaEditor.js +72 -0
- package/dist/esm/builder/components/AddSchemaFields.js +64 -0
- package/dist/esm/builder/components/SchemaArrayForm.js +146 -0
- package/dist/esm/builder/components/SchemaField.js +423 -0
- package/dist/esm/form/Controller.js +66 -19
- package/dist/esm/form/Form.js +97 -39
- package/dist/esm/form/useForm.js +32 -0
- package/dist/esm/hooks/useFileUploader.js +75 -46
- package/dist/esm/hooks/useFormBuilder.js +84 -30
- package/dist/esm/i18n/{ar.json.js → ar/form.json.js} +5 -4
- package/dist/esm/i18n/{en.json.js → en/form.json.js} +5 -5
- package/dist/esm/i18n/index.js +2 -2
- package/dist/esm/index.js +36 -1
- package/dist/esm/input/ArrayInput.js +260 -136
- package/dist/esm/input/AvatarInput.js +99 -63
- package/dist/esm/input/Checkbox.js +26 -27
- package/dist/esm/input/ColorInput.js +141 -84
- package/dist/esm/input/ColorPicker.js +80 -97
- package/dist/esm/input/DateInput.js +192 -0
- package/dist/esm/input/Field.js +21 -20
- package/dist/esm/input/File.js +89 -84
- package/dist/esm/input/Hidden.js +28 -14
- package/dist/esm/input/Input.js +51 -50
- package/dist/esm/input/Number.js +48 -46
- package/dist/esm/input/Password.js +54 -52
- package/dist/esm/input/Radio.js +27 -17
- package/dist/esm/input/RadioGroup.js +27 -12
- package/dist/esm/input/{Range.js → RangeSlider.js} +62 -67
- package/dist/esm/input/Search.js +61 -67
- package/dist/esm/input/Slider.js +58 -67
- package/dist/esm/input/Switch.js +31 -29
- package/dist/esm/input/Textarea.js +67 -55
- package/dist/esm/internal/ErrorIcon.js +11 -11
- package/dist/esm/internal/FieldWrapper.js +23 -13
- package/dist/esm/internal/InputWrapper.js +88 -90
- package/dist/esm/ranger/index.js +7 -7
- package/dist/esm/ranger/utils.js +9 -4
- package/dist/esm/select/AutoComplete.js +95 -78
- package/dist/esm/select/DualList.js +233 -152
- package/dist/esm/select/List.js +125 -92
- package/dist/esm/select/Options.js +58 -54
- package/dist/esm/select/Select.js +122 -92
- package/dist/esm/select/helpers.js +5 -5
- package/dist/esm/select/useSelect.js +84 -59
- package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
- package/dist/esm/utils/eventHandlers.js +16 -6
- package/dist/esm/utils/getColor.js +6 -6
- package/dist/esm/utils/getProps.js +49 -0
- package/dist/types/builder/FileField.d.ts +7 -4
- package/dist/types/builder/FormField.d.ts +4 -2
- package/dist/types/builder/SchemaEditor.d.ts +6 -0
- package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
- package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
- package/dist/types/builder/components/SchemaField.d.ts +12 -0
- package/dist/types/form/Controller.d.ts +45 -3
- package/dist/types/form/Form.d.ts +38 -7
- package/dist/types/form/useForm.d.ts +6 -0
- package/dist/types/hooks/useFileUploader.d.ts +24 -3
- package/dist/types/hooks/useFormBuilder.d.ts +41 -3
- package/dist/types/index.d.ts +32 -2
- package/dist/types/input/ArrayInput.d.ts +68 -13
- package/dist/types/input/AvatarInput.d.ts +23 -6
- package/dist/types/input/Checkbox.d.ts +12 -10
- package/dist/types/input/ColorInput.d.ts +15 -5
- package/dist/types/input/ColorPicker.d.ts +10 -2
- package/dist/types/input/DateInput.d.ts +21 -0
- package/dist/types/input/Field.d.ts +9 -36
- package/dist/types/input/File.d.ts +16 -4
- package/dist/types/input/Hidden.d.ts +7 -6
- package/dist/types/input/Input.d.ts +11 -4
- package/dist/types/input/Number.d.ts +9 -4
- package/dist/types/input/Password.d.ts +9 -4
- package/dist/types/input/Radio.d.ts +9 -4
- package/dist/types/input/RadioGroup.d.ts +14 -6
- package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +11 -6
- package/dist/types/input/Search.d.ts +14 -4
- package/dist/types/input/Slider.d.ts +9 -6
- package/dist/types/input/Switch.d.ts +13 -8
- package/dist/types/input/Textarea.d.ts +15 -4
- package/dist/types/internal/ErrorIcon.d.ts +4 -1
- package/dist/types/internal/FieldWrapper.d.ts +10 -1
- package/dist/types/internal/InputWrapper.d.ts +19 -1
- package/dist/types/ranger/index.d.ts +7 -3
- package/dist/types/ranger/utils.d.ts +2 -2
- package/dist/types/select/AutoComplete.d.ts +7 -2
- package/dist/types/select/DualList.d.ts +11 -37
- package/dist/types/select/List.d.ts +7 -2
- package/dist/types/select/Options.d.ts +8 -7
- package/dist/types/select/Select.d.ts +7 -2
- package/dist/types/select/helpers.d.ts +2 -2
- package/dist/types/select/useSelect.d.ts +10 -28
- package/dist/types/types/index.d.ts +73 -39
- package/dist/types/types/schema.d.ts +78 -32
- package/dist/types/utils/eventHandlers.d.ts +4 -4
- package/dist/types/utils/getColor.d.ts +4 -4
- package/dist/types/utils/getProps.d.ts +26 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/package.json +25 -25
- package/src/builder/FileField.tsx +73 -56
- package/src/builder/FormField.tsx +66 -58
- package/src/builder/SchemaEditor.tsx +78 -0
- package/src/builder/components/AddSchemaFields.tsx +64 -0
- package/src/builder/components/SchemaArrayForm.tsx +146 -0
- package/src/builder/components/SchemaField.tsx +405 -0
- package/src/form/Controller.tsx +61 -24
- package/src/form/Form.tsx +74 -50
- package/src/form/useForm.ts +53 -0
- package/src/hooks/useFileUploader.ts +115 -84
- package/src/hooks/useFormBuilder.tsx +80 -41
- package/src/i18n/{ar.json → ar/form.json} +5 -4
- package/src/i18n/{en.json → en/form.json} +5 -5
- package/src/i18n/index.ts +2 -2
- package/src/index.ts +34 -20
- package/src/input/ArrayInput.tsx +255 -173
- package/src/input/AvatarInput.tsx +82 -39
- package/src/input/Checkbox.tsx +30 -54
- package/src/input/ColorInput.tsx +146 -105
- package/src/input/ColorPicker.tsx +76 -102
- package/src/input/DateInput.tsx +225 -0
- package/src/input/Field.tsx +23 -65
- package/src/input/File.tsx +76 -98
- package/src/input/Hidden.tsx +36 -24
- package/src/input/Input.tsx +55 -53
- package/src/input/Number.tsx +49 -54
- package/src/input/Password.tsx +61 -66
- package/src/input/Radio.tsx +28 -37
- package/src/input/RadioGroup.tsx +30 -25
- package/src/input/{Range.tsx → RangeSlider.tsx} +81 -109
- package/src/input/Search.tsx +64 -77
- package/src/input/Slider.tsx +80 -109
- package/src/input/Switch.tsx +39 -53
- package/src/input/Textarea.tsx +72 -59
- package/src/internal/ErrorIcon.tsx +22 -21
- package/src/internal/FieldWrapper.tsx +33 -23
- package/src/internal/InputWrapper.tsx +132 -143
- package/src/ranger/index.tsx +37 -117
- package/src/ranger/utils.ts +12 -11
- package/src/select/AutoComplete.tsx +113 -123
- package/src/select/DualList.tsx +218 -204
- package/src/select/List.tsx +130 -97
- package/src/select/Options.tsx +59 -57
- package/src/select/Select.tsx +148 -122
- package/src/select/helpers.ts +13 -8
- package/src/select/useSelect.tsx +110 -127
- package/src/types/index.ts +76 -45
- package/src/types/schema.ts +97 -40
- package/src/utils/eventHandlers.ts +24 -26
- package/src/utils/getColor.ts +9 -8
- package/src/utils/getProps.ts +47 -0
- package/src/utils/index.ts +1 -0
- package/dist/cjs/input/ArrayInput.module.css +0 -1
- package/dist/cjs/input/ColorPicker.module.css +0 -1
- package/dist/cjs/input/File.module.css +0 -1
- package/dist/cjs/internal/Field.module.css +0 -1
- package/dist/esm/input/ArrayInput.module.css +0 -1
- package/dist/esm/input/ColorPicker.module.css +0 -1
- package/dist/esm/input/File.module.css +0 -1
- package/dist/esm/internal/Field.module.css +0 -1
package/src/form/Form.tsx
CHANGED
|
@@ -21,24 +21,14 @@
|
|
|
21
21
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { yupResolver } from "@hookform/resolvers/yup";
|
|
25
24
|
import { useDebounce } from "@react-fabric/core";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
type PropsWithChildren,
|
|
34
|
-
type Ref,
|
|
35
|
-
} from "react";
|
|
36
|
-
import {
|
|
37
|
-
FormProvider,
|
|
38
|
-
useForm,
|
|
39
|
-
type DefaultValues,
|
|
40
|
-
type FormState,
|
|
41
|
-
} from "react-hook-form";
|
|
25
|
+
import { EMPTY_OBJECT } from "@react-fabric/utilities";
|
|
26
|
+
import { useCallback, useEffect, useImperativeHandle, useRef, useTransition, type PropsWithChildren, type Ref } from "react";
|
|
27
|
+
import { FormProvider, useForm as useFormHook, type FormState, type Path, type Resolver } from "react-hook-form";
|
|
28
|
+
|
|
29
|
+
type NestedKeyOf<T extends object> = {
|
|
30
|
+
[K in keyof T & (string | number)]: T[K] extends object ? `${K}` | `${K}.${NestedKeyOf<T[K]>}` : K;
|
|
31
|
+
}[keyof T & (string | number)];
|
|
42
32
|
|
|
43
33
|
export interface FormRef<K extends KeyValue> {
|
|
44
34
|
state: FormState<K>;
|
|
@@ -47,20 +37,25 @@ export interface FormRef<K extends KeyValue> {
|
|
|
47
37
|
submit: () => void;
|
|
48
38
|
validate: () => Promise<boolean>;
|
|
49
39
|
getValues: () => K;
|
|
50
|
-
setValue: (key:
|
|
40
|
+
setValue: (key: NestedKeyOf<K>, value: unknown) => void;
|
|
51
41
|
setValues: (values: K) => void;
|
|
52
42
|
}
|
|
53
43
|
|
|
54
44
|
export interface FormProps<K extends KeyValue = KeyValue> {
|
|
55
45
|
formRef?: Ref<FormRef<K>>;
|
|
56
46
|
/**
|
|
57
|
-
* form
|
|
47
|
+
* react-hook-form resolver for validation
|
|
48
|
+
* @see {@link https://react-hook-form.com/ts#Resolver}
|
|
58
49
|
*/
|
|
59
|
-
|
|
50
|
+
resolver?: Resolver<K>;
|
|
60
51
|
/**
|
|
61
52
|
* default data values
|
|
62
53
|
*/
|
|
63
|
-
defaultValues?: K
|
|
54
|
+
defaultValues?: K | Promise<K>;
|
|
55
|
+
/**
|
|
56
|
+
* reset values on defaultValues change (workaround for using fetch result)
|
|
57
|
+
*/
|
|
58
|
+
resetOnChange?: boolean;
|
|
64
59
|
/**
|
|
65
60
|
* change callback
|
|
66
61
|
*/
|
|
@@ -73,42 +68,72 @@ export interface FormProps<K extends KeyValue = KeyValue> {
|
|
|
73
68
|
|
|
74
69
|
const DEFAULT_SUBMIT = () => undefined;
|
|
75
70
|
|
|
76
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Form component for managing form state and validation using react-hook-form and yup.
|
|
73
|
+
* It provides a simple interface for form handling, including submission, validation, and value management.
|
|
74
|
+
* It supports nested form structures and allows for custom validation schemas using Yup.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```jsx
|
|
78
|
+
* <Form
|
|
79
|
+
* formRef={formRef}
|
|
80
|
+
* resolver={hookformResolver(schema)}
|
|
81
|
+
* defaultValues={{ name: "", email: "" }}
|
|
82
|
+
* onSubmit={(values) => {
|
|
83
|
+
* console.log("Form submitted with values:", values);
|
|
84
|
+
* }}
|
|
85
|
+
* >
|
|
86
|
+
* <input name="name" ref={form.register} placeholder="Name" />
|
|
87
|
+
* <input name="email" ref={form.register} placeholder="Email" />
|
|
88
|
+
* <button type="submit">Submit</button>
|
|
89
|
+
* </Form>
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @see {@link https://react-hook-form.com/docs/useform} for more details on the properties.
|
|
93
|
+
*/
|
|
94
|
+
export function Form<K extends KeyValue>({
|
|
77
95
|
formRef,
|
|
78
|
-
|
|
96
|
+
resolver,
|
|
79
97
|
children,
|
|
80
|
-
|
|
98
|
+
resetOnChange,
|
|
99
|
+
defaultValues = EMPTY_OBJECT as K,
|
|
81
100
|
onSubmit = DEFAULT_SUBMIT,
|
|
82
101
|
onChange,
|
|
83
102
|
...rest
|
|
84
|
-
}: PropsWithChildren<FormProps<K>>)
|
|
103
|
+
}: PropsWithChildren<FormProps<K>>) {
|
|
85
104
|
const ref = useRef<HTMLFormElement>(null);
|
|
86
|
-
const form =
|
|
105
|
+
const form = useFormHook<K>({
|
|
87
106
|
shouldFocusError: true,
|
|
88
|
-
resolver
|
|
89
|
-
defaultValues:
|
|
107
|
+
resolver,
|
|
108
|
+
defaultValues: async () => await Promise.resolve(defaultValues),
|
|
90
109
|
});
|
|
91
110
|
|
|
92
111
|
const [, startTransition] = useTransition();
|
|
93
|
-
const changeHandler = useDebounce(onChange, [onChange]);
|
|
112
|
+
const changeHandler = useDebounce(onChange, [onChange], 300);
|
|
94
113
|
useEffect(() => {
|
|
95
114
|
const subscription = form.watch((value) => {
|
|
96
|
-
startTransition(() => changeHandler?.(value
|
|
115
|
+
startTransition(() => changeHandler?.(value));
|
|
97
116
|
});
|
|
98
117
|
return () => subscription.unsubscribe();
|
|
99
|
-
}, [form.watch, changeHandler]);
|
|
118
|
+
}, [form.watch, changeHandler, form]);
|
|
100
119
|
|
|
101
120
|
const handleReset = useCallback(
|
|
102
|
-
(reset:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
?.querySelector<HTMLElement>("[auto-focus], input, textarea")
|
|
107
|
-
|
|
121
|
+
(reset: K | Promise<K> = {} as K) => {
|
|
122
|
+
void Promise.resolve(reset).then((resp) => {
|
|
123
|
+
form.reset(resp);
|
|
124
|
+
form.clearErrors();
|
|
125
|
+
ref.current?.querySelector<HTMLElement>("[auto-focus], input, textarea")?.focus();
|
|
126
|
+
});
|
|
108
127
|
},
|
|
109
|
-
[
|
|
128
|
+
[form],
|
|
110
129
|
);
|
|
111
130
|
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (resetOnChange) {
|
|
133
|
+
handleReset(defaultValues);
|
|
134
|
+
}
|
|
135
|
+
}, [defaultValues, handleReset, resetOnChange]);
|
|
136
|
+
|
|
112
137
|
useImperativeHandle(
|
|
113
138
|
formRef,
|
|
114
139
|
() => ({
|
|
@@ -116,12 +141,13 @@ export const Form = <K extends KeyValue>({
|
|
|
116
141
|
reset: () => handleReset(defaultValues),
|
|
117
142
|
clear: () => handleReset(),
|
|
118
143
|
submit: () => form.handleSubmit(onSubmit),
|
|
119
|
-
validate: async () => await form.trigger(),
|
|
144
|
+
validate: async (fields?: Array<Path<K>>) => await form.trigger(fields),
|
|
120
145
|
getValues: () => form.getValues(),
|
|
121
146
|
setValues: (v) => setTimeout(() => form.reset(v), 50),
|
|
122
147
|
setValue: (k, v) =>
|
|
123
148
|
setTimeout(
|
|
124
149
|
() =>
|
|
150
|
+
// @ts-expect-error ignore
|
|
125
151
|
form.setValue(k as AnyObject, v, {
|
|
126
152
|
shouldDirty: false,
|
|
127
153
|
shouldTouch: false,
|
|
@@ -129,18 +155,13 @@ export const Form = <K extends KeyValue>({
|
|
|
129
155
|
50,
|
|
130
156
|
),
|
|
131
157
|
}),
|
|
132
|
-
[
|
|
158
|
+
[defaultValues, form, handleReset, onSubmit],
|
|
133
159
|
);
|
|
134
160
|
|
|
135
|
-
const onInvalid = useCallback((_: AnyObject, e
|
|
136
|
-
const el = e
|
|
161
|
+
const onInvalid = useCallback((_: AnyObject, e?: React.BaseSyntheticEvent) => {
|
|
162
|
+
const el = e?.target as HTMLFormElement;
|
|
137
163
|
setTimeout(
|
|
138
|
-
() =>
|
|
139
|
-
el
|
|
140
|
-
.querySelector(
|
|
141
|
-
"input[aria-invalid='true'],textarea[aria-invalid='true']",
|
|
142
|
-
)
|
|
143
|
-
?.focus(),
|
|
164
|
+
() => el?.querySelector<HTMLInputElement>("input[aria-invalid='true'],textarea[aria-invalid='true']")?.focus(),
|
|
144
165
|
10,
|
|
145
166
|
);
|
|
146
167
|
}, []);
|
|
@@ -150,7 +171,10 @@ export const Form = <K extends KeyValue>({
|
|
|
150
171
|
<form
|
|
151
172
|
ref={ref}
|
|
152
173
|
onReset={() => handleReset(defaultValues)}
|
|
153
|
-
onSubmit={
|
|
174
|
+
onSubmit={(e) => {
|
|
175
|
+
e.stopPropagation();
|
|
176
|
+
void form.handleSubmit(onSubmit, onInvalid)(e);
|
|
177
|
+
}}
|
|
154
178
|
data-loading={form.formState.isSubmitting}
|
|
155
179
|
className="contents"
|
|
156
180
|
autoComplete="off"
|
|
@@ -161,4 +185,4 @@ export const Form = <K extends KeyValue>({
|
|
|
161
185
|
</form>
|
|
162
186
|
</FormProvider>
|
|
163
187
|
);
|
|
164
|
-
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* React Fabric
|
|
3
|
+
* @version: 1.0.0
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
* The MIT License (MIT)
|
|
7
|
+
* Copyright (c) 2026 Adarsh Pastakia
|
|
8
|
+
*
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
10
|
+
* and associated documentation files (the "Software"), to deal in the Software without restriction,
|
|
11
|
+
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
12
|
+
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
|
13
|
+
* subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
16
|
+
* portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
19
|
+
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
20
|
+
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { FieldValues, UseFormReturn } from "react-hook-form";
|
|
25
|
+
import { useFormContext, useWatch } from "react-hook-form";
|
|
26
|
+
|
|
27
|
+
export type UseCustomFormReturn<K extends FieldValues = FieldValues> = Pick<
|
|
28
|
+
UseFormReturn<K>,
|
|
29
|
+
"formState" | "getValues" | "setValue" | "watch" | "reset" | "resetField" | "setError" | "setFocus"
|
|
30
|
+
> & {
|
|
31
|
+
values: K;
|
|
32
|
+
watch: UseFormReturn<K>["watch"];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function useForm<K extends KeyValue = KeyValue>(): UseCustomFormReturn<K> {
|
|
36
|
+
const { formState, getValues, setValue, watch, reset, resetField, setError, setFocus } = useFormContext<K>();
|
|
37
|
+
const values = useWatch<K>();
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
values: {
|
|
41
|
+
...values,
|
|
42
|
+
...getValues(),
|
|
43
|
+
},
|
|
44
|
+
formState,
|
|
45
|
+
getValues,
|
|
46
|
+
setValue,
|
|
47
|
+
watch,
|
|
48
|
+
reset,
|
|
49
|
+
resetField,
|
|
50
|
+
setError,
|
|
51
|
+
setFocus,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -22,13 +22,10 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { isArray, isObject, isString, uuid } from "@react-fabric/utilities";
|
|
25
|
-
import {
|
|
26
|
-
import { useCallback, useReducer } from "react";
|
|
25
|
+
import type { AxiosRequestConfig } from "axios";
|
|
26
|
+
import { useCallback, useEffect, useReducer } from "react";
|
|
27
27
|
|
|
28
|
-
export type UploadHandler = (
|
|
29
|
-
data: FormData,
|
|
30
|
-
config: AxiosRequestConfig,
|
|
31
|
-
) => Promise<string>;
|
|
28
|
+
export type UploadHandler = (data: FormData, config: AxiosRequestConfig) => Promise<string>;
|
|
32
29
|
|
|
33
30
|
interface FileInfo {
|
|
34
31
|
key: string;
|
|
@@ -54,14 +51,42 @@ type UploaderActions =
|
|
|
54
51
|
| { type: "stop"; key: string; path: string }
|
|
55
52
|
| { type: "error"; key: string; error: string }
|
|
56
53
|
| { type: "progress"; key: string; progress: number }
|
|
54
|
+
| { type: "init"; initialList?: string | FileInfo | FileInfo[] }
|
|
57
55
|
| { type: "remove"; key: string }
|
|
58
56
|
| { type: "abort"; key: string };
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
/**
|
|
59
|
+
* useFileUploader hook provides a way to handle file uploads
|
|
60
|
+
* with progress tracking, error handling, and file management.
|
|
61
|
+
* It uses a reducer to manage the state of uploaded files and their statuses.
|
|
62
|
+
*
|
|
63
|
+
* @param uploader - The function that handles the file upload.
|
|
64
|
+
* @param initialList - Initial list of files to be managed by the uploader.
|
|
65
|
+
* @param multiple - Whether to allow multiple file uploads or not.
|
|
66
|
+
* @returns An object containing methods to upload, remove, retry files,
|
|
67
|
+
* and the current state of the uploader including pending uploads and file list.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```jsx
|
|
71
|
+
* const { upload, remove, retry, state } = useFileUploader(uploader, initialList, multiple);
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function useFileUploader(
|
|
61
75
|
uploader: UploadHandler,
|
|
62
76
|
initialList?: string | FileInfo | FileInfo[],
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
{
|
|
78
|
+
multiple = true,
|
|
79
|
+
onChange,
|
|
80
|
+
}:
|
|
81
|
+
| {
|
|
82
|
+
multiple?: true;
|
|
83
|
+
onChange?: (files: FileInfo[] | null) => void;
|
|
84
|
+
}
|
|
85
|
+
| {
|
|
86
|
+
multiple: false;
|
|
87
|
+
onChange?: (files: FileInfo | null) => void;
|
|
88
|
+
} = {},
|
|
89
|
+
) {
|
|
65
90
|
const [state, dispatch] = useReducer(
|
|
66
91
|
(state: UploaderState, action: UploaderActions) => {
|
|
67
92
|
if (action.type === "start") {
|
|
@@ -72,7 +97,7 @@ export const useFileUploader = (
|
|
|
72
97
|
if (action.type === "stop") {
|
|
73
98
|
const fileObject = state.files.get(action.key);
|
|
74
99
|
if (fileObject) {
|
|
75
|
-
const { progress, abort, ...rest } = fileObject;
|
|
100
|
+
const { progress: _, abort: __, ...rest } = fileObject;
|
|
76
101
|
state.files.set(action.key, {
|
|
77
102
|
...rest,
|
|
78
103
|
path: action.path,
|
|
@@ -84,7 +109,7 @@ export const useFileUploader = (
|
|
|
84
109
|
if (action.type === "error") {
|
|
85
110
|
const fileObject = state.files.get(action.key);
|
|
86
111
|
if (fileObject) {
|
|
87
|
-
const { file, progress, abort, ...rest } = fileObject;
|
|
112
|
+
const { file: _, progress: __, abort: ___, ...rest } = fileObject;
|
|
88
113
|
state.files.set(action.key, { ...rest, error: action.error });
|
|
89
114
|
}
|
|
90
115
|
state.count--;
|
|
@@ -103,6 +128,29 @@ export const useFileUploader = (
|
|
|
103
128
|
if (action.type === "remove") {
|
|
104
129
|
state.files.delete(action.key);
|
|
105
130
|
}
|
|
131
|
+
if (action.type === "init") {
|
|
132
|
+
state.files = new Map();
|
|
133
|
+
if (isString(action.initialList)) {
|
|
134
|
+
const key = uuid();
|
|
135
|
+
state.files.set(key, {
|
|
136
|
+
key,
|
|
137
|
+
path: action.initialList,
|
|
138
|
+
filename: "",
|
|
139
|
+
size: 0,
|
|
140
|
+
mime: "",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (isObject(action.initialList)) {
|
|
144
|
+
const key = uuid();
|
|
145
|
+
state.files.set(key, { ...(action.initialList as unknown as FileInfo), key });
|
|
146
|
+
}
|
|
147
|
+
if (isArray(action.initialList)) {
|
|
148
|
+
action.initialList.forEach((file) => {
|
|
149
|
+
const key = uuid();
|
|
150
|
+
state.files.set(key, { ...file, key });
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
106
154
|
state.list = Array.from(state.files.values())
|
|
107
155
|
.map((file) => {
|
|
108
156
|
return (
|
|
@@ -118,81 +166,68 @@ export const useFileUploader = (
|
|
|
118
166
|
);
|
|
119
167
|
})
|
|
120
168
|
.filter(Boolean) as FileInfo[];
|
|
169
|
+
if (action.type === "remove" || action.type === "stop") {
|
|
170
|
+
setTimeout(() => {
|
|
171
|
+
// @ts-expect-error ignore
|
|
172
|
+
onChange?.(multiple ? state.list : (state.list[0] ?? null));
|
|
173
|
+
}, 100);
|
|
174
|
+
}
|
|
121
175
|
return { ...state };
|
|
122
176
|
},
|
|
123
177
|
{
|
|
124
178
|
files: new Map(),
|
|
125
179
|
count: 0,
|
|
126
180
|
} as UploaderState,
|
|
127
|
-
(state: UploaderState) => {
|
|
128
|
-
if (initialList) {
|
|
129
|
-
if (isString(initialList)) {
|
|
130
|
-
const key = uuid();
|
|
131
|
-
state.files.set(key, {
|
|
132
|
-
key,
|
|
133
|
-
path: initialList,
|
|
134
|
-
filename: "",
|
|
135
|
-
size: 0,
|
|
136
|
-
mime: "",
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
if (isObject(initialList)) {
|
|
140
|
-
const key = uuid();
|
|
141
|
-
state.files.set(key, { ...(initialList as AnyObject), key });
|
|
142
|
-
}
|
|
143
|
-
if (isArray(initialList)) {
|
|
144
|
-
initialList.forEach((file) => {
|
|
145
|
-
const key = uuid();
|
|
146
|
-
state.files.set(key, { ...file, key });
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
state.list = Array.from(state.files.values());
|
|
151
|
-
return state;
|
|
152
|
-
},
|
|
153
181
|
);
|
|
154
182
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
file
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
dispatch({ type: "init", initialList });
|
|
185
|
+
}, [initialList]);
|
|
186
|
+
|
|
187
|
+
const doUpload = useCallback(
|
|
188
|
+
async (key: string, file: File) => {
|
|
189
|
+
const data = new FormData();
|
|
190
|
+
data.append("file", file);
|
|
191
|
+
const abort = new AbortController();
|
|
192
|
+
dispatch({
|
|
193
|
+
type: "start",
|
|
163
194
|
key,
|
|
164
|
-
file
|
|
165
|
-
progress: 0.001,
|
|
166
|
-
mime: file.type,
|
|
167
|
-
filename: file.name,
|
|
168
|
-
size: file.size,
|
|
169
|
-
abort: () => abort.abort(),
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
await uploader(data, {
|
|
173
|
-
signal: abort.signal,
|
|
174
|
-
onUploadProgress(progressEvent) {
|
|
175
|
-
dispatch({
|
|
176
|
-
type: "progress",
|
|
195
|
+
file: {
|
|
177
196
|
key,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
file,
|
|
198
|
+
progress: 0.001,
|
|
199
|
+
mime: file.type,
|
|
200
|
+
filename: file.name,
|
|
201
|
+
size: file.size,
|
|
202
|
+
abort: () => abort.abort(),
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
await uploader(data, {
|
|
206
|
+
signal: abort.signal,
|
|
207
|
+
onUploadProgress(progressEvent) {
|
|
208
|
+
dispatch({
|
|
209
|
+
type: "progress",
|
|
210
|
+
key,
|
|
211
|
+
progress: progressEvent.progress ?? 0,
|
|
212
|
+
});
|
|
213
|
+
},
|
|
184
214
|
})
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
215
|
+
.then((path) => {
|
|
216
|
+
dispatch({ type: "stop", key, path });
|
|
217
|
+
})
|
|
218
|
+
.catch((error: KeyValue) => {
|
|
219
|
+
if (error.code === "ERR_CANCELED" || error.name === "AbortError") {
|
|
220
|
+
return dispatch({ type: "abort", key });
|
|
221
|
+
}
|
|
222
|
+
dispatch({
|
|
223
|
+
type: "error",
|
|
224
|
+
key,
|
|
225
|
+
error: (error?.message as string) ?? "form:fileUploadFailed",
|
|
226
|
+
});
|
|
193
227
|
});
|
|
194
|
-
|
|
195
|
-
|
|
228
|
+
},
|
|
229
|
+
[uploader],
|
|
230
|
+
);
|
|
196
231
|
|
|
197
232
|
const upload = useCallback(
|
|
198
233
|
(files: FileList | null) => {
|
|
@@ -205,7 +240,7 @@ export const useFileUploader = (
|
|
|
205
240
|
void doUpload(uuid(), file);
|
|
206
241
|
});
|
|
207
242
|
},
|
|
208
|
-
[multiple],
|
|
243
|
+
[doUpload, multiple, state.files],
|
|
209
244
|
);
|
|
210
245
|
|
|
211
246
|
const retry = useCallback(
|
|
@@ -213,22 +248,18 @@ export const useFileUploader = (
|
|
|
213
248
|
const fileObject = state.files.get(key);
|
|
214
249
|
void (fileObject?.file && doUpload(key, fileObject.file));
|
|
215
250
|
},
|
|
216
|
-
[state],
|
|
251
|
+
[doUpload, state.files],
|
|
217
252
|
);
|
|
218
253
|
|
|
219
|
-
const remove = useCallback(
|
|
220
|
-
(
|
|
221
|
-
|
|
222
|
-
},
|
|
223
|
-
[state],
|
|
224
|
-
);
|
|
254
|
+
const remove = useCallback((key: string) => {
|
|
255
|
+
dispatch({ type: "remove", key });
|
|
256
|
+
}, []);
|
|
225
257
|
|
|
226
258
|
return {
|
|
227
259
|
upload,
|
|
228
260
|
remove,
|
|
229
261
|
retry,
|
|
230
262
|
pending: state.count,
|
|
231
|
-
list: state.list,
|
|
232
263
|
files: Array.from(state.files.values()),
|
|
233
264
|
};
|
|
234
|
-
}
|
|
265
|
+
}
|