@react-fabric/form 1.0.0-beta.8 → 1.0.1

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.
Files changed (215) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +95 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +94 -33
  4. package/{src/input/File.module.css → css/_file.css} +21 -6
  5. package/css/styles.css +27 -0
  6. package/dist/cjs/builder/FileField.js +107 -78
  7. package/dist/cjs/builder/FormField.js +60 -39
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +64 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +146 -0
  11. package/dist/cjs/builder/components/SchemaField.js +423 -0
  12. package/dist/cjs/form/Controller.js +66 -19
  13. package/dist/cjs/form/Form.js +97 -39
  14. package/dist/cjs/form/useForm.js +32 -0
  15. package/dist/cjs/hooks/useFileUploader.js +74 -45
  16. package/dist/cjs/hooks/useFormBuilder.js +85 -31
  17. package/dist/cjs/i18n/{ar.json.js → ar/form.json.js} +5 -4
  18. package/dist/cjs/i18n/{en.json.js → en/form.json.js} +5 -5
  19. package/dist/cjs/i18n/index.js +4 -4
  20. package/dist/cjs/index.js +41 -2
  21. package/dist/cjs/input/ArrayInput.js +259 -135
  22. package/dist/cjs/input/AvatarInput.js +99 -63
  23. package/dist/cjs/input/Checkbox.js +24 -25
  24. package/dist/cjs/input/ColorInput.js +138 -81
  25. package/dist/cjs/input/ColorPicker.js +78 -95
  26. package/dist/cjs/input/DateInput.js +192 -0
  27. package/dist/cjs/input/Field.js +21 -20
  28. package/dist/cjs/input/File.js +85 -80
  29. package/dist/cjs/input/Hidden.js +27 -13
  30. package/dist/cjs/input/Input.js +49 -48
  31. package/dist/cjs/input/Number.js +46 -44
  32. package/dist/cjs/input/Password.js +51 -49
  33. package/dist/cjs/input/Radio.js +26 -16
  34. package/dist/cjs/input/RadioGroup.js +26 -11
  35. package/dist/cjs/input/{Range.js → RangeSlider.js} +61 -66
  36. package/dist/cjs/input/Search.js +58 -64
  37. package/dist/cjs/input/Slider.js +57 -66
  38. package/dist/cjs/input/Switch.js +29 -27
  39. package/dist/cjs/input/Textarea.js +65 -53
  40. package/dist/cjs/internal/ErrorIcon.js +9 -9
  41. package/dist/cjs/internal/FieldWrapper.js +23 -13
  42. package/dist/cjs/internal/InputWrapper.js +85 -87
  43. package/dist/cjs/ranger/index.js +7 -7
  44. package/dist/cjs/ranger/utils.js +9 -4
  45. package/dist/cjs/select/AutoComplete.js +93 -76
  46. package/dist/cjs/select/DualList.js +235 -154
  47. package/dist/cjs/select/List.js +122 -89
  48. package/dist/cjs/select/Options.js +58 -54
  49. package/dist/cjs/select/Select.js +119 -89
  50. package/dist/cjs/select/helpers.js +5 -5
  51. package/dist/cjs/select/useSelect.js +83 -58
  52. package/dist/cjs/types/schema.js +43 -0
  53. package/dist/cjs/utils/eventHandlers.js +16 -6
  54. package/dist/cjs/utils/getColor.js +6 -6
  55. package/dist/cjs/utils/getProps.js +49 -0
  56. package/dist/esm/builder/FileField.js +109 -80
  57. package/dist/esm/builder/FormField.js +60 -39
  58. package/dist/esm/builder/SchemaEditor.js +72 -0
  59. package/dist/esm/builder/components/AddSchemaFields.js +64 -0
  60. package/dist/esm/builder/components/SchemaArrayForm.js +146 -0
  61. package/dist/esm/builder/components/SchemaField.js +423 -0
  62. package/dist/esm/form/Controller.js +66 -19
  63. package/dist/esm/form/Form.js +97 -39
  64. package/dist/esm/form/useForm.js +32 -0
  65. package/dist/esm/hooks/useFileUploader.js +75 -46
  66. package/dist/esm/hooks/useFormBuilder.js +84 -30
  67. package/dist/esm/i18n/{ar.json.js → ar/form.json.js} +5 -4
  68. package/dist/esm/i18n/{en.json.js → en/form.json.js} +5 -5
  69. package/dist/esm/i18n/index.js +2 -2
  70. package/dist/esm/index.js +36 -1
  71. package/dist/esm/input/ArrayInput.js +260 -136
  72. package/dist/esm/input/AvatarInput.js +99 -63
  73. package/dist/esm/input/Checkbox.js +26 -27
  74. package/dist/esm/input/ColorInput.js +141 -84
  75. package/dist/esm/input/ColorPicker.js +80 -97
  76. package/dist/esm/input/DateInput.js +192 -0
  77. package/dist/esm/input/Field.js +21 -20
  78. package/dist/esm/input/File.js +89 -84
  79. package/dist/esm/input/Hidden.js +28 -14
  80. package/dist/esm/input/Input.js +51 -50
  81. package/dist/esm/input/Number.js +48 -46
  82. package/dist/esm/input/Password.js +54 -52
  83. package/dist/esm/input/Radio.js +27 -17
  84. package/dist/esm/input/RadioGroup.js +27 -12
  85. package/dist/esm/input/{Range.js → RangeSlider.js} +62 -67
  86. package/dist/esm/input/Search.js +61 -67
  87. package/dist/esm/input/Slider.js +58 -67
  88. package/dist/esm/input/Switch.js +31 -29
  89. package/dist/esm/input/Textarea.js +67 -55
  90. package/dist/esm/internal/ErrorIcon.js +11 -11
  91. package/dist/esm/internal/FieldWrapper.js +23 -13
  92. package/dist/esm/internal/InputWrapper.js +88 -90
  93. package/dist/esm/ranger/index.js +7 -7
  94. package/dist/esm/ranger/utils.js +9 -4
  95. package/dist/esm/select/AutoComplete.js +95 -78
  96. package/dist/esm/select/DualList.js +233 -152
  97. package/dist/esm/select/List.js +125 -92
  98. package/dist/esm/select/Options.js +58 -54
  99. package/dist/esm/select/Select.js +122 -92
  100. package/dist/esm/select/helpers.js +5 -5
  101. package/dist/esm/select/useSelect.js +84 -59
  102. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  103. package/dist/esm/utils/eventHandlers.js +16 -6
  104. package/dist/esm/utils/getColor.js +6 -6
  105. package/dist/esm/utils/getProps.js +49 -0
  106. package/dist/types/builder/FileField.d.ts +7 -4
  107. package/dist/types/builder/FormField.d.ts +4 -2
  108. package/dist/types/builder/SchemaEditor.d.ts +6 -0
  109. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  110. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  111. package/dist/types/builder/components/SchemaField.d.ts +12 -0
  112. package/dist/types/form/Controller.d.ts +45 -3
  113. package/dist/types/form/Form.d.ts +38 -7
  114. package/dist/types/form/useForm.d.ts +6 -0
  115. package/dist/types/hooks/useFileUploader.d.ts +24 -3
  116. package/dist/types/hooks/useFormBuilder.d.ts +41 -3
  117. package/dist/types/index.d.ts +32 -2
  118. package/dist/types/input/ArrayInput.d.ts +68 -13
  119. package/dist/types/input/AvatarInput.d.ts +23 -6
  120. package/dist/types/input/Checkbox.d.ts +12 -10
  121. package/dist/types/input/ColorInput.d.ts +15 -5
  122. package/dist/types/input/ColorPicker.d.ts +10 -2
  123. package/dist/types/input/DateInput.d.ts +21 -0
  124. package/dist/types/input/Field.d.ts +9 -36
  125. package/dist/types/input/File.d.ts +16 -4
  126. package/dist/types/input/Hidden.d.ts +7 -6
  127. package/dist/types/input/Input.d.ts +11 -4
  128. package/dist/types/input/Number.d.ts +9 -4
  129. package/dist/types/input/Password.d.ts +9 -4
  130. package/dist/types/input/Radio.d.ts +9 -4
  131. package/dist/types/input/RadioGroup.d.ts +14 -6
  132. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +11 -6
  133. package/dist/types/input/Search.d.ts +14 -4
  134. package/dist/types/input/Slider.d.ts +9 -6
  135. package/dist/types/input/Switch.d.ts +13 -8
  136. package/dist/types/input/Textarea.d.ts +15 -4
  137. package/dist/types/internal/ErrorIcon.d.ts +4 -1
  138. package/dist/types/internal/FieldWrapper.d.ts +10 -1
  139. package/dist/types/internal/InputWrapper.d.ts +19 -1
  140. package/dist/types/ranger/index.d.ts +7 -3
  141. package/dist/types/ranger/utils.d.ts +2 -2
  142. package/dist/types/select/AutoComplete.d.ts +7 -2
  143. package/dist/types/select/DualList.d.ts +11 -37
  144. package/dist/types/select/List.d.ts +7 -2
  145. package/dist/types/select/Options.d.ts +8 -7
  146. package/dist/types/select/Select.d.ts +7 -2
  147. package/dist/types/select/helpers.d.ts +2 -2
  148. package/dist/types/select/useSelect.d.ts +10 -28
  149. package/dist/types/types/index.d.ts +73 -39
  150. package/dist/types/types/schema.d.ts +78 -32
  151. package/dist/types/utils/eventHandlers.d.ts +4 -4
  152. package/dist/types/utils/getColor.d.ts +4 -4
  153. package/dist/types/utils/getProps.d.ts +26 -0
  154. package/dist/types/utils/index.d.ts +1 -0
  155. package/package.json +25 -25
  156. package/src/builder/FileField.tsx +73 -56
  157. package/src/builder/FormField.tsx +66 -58
  158. package/src/builder/SchemaEditor.tsx +78 -0
  159. package/src/builder/components/AddSchemaFields.tsx +64 -0
  160. package/src/builder/components/SchemaArrayForm.tsx +146 -0
  161. package/src/builder/components/SchemaField.tsx +405 -0
  162. package/src/form/Controller.tsx +61 -24
  163. package/src/form/Form.tsx +74 -50
  164. package/src/form/useForm.ts +53 -0
  165. package/src/hooks/useFileUploader.ts +115 -84
  166. package/src/hooks/useFormBuilder.tsx +80 -41
  167. package/src/i18n/{ar.json → ar/form.json} +5 -4
  168. package/src/i18n/{en.json → en/form.json} +5 -5
  169. package/src/i18n/index.ts +2 -2
  170. package/src/index.ts +34 -20
  171. package/src/input/ArrayInput.tsx +255 -173
  172. package/src/input/AvatarInput.tsx +82 -39
  173. package/src/input/Checkbox.tsx +30 -54
  174. package/src/input/ColorInput.tsx +146 -105
  175. package/src/input/ColorPicker.tsx +76 -102
  176. package/src/input/DateInput.tsx +225 -0
  177. package/src/input/Field.tsx +23 -65
  178. package/src/input/File.tsx +76 -98
  179. package/src/input/Hidden.tsx +36 -24
  180. package/src/input/Input.tsx +55 -53
  181. package/src/input/Number.tsx +49 -54
  182. package/src/input/Password.tsx +61 -66
  183. package/src/input/Radio.tsx +28 -37
  184. package/src/input/RadioGroup.tsx +30 -25
  185. package/src/input/{Range.tsx → RangeSlider.tsx} +81 -109
  186. package/src/input/Search.tsx +64 -77
  187. package/src/input/Slider.tsx +80 -109
  188. package/src/input/Switch.tsx +39 -53
  189. package/src/input/Textarea.tsx +72 -59
  190. package/src/internal/ErrorIcon.tsx +22 -21
  191. package/src/internal/FieldWrapper.tsx +33 -23
  192. package/src/internal/InputWrapper.tsx +132 -143
  193. package/src/ranger/index.tsx +37 -117
  194. package/src/ranger/utils.ts +12 -11
  195. package/src/select/AutoComplete.tsx +113 -123
  196. package/src/select/DualList.tsx +218 -204
  197. package/src/select/List.tsx +130 -97
  198. package/src/select/Options.tsx +59 -57
  199. package/src/select/Select.tsx +148 -122
  200. package/src/select/helpers.ts +13 -8
  201. package/src/select/useSelect.tsx +110 -127
  202. package/src/types/index.ts +76 -45
  203. package/src/types/schema.ts +97 -40
  204. package/src/utils/eventHandlers.ts +24 -26
  205. package/src/utils/getColor.ts +9 -8
  206. package/src/utils/getProps.ts +47 -0
  207. package/src/utils/index.ts +1 -0
  208. package/dist/cjs/input/ArrayInput.module.css +0 -1
  209. package/dist/cjs/input/ColorPicker.module.css +0 -1
  210. package/dist/cjs/input/File.module.css +0 -1
  211. package/dist/cjs/internal/Field.module.css +0 -1
  212. package/dist/esm/input/ArrayInput.module.css +0 -1
  213. package/dist/esm/input/ColorPicker.module.css +0 -1
  214. package/dist/esm/input/File.module.css +0 -1
  215. 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 { type yup } from "@react-fabric/utilities";
27
- import {
28
- useCallback,
29
- useEffect,
30
- useImperativeHandle,
31
- useRef,
32
- useTransition,
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: keyof K, value: AnyObject) => void;
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 data schema
47
+ * react-hook-form resolver for validation
48
+ * @see {@link https://react-hook-form.com/ts#Resolver}
58
49
  */
59
- schema?: yup.ObjectSchema<K>;
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
- export const Form = <K extends KeyValue>({
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
- schema,
96
+ resolver,
79
97
  children,
80
- defaultValues,
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 = useForm<K>({
105
+ const form = useFormHook<K>({
87
106
  shouldFocusError: true,
88
- resolver: schema && (yupResolver(schema) as AnyObject),
89
- defaultValues: defaultValues as DefaultValues<K>,
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 as K));
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: AnyObject = {}) => {
103
- form.reset(reset);
104
- form.clearErrors();
105
- ref.current
106
- ?.querySelector<HTMLElement>("[auto-focus], input, textarea")
107
- ?.focus();
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
- [defaultValues],
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
- [onSubmit, form, form.formState, defaultValues],
158
+ [defaultValues, form, handleReset, onSubmit],
133
159
  );
134
160
 
135
- const onInvalid = useCallback((_: AnyObject, e: AnyObject) => {
136
- const el = e.target;
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={form.handleSubmit(onSubmit, onInvalid) as AnyObject}
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 { type AxiosRequestConfig } from "axios";
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
- export const useFileUploader = (
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
- multiple = true,
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
- const doUpload = useCallback(async (key: string, file: File) => {
156
- const data = new FormData();
157
- data.append("file", file);
158
- const abort = new AbortController();
159
- dispatch({
160
- type: "start",
161
- key,
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
- progress: progressEvent.progress ?? 0,
179
- });
180
- },
181
- })
182
- .then((path) => {
183
- dispatch({ type: "stop", key, path });
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
- .catch((error) => {
186
- if (error.code === "ERR_CANCELED" || error.name === "AbortError") {
187
- return dispatch({ type: "abort", key });
188
- }
189
- dispatch({
190
- type: "error",
191
- key,
192
- error: error?.message ?? "form:fileUploadFailed",
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
- (key: string) => {
221
- dispatch({ type: "remove", key });
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
+ }