@react-fabric/form 1.0.0-beta.5 → 1.0.0-beta.52

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 (187) hide show
  1. package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
  2. package/css/_colorPicker.css +65 -0
  3. package/{src/internal/Field.module.css → css/_field.css} +64 -29
  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 +37 -25
  7. package/dist/cjs/builder/FormField.js +50 -34
  8. package/dist/cjs/builder/SchemaEditor.js +72 -0
  9. package/dist/cjs/builder/components/AddSchemaFields.js +62 -0
  10. package/dist/cjs/builder/components/SchemaArrayForm.js +143 -0
  11. package/dist/cjs/builder/components/SchemaField.js +414 -0
  12. package/dist/cjs/form/Controller.js +46 -1
  13. package/dist/cjs/form/Form.js +43 -6
  14. package/dist/cjs/hooks/useFileUploader.js +16 -0
  15. package/dist/cjs/hooks/useFormBuilder.js +57 -15
  16. package/dist/cjs/i18n/ar.json.js +5 -4
  17. package/dist/cjs/i18n/en.json.js +5 -5
  18. package/dist/cjs/index.js +11 -2
  19. package/dist/cjs/input/ArrayInput.js +100 -33
  20. package/dist/cjs/input/AvatarInput.js +17 -8
  21. package/dist/cjs/input/Checkbox.js +15 -5
  22. package/dist/cjs/input/ColorInput.js +108 -56
  23. package/dist/cjs/input/ColorPicker.js +11 -35
  24. package/dist/cjs/input/DateInput.js +195 -0
  25. package/dist/cjs/input/Field.js +10 -4
  26. package/dist/cjs/input/File.js +14 -7
  27. package/dist/cjs/input/Hidden.js +11 -1
  28. package/dist/cjs/input/Input.js +19 -7
  29. package/dist/cjs/input/Number.js +17 -7
  30. package/dist/cjs/input/Password.js +16 -8
  31. package/dist/cjs/input/Radio.js +11 -2
  32. package/dist/cjs/input/RadioGroup.js +14 -3
  33. package/dist/cjs/input/{Range.js → RangeSlider.js} +14 -6
  34. package/dist/cjs/input/Search.js +25 -16
  35. package/dist/cjs/input/Slider.js +10 -4
  36. package/dist/cjs/input/Switch.js +39 -7
  37. package/dist/cjs/input/Textarea.js +23 -4
  38. package/dist/cjs/internal/ErrorIcon.js +1 -1
  39. package/dist/cjs/internal/FieldWrapper.js +11 -4
  40. package/dist/cjs/internal/InputWrapper.js +19 -16
  41. package/dist/cjs/select/AutoComplete.js +19 -8
  42. package/dist/cjs/select/DualList.js +15 -1
  43. package/dist/cjs/select/List.js +45 -20
  44. package/dist/cjs/select/Options.js +2 -2
  45. package/dist/cjs/select/Select.js +14 -4
  46. package/dist/cjs/select/useSelect.js +10 -6
  47. package/dist/cjs/types/schema.js +43 -0
  48. package/dist/cjs/utils/eventHandlers.js +11 -5
  49. package/dist/cjs/utils/getColor.js +6 -0
  50. package/dist/esm/builder/FileField.js +39 -27
  51. package/dist/esm/builder/FormField.js +50 -34
  52. package/dist/esm/builder/SchemaEditor.js +72 -0
  53. package/dist/esm/builder/components/AddSchemaFields.js +62 -0
  54. package/dist/esm/builder/components/SchemaArrayForm.js +143 -0
  55. package/dist/esm/builder/components/SchemaField.js +414 -0
  56. package/dist/esm/form/Controller.js +46 -1
  57. package/dist/esm/form/Form.js +43 -6
  58. package/dist/esm/hooks/useFileUploader.js +16 -0
  59. package/dist/esm/hooks/useFormBuilder.js +55 -13
  60. package/dist/esm/i18n/ar.json.js +5 -4
  61. package/dist/esm/i18n/en.json.js +5 -5
  62. package/dist/esm/index.js +5 -1
  63. package/dist/esm/input/ArrayInput.js +100 -33
  64. package/dist/esm/input/AvatarInput.js +17 -8
  65. package/dist/esm/input/Checkbox.js +15 -5
  66. package/dist/esm/input/ColorInput.js +110 -58
  67. package/dist/esm/input/ColorPicker.js +11 -35
  68. package/dist/esm/input/DateInput.js +195 -0
  69. package/dist/esm/input/Field.js +11 -5
  70. package/dist/esm/input/File.js +16 -9
  71. package/dist/esm/input/Hidden.js +11 -1
  72. package/dist/esm/input/Input.js +21 -9
  73. package/dist/esm/input/Number.js +19 -9
  74. package/dist/esm/input/Password.js +18 -10
  75. package/dist/esm/input/Radio.js +11 -2
  76. package/dist/esm/input/RadioGroup.js +15 -4
  77. package/dist/esm/input/{Range.js → RangeSlider.js} +14 -6
  78. package/dist/esm/input/Search.js +27 -18
  79. package/dist/esm/input/Slider.js +10 -4
  80. package/dist/esm/input/Switch.js +41 -9
  81. package/dist/esm/input/Textarea.js +24 -5
  82. package/dist/esm/internal/ErrorIcon.js +1 -1
  83. package/dist/esm/internal/FieldWrapper.js +11 -4
  84. package/dist/esm/internal/InputWrapper.js +19 -16
  85. package/dist/esm/select/AutoComplete.js +19 -8
  86. package/dist/esm/select/DualList.js +15 -1
  87. package/dist/esm/select/List.js +46 -21
  88. package/dist/esm/select/Options.js +2 -2
  89. package/dist/esm/select/Select.js +14 -4
  90. package/dist/esm/select/useSelect.js +11 -7
  91. package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
  92. package/dist/esm/utils/eventHandlers.js +11 -5
  93. package/dist/esm/utils/getColor.js +6 -1
  94. package/dist/types/builder/FileField.d.ts +4 -2
  95. package/dist/types/builder/FormField.d.ts +3 -1
  96. package/dist/types/builder/SchemaEditor.d.ts +9 -0
  97. package/dist/types/builder/components/AddSchemaFields.d.ts +2 -0
  98. package/dist/types/builder/components/SchemaArrayForm.d.ts +2 -0
  99. package/dist/types/builder/components/SchemaField.d.ts +10 -0
  100. package/dist/types/form/Controller.d.ts +47 -2
  101. package/dist/types/form/Form.d.ts +37 -2
  102. package/dist/types/hooks/useFileUploader.d.ts +16 -0
  103. package/dist/types/hooks/useFormBuilder.d.ts +32 -1
  104. package/dist/types/index.d.ts +5 -2
  105. package/dist/types/input/ArrayInput.d.ts +78 -6
  106. package/dist/types/input/AvatarInput.d.ts +14 -3
  107. package/dist/types/input/Checkbox.d.ts +13 -4
  108. package/dist/types/input/ColorInput.d.ts +15 -2
  109. package/dist/types/input/ColorPicker.d.ts +11 -1
  110. package/dist/types/input/DateInput.d.ts +28 -0
  111. package/dist/types/input/Field.d.ts +5 -1
  112. package/dist/types/input/File.d.ts +9 -0
  113. package/dist/types/input/Hidden.d.ts +8 -0
  114. package/dist/types/input/Input.d.ts +9 -1
  115. package/dist/types/input/Number.d.ts +9 -1
  116. package/dist/types/input/Password.d.ts +7 -1
  117. package/dist/types/input/Radio.d.ts +9 -1
  118. package/dist/types/input/RadioGroup.d.ts +26 -3
  119. package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +10 -2
  120. package/dist/types/input/Search.d.ts +14 -1
  121. package/dist/types/input/Slider.d.ts +7 -1
  122. package/dist/types/input/Switch.d.ts +16 -4
  123. package/dist/types/input/Textarea.d.ts +15 -1
  124. package/dist/types/internal/FieldWrapper.d.ts +1 -1
  125. package/dist/types/internal/InputWrapper.d.ts +1 -1
  126. package/dist/types/ranger/index.d.ts +2 -2
  127. package/dist/types/select/AutoComplete.d.ts +8 -1
  128. package/dist/types/select/DualList.d.ts +17 -2
  129. package/dist/types/select/List.d.ts +9 -1
  130. package/dist/types/select/Select.d.ts +9 -1
  131. package/dist/types/select/useSelect.d.ts +7 -6
  132. package/dist/types/types/index.d.ts +28 -7
  133. package/dist/types/types/schema.d.ts +78 -33
  134. package/dist/types/utils/eventHandlers.d.ts +1 -1
  135. package/package.json +15 -12
  136. package/src/builder/FileField.tsx +53 -27
  137. package/src/builder/FormField.tsx +52 -32
  138. package/src/builder/SchemaEditor.tsx +79 -0
  139. package/src/builder/components/AddSchemaFields.tsx +74 -0
  140. package/src/builder/components/SchemaArrayForm.tsx +158 -0
  141. package/src/builder/components/SchemaField.tsx +436 -0
  142. package/src/form/Controller.tsx +48 -3
  143. package/src/form/Form.tsx +45 -5
  144. package/src/hooks/useFileUploader.ts +16 -0
  145. package/src/hooks/useFormBuilder.tsx +58 -15
  146. package/src/i18n/ar.json +5 -4
  147. package/src/i18n/en.json +5 -5
  148. package/src/index.ts +11 -2
  149. package/src/input/ArrayInput.tsx +169 -54
  150. package/src/input/AvatarInput.tsx +20 -8
  151. package/src/input/Checkbox.tsx +15 -5
  152. package/src/input/ColorInput.tsx +126 -69
  153. package/src/input/ColorPicker.tsx +12 -36
  154. package/src/input/DateInput.tsx +259 -0
  155. package/src/input/Field.tsx +15 -11
  156. package/src/input/File.tsx +16 -14
  157. package/src/input/Hidden.tsx +12 -1
  158. package/src/input/Input.tsx +23 -10
  159. package/src/input/Number.tsx +18 -10
  160. package/src/input/Password.tsx +19 -12
  161. package/src/input/Radio.tsx +11 -2
  162. package/src/input/RadioGroup.tsx +35 -4
  163. package/src/input/{Range.tsx → RangeSlider.tsx} +13 -5
  164. package/src/input/Search.tsx +29 -19
  165. package/src/input/Slider.tsx +10 -4
  166. package/src/input/Switch.tsx +52 -7
  167. package/src/input/Textarea.tsx +29 -5
  168. package/src/internal/ErrorIcon.tsx +1 -1
  169. package/src/internal/FieldWrapper.tsx +8 -4
  170. package/src/internal/InputWrapper.tsx +36 -20
  171. package/src/select/AutoComplete.tsx +12 -1
  172. package/src/select/DualList.tsx +19 -0
  173. package/src/select/List.tsx +49 -8
  174. package/src/select/Options.tsx +1 -1
  175. package/src/select/Select.tsx +21 -3
  176. package/src/select/useSelect.tsx +13 -5
  177. package/src/types/index.ts +34 -8
  178. package/src/types/schema.ts +96 -41
  179. package/src/utils/eventHandlers.ts +9 -3
  180. package/dist/cjs/input/ArrayInput.module.css +0 -1
  181. package/dist/cjs/input/ColorPicker.module.css +0 -1
  182. package/dist/cjs/input/File.module.css +0 -1
  183. package/dist/cjs/internal/Field.module.css +0 -1
  184. package/dist/esm/input/ArrayInput.module.css +0 -1
  185. package/dist/esm/input/ColorPicker.module.css +0 -1
  186. package/dist/esm/input/File.module.css +0 -1
  187. package/dist/esm/internal/Field.module.css +0 -1
package/src/form/Form.tsx CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  import { yupResolver } from "@hookform/resolvers/yup";
25
25
  import { useDebounce } from "@react-fabric/core";
26
- import { type yup } from "@react-fabric/utilities";
26
+ import { EMPTY_OBJECT, type yup } from "@react-fabric/utilities";
27
27
  import {
28
28
  useCallback,
29
29
  useEffect,
@@ -40,6 +40,12 @@ import {
40
40
  type FormState,
41
41
  } from "react-hook-form";
42
42
 
43
+ type NestedKeyOf<T extends object> = {
44
+ [K in keyof T & (string | number)]: T[K] extends object
45
+ ? `${K}` | `${K}.${NestedKeyOf<T[K]>}`
46
+ : K;
47
+ }[keyof T & (string | number)];
48
+
43
49
  export interface FormRef<K extends KeyValue> {
44
50
  state: FormState<K>;
45
51
  reset: () => void;
@@ -47,7 +53,7 @@ export interface FormRef<K extends KeyValue> {
47
53
  submit: () => void;
48
54
  validate: () => Promise<boolean>;
49
55
  getValues: () => K;
50
- setValue: (key: keyof K, value: AnyObject) => void;
56
+ setValue: (key: NestedKeyOf<K>, value: AnyObject) => void;
51
57
  setValues: (values: K) => void;
52
58
  }
53
59
 
@@ -73,11 +79,42 @@ export interface FormProps<K extends KeyValue = KeyValue> {
73
79
 
74
80
  const DEFAULT_SUBMIT = () => undefined;
75
81
 
82
+ /**
83
+ * Form component for managing form state and validation using react-hook-form and yup.
84
+ * It provides a simple interface for form handling, including submission, validation, and value management.
85
+ * It supports nested form structures and allows for custom validation schemas using Yup.
86
+ *
87
+ * @param {FormProps<K>} props - The properties for the Form component.
88
+ * @returns {JSX.Element} The rendered Form component.
89
+ *
90
+ * @example
91
+ * ```jsx
92
+ * <Form
93
+ * formRef={formRef}
94
+ * schema={yup.object().shape({
95
+ * name: yup.string().required("Name is required"),
96
+ * email: yup.string().email("Invalid email").required("Email is required"),
97
+ * })}
98
+ * defaultValues={{ name: "", email: "" }}
99
+ * onSubmit={(values) => {
100
+ * console.log("Form submitted with values:", values);
101
+ * }}
102
+ * >
103
+ * <input name="name" ref={form.register} placeholder="Name" />
104
+ * <input name="email" ref={form.register} placeholder="Email" />
105
+ * <button type="submit">Submit</button>
106
+ * </Form>
107
+ * ```
108
+ *
109
+ * @see {@link https://adarshpastakia.github.io/react-fabric/?path=/docs/form-form--docs} for the properties that can be passed to this component.
110
+ * @see {@link https://react-hook-form.com/docs/useform} for more details on the properties.
111
+ * @see {@link https://github.com/jquense/yup} for more details on the Yup validation schema.
112
+ */
76
113
  export const Form = <K extends KeyValue>({
77
114
  formRef,
78
115
  schema,
79
116
  children,
80
- defaultValues,
117
+ defaultValues = EMPTY_OBJECT as K,
81
118
  onSubmit = DEFAULT_SUBMIT,
82
119
  onChange,
83
120
  ...rest
@@ -90,7 +127,7 @@ export const Form = <K extends KeyValue>({
90
127
  });
91
128
 
92
129
  const [, startTransition] = useTransition();
93
- const changeHandler = useDebounce(onChange, [onChange]);
130
+ const changeHandler = useDebounce(onChange, [onChange], 300);
94
131
  useEffect(() => {
95
132
  const subscription = form.watch((value) => {
96
133
  startTransition(() => changeHandler?.(value as K));
@@ -150,7 +187,10 @@ export const Form = <K extends KeyValue>({
150
187
  <form
151
188
  ref={ref}
152
189
  onReset={() => handleReset(defaultValues)}
153
- onSubmit={form.handleSubmit(onSubmit, onInvalid) as AnyObject}
190
+ onSubmit={(e) => {
191
+ e.stopPropagation();
192
+ void form.handleSubmit(onSubmit, onInvalid)(e);
193
+ }}
154
194
  data-loading={form.formState.isSubmitting}
155
195
  className="contents"
156
196
  autoComplete="off"
@@ -57,6 +57,22 @@ type UploaderActions =
57
57
  | { type: "remove"; key: string }
58
58
  | { type: "abort"; key: string };
59
59
 
60
+ /**
61
+ * useFileUploader hook provides a way to handle file uploads
62
+ * with progress tracking, error handling, and file management.
63
+ * It uses a reducer to manage the state of uploaded files and their statuses.
64
+ *
65
+ * @param uploader - The function that handles the file upload.
66
+ * @param initialList - Initial list of files to be managed by the uploader.
67
+ * @param multiple - Whether to allow multiple file uploads or not.
68
+ * @returns An object containing methods to upload, remove, retry files,
69
+ * and the current state of the uploader including pending uploads and file list.
70
+ *
71
+ * @example
72
+ * ```jsx
73
+ * const { upload, remove, retry, state } = useFileUploader(uploader, initialList, multiple);
74
+ * ```
75
+ */
60
76
  export const useFileUploader = (
61
77
  uploader: UploadHandler,
62
78
  initialList?: string | FileInfo | FileInfo[],
@@ -21,38 +21,47 @@
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 { EmptyContent } from "@react-fabric/core";
24
25
  import { isEmpty, isNil, yup } from "@react-fabric/utilities";
25
26
  import { Fragment, useMemo } from "react";
26
27
  import { FormField } from "../builder/FormField";
27
- import { type FormSchema } from "../types/schema";
28
+ import { type BaseSelectProps } from "../types";
29
+ import { DATA_TYPES, type FormSchema, type SchemaDef } from "../types/schema";
28
30
  import { type UploadHandler } from "./useFileUploader";
29
31
 
30
- const makeYupField = (field: FormSchema) => {
31
- let yupField: AnyObject = yup.string();
32
- if (field.datatype === "schema") {
32
+ const makeYupField = (field: SchemaDef) => {
33
+ let yupField: AnyObject = yup.string().trim();
34
+ if (field.datatype === DATA_TYPES.SCHEMA) {
33
35
  const child: yup.ObjectShape = Object.fromEntries(
34
36
  field.schema.map((field) => [field.id, makeYupField(field)]),
35
37
  );
36
38
  yupField = yup.object(child);
37
39
  }
38
- if (field.datatype === "boolean") yupField = yup.boolean();
40
+ if (field.datatype === DATA_TYPES.BOOL) {
41
+ yupField = yup.boolean();
42
+ if (field.required)
43
+ yupField = yupField.test({
44
+ test: (value: boolean) => !!value,
45
+ });
46
+ }
39
47
  if (
40
- field.datatype === "decimal" ||
41
- field.datatype === "number" ||
42
- field.datatype === "range"
48
+ field.datatype === DATA_TYPES.NUMBER ||
49
+ field.datatype === DATA_TYPES.RANGE
43
50
  ) {
44
51
  yupField = yup.number();
45
52
  !isNil(field.min) && (yupField = yupField.min(field.min));
46
53
  !isNil(field.max) && (yupField = yupField.max(field.max));
47
54
 
48
- if (field.datatype === "range") yupField = yup.array(yupField);
55
+ if (field.datatype === DATA_TYPES.RANGE) yupField = yup.array(yupField);
49
56
  }
50
- if (field.datatype === "string") {
51
- field.options && (yupField = yupField.oneOf(field.options));
57
+ if (field.datatype === DATA_TYPES.STRING) {
58
+ field.options &&
59
+ !field.allowCustom &&
60
+ (yupField = yupField.oneOf(field.options));
52
61
  field.regex && (yupField = yupField.matches(new RegExp(field.regex)));
53
62
  }
54
63
 
55
- if (field.datatype === "file") {
64
+ if (field.datatype === DATA_TYPES.FILE) {
56
65
  yupField = yup
57
66
  .object({
58
67
  filename: yup.string().required(),
@@ -83,13 +92,43 @@ const makeYupField = (field: FormSchema) => {
83
92
  return yupField.label(field.label);
84
93
  };
85
94
 
86
- export const useFormBuilder = (
87
- schema: FormSchema[],
95
+ /**
96
+ * useFormBuilder hook to create a form definition based on a schema.
97
+ * It generates a Yup validation schema and a form definition component.
98
+ *
99
+ * @param {FormSchema} schema - The schema defining the form fields.
100
+ * @param {Object} options - Optional parameters for the form builder.
101
+ * @returns An object containing the form definition and the Yup validation schema.
102
+ *
103
+ * @example
104
+ * ```jsx
105
+ * const { formDef, schemaDef } = useFormBuilder(schema, {
106
+ * prefix: "form",
107
+ * inline: true,
108
+ * fileUrl: (path) => `/files/${path}`,
109
+ * uploadHandler: async (file) => {
110
+ * // handle file upload
111
+ * },
112
+ * optionLists: {
113
+ * // define option lists for select fields
114
+ * }
115
+ * * });
116
+ * <FormBuilder
117
+ * schema={schemaDef}
118
+ * onSubmit={(data) => console.log(data)}
119
+ * >
120
+ * {formDef}
121
+ * </FormBuilder>
122
+ * ```
123
+ */
124
+ export const useFormBuilder = <T extends AnyObject = string>(
125
+ schema: FormSchema,
88
126
  options: {
89
127
  prefix?: string;
90
128
  inline?: boolean;
91
129
  fileUrl?: (path: string) => string;
92
130
  uploadHandler?: UploadHandler;
131
+ optionLists?: KeyValue<BaseSelectProps<T>>;
93
132
  } = {},
94
133
  ) => {
95
134
  const filteredSchema = useMemo(
@@ -106,11 +145,12 @@ export const useFormBuilder = (
106
145
  <Fragment>
107
146
  {filteredSchema.map((field, idx) => (
108
147
  <div className="mb-4" key={field.id}>
109
- {field.datatype === "schema" ? null : (
148
+ {field.datatype === DATA_TYPES.SCHEMA ? null : (
110
149
  <FormField
111
150
  {...field}
112
151
  autoFocus={idx === 0}
113
152
  fileUrl={options.fileUrl}
153
+ optionLists={options.optionLists}
114
154
  uploadHandler={options.uploadHandler}
115
155
  inline={options.inline}
116
156
  prefix={options.prefix}
@@ -118,6 +158,9 @@ export const useFormBuilder = (
118
158
  )}
119
159
  </div>
120
160
  ))}
161
+ {filteredSchema.length === 0 && (
162
+ <EmptyContent message="No schema defined" />
163
+ )}
121
164
  </Fragment>
122
165
  );
123
166
 
package/src/i18n/ar.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "addArray": "اضافة عنصر",
3
3
  "password": {
4
- "show": "عرض كلمة المرور",
5
- "hide": "اخفاء كلمة المرور"
4
+ "hide": "اخفاء كلمة المرور",
5
+ "show": "عرض كلمة المرور"
6
6
  },
7
7
  "select": {
8
+ "createOption": "إنشاء خيار جديد ل ",
8
9
  "emptyList": "لا توجد خيارات متاحة",
9
- "notFound": "لم يتم العثور على خيارات ل ",
10
- "createOption": "إنشاء خيار جديد ل "
10
+ "notFound": "لم يتم العثور على خيارات ل "
11
11
  },
12
12
  "yup": {
13
13
  "arrayMax": "{{label}} حدد ما يصل {{max}} غرض",
14
14
  "arrayMin": "{{label}} اختر على الأقل {{min}} غرض",
15
+ "empty": "{{label}} يجب ألا يكون فارغًا",
15
16
  "invalid": "{{label}} نوع بيانات غير صالح",
16
17
  "invalidType": "{{label}} غير صالح",
17
18
  "numberMax": "{{label}} الحد الأقصى {{max}}",
package/src/i18n/en.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "addArray": "Add Item",
3
3
  "password": {
4
- "show": "Show password",
5
- "hide": "Hide password"
4
+ "hide": "Hide password",
5
+ "show": "Show password"
6
6
  },
7
7
  "select": {
8
+ "createOption": "Create new option for <0>{{query}}</0>",
8
9
  "emptyList": "No options available",
9
- "notFound": "No options found for <0>{{query}}</0>",
10
- "createOption": "Create new option for <0>{{query}}</0>"
10
+ "notFound": "No options found for <0>{{query}}</0>"
11
11
  },
12
12
  "yup": {
13
13
  "arrayMax": "{{label}} must contain upto {{max}} item",
14
14
  "arrayMin": "{{label}} must contain atleast {{min}} item",
15
- "invalid": "{{label}} invalid",
16
15
  "empty": "{{label}} must not be empty",
16
+ "invalid": "{{label}} invalid",
17
17
  "invalidType": "{{label}} invalid datatype",
18
18
  "numberMax": "{{label}} maximum {{max}}",
19
19
  "numberMin": "{{label}} minimum {{min}}",
package/src/index.ts CHANGED
@@ -26,15 +26,24 @@ import "./i18n";
26
26
  export { Controller } from "./form/Controller";
27
27
  export { Form, type FormRef } from "./form/Form";
28
28
 
29
+ export { SchemaEditor } from "./builder/SchemaEditor";
30
+
29
31
  export { useFileUploader } from "./hooks/useFileUploader";
30
32
  export { useFormBuilder } from "./hooks/useFormBuilder";
31
33
 
32
- export { type FormSchema } from "./types/schema";
34
+ export {
35
+ DATA_TYPES,
36
+ SCHEMA_CUSTOM_LIST,
37
+ type FormSchema,
38
+ type SchemaDef,
39
+ } from "./types/schema";
33
40
 
34
41
  export { ArrayInput } from "./input/ArrayInput";
42
+ export { AvatarInput } from "./input/AvatarInput";
35
43
  export { Checkbox } from "./input/Checkbox";
36
44
  export { ColorInput } from "./input/ColorInput";
37
45
  export { ColorPicker } from "./input/ColorPicker";
46
+ export { DateInput } from "./input/DateInput";
38
47
  export { Field } from "./input/Field";
39
48
  export { FileInput } from "./input/File";
40
49
  export { HiddenInput } from "./input/Hidden";
@@ -43,7 +52,7 @@ export { Number } from "./input/Number";
43
52
  export { Password } from "./input/Password";
44
53
  export { Radio } from "./input/Radio";
45
54
  export { RadioGroup } from "./input/RadioGroup";
46
- export { Range } from "./input/Range";
55
+ export { RangeSlider } from "./input/RangeSlider";
47
56
  export { Search } from "./input/Search";
48
57
  export { Slider } from "./input/Slider";
49
58
  export { Switch } from "./input/Switch";
@@ -23,27 +23,32 @@
23
23
 
24
24
  import { DndContext, type DragEndEvent } from "@dnd-kit/core";
25
25
  import { SortableContext, useSortable } from "@dnd-kit/sortable";
26
- import { Button, CoreIcons, Icon, Tooltip } from "@react-fabric/core";
27
- import { isString } from "@react-fabric/utilities";
26
+ import {
27
+ Button,
28
+ cloneChildren,
29
+ CoreIcons,
30
+ Icon,
31
+ Tooltip,
32
+ } from "@react-fabric/core";
33
+ import { isFalse, isString } from "@react-fabric/utilities";
28
34
  import classNames from "classnames";
29
35
  import {
30
- Children,
31
36
  cloneElement,
32
37
  useCallback,
33
38
  useImperativeHandle,
34
39
  useMemo,
35
40
  type PropsWithChildren,
36
41
  type ReactNode,
37
- type RefObject,
42
+ type Ref,
38
43
  } from "react";
39
44
  import { useFieldArray, useFormContext } from "react-hook-form";
40
45
  import { useTranslation } from "react-i18next";
41
46
  import { FieldWrapper } from "../internal/FieldWrapper";
42
- import classes from "./ArrayInput.module.css";
43
47
 
44
48
  import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
45
49
  import { CSS } from "@dnd-kit/utilities";
46
- export interface ArrayInputProps {
50
+
51
+ export interface ArrayInputProps<T extends AnyObject = string> {
47
52
  /**
48
53
  * input label
49
54
  */
@@ -77,6 +82,10 @@ export interface ArrayInputProps {
77
82
  * minimum required length
78
83
  */
79
84
  minItems?: number;
85
+ /**
86
+ * maximum length
87
+ */
88
+ maxItems?: number;
80
89
  /**
81
90
  * field name to focus on add
82
91
  */
@@ -92,34 +101,104 @@ export interface ArrayInputProps {
92
101
  * add item button label
93
102
  */
94
103
  addLabel?: string;
104
+ buttonPosition?: "top" | "bottom" | "both";
95
105
  /**
96
106
  * fixed length list
97
107
  */
98
108
  fixedList?: boolean;
99
- arrayRef?: RefObject<{
100
- addItem: (item: AnyObject) => void;
109
+ arrayRef?: Ref<{
110
+ addItem: (item: T) => void;
101
111
  removeItem: (idx: number) => void;
102
112
  }>;
103
113
  /**
104
- * Add item
114
+ * add new item
105
115
  */
106
116
  onAdd?: () => AnyObject;
117
+ /**
118
+ * on item remove
119
+ */
120
+ onRemove?: (item: T, idx: number) => void;
121
+ /**
122
+ * before item remove
123
+ */
124
+ onBeforeRemove?: (item: T, idx: number) => Promise<boolean> | boolean;
125
+ /**
126
+ * can remove item
127
+ */
128
+ canRemove?: (props: { item: T; index: number; lastItem: boolean }) => boolean;
129
+
130
+ /**
131
+ * field width
132
+ */
133
+ width?: number | string;
134
+ /**
135
+ * inline label and input
136
+ */
137
+ inline?: boolean;
138
+ /**
139
+ * label width for inline
140
+ */
141
+ labelWidth?: string;
107
142
  }
108
143
 
109
- export const ArrayInput = ({
144
+ /**
145
+ * ArrayInput component for managing an array of items in a form.
146
+ * It allows adding, removing, and sorting items within the array.
147
+ * It integrates with react-hook-form for form state management and validation.
148
+ * It supports fixed-length lists, custom add/remove handlers, and can display validation errors.
149
+ * It also provides options for inline display and custom button positioning.
150
+ *
151
+ * It uses the `useFieldArray` hook from `react-hook-form` to manage the array state and provides a simple interface for adding and removing items.
152
+ *
153
+ * It also supports drag-and-drop sorting using `@dnd-kit/core` and `@dnd-kit/sortable` for a better user experience when reordering items.
154
+ *
155
+ * @param {ArrayInputProps<T>} props - The properties for the ArrayInput component.
156
+ * @returns {JSX.Element} The rendered ArrayInput component.
157
+ *
158
+ * @example
159
+ * ```jsx
160
+ * <ArrayInput
161
+ * name="items"
162
+ * label="Items"
163
+ * addLabel="Add Item"
164
+ * onAdd={() => ({ name: "", value: "" })}
165
+ * onRemove={(item, index) => console.log("Removed item:", item, "at index:", index)}
166
+ * onBeforeRemove={(item, index) => confirm(`Are you sure you want to remove item at index ${index}?`)}
167
+ * canRemove={({ item, index, lastItem }) => !lastItem} // Prevent removing the last item
168
+ * >
169
+ * {({ index, name }) => (
170
+ * <Input
171
+ * name={name}
172
+ * label={`Item ${index + 1}`}
173
+ * placeholder="Enter item value"
174
+ * required={index === 0} // Make the first item required
175
+ * />
176
+ * )}
177
+ * </ArrayInput>
178
+ * ```
179
+ *
180
+ * @see {@link https://react-hook-form.com/api/usefieldarray} for more details on `useFieldArray`.
181
+ * @see {@link https://react-dnd.github.io/react-dnd/about} for more details on DnD context and sorting.
182
+ */
183
+ export const ArrayInput = <T extends AnyObject = string>({
110
184
  name,
111
185
  children,
112
186
  addLabel,
113
187
  fixedList,
114
188
  focusName = "",
189
+ buttonPosition = "bottom",
115
190
  onAdd,
191
+ onRemove,
192
+ onBeforeRemove,
193
+ canRemove,
116
194
  arrayRef,
117
195
  enableSorting,
118
196
  disabled = false,
119
197
  readOnly = false,
120
198
  minItems = 0,
199
+ maxItems = Number.MAX_SAFE_INTEGER,
121
200
  ...rest
122
- }: ArrayInputProps) => {
201
+ }: ArrayInputProps<T>) => {
123
202
  const { t } = useTranslation("form");
124
203
  const form = useFormContext();
125
204
 
@@ -128,6 +207,7 @@ export const ArrayInput = ({
128
207
 
129
208
  const { fields, append, remove, move } = useFieldArray({
130
209
  name,
210
+ keyName: "__ID__",
131
211
  control: form.control,
132
212
  });
133
213
 
@@ -139,7 +219,7 @@ export const ArrayInput = ({
139
219
  );
140
220
 
141
221
  const handleAdd = useCallback(
142
- (item: AnyObject) => {
222
+ (item: T) => {
143
223
  append(item);
144
224
  setTimeout(() => {
145
225
  form.setFocus(
@@ -150,6 +230,19 @@ export const ArrayInput = ({
150
230
  [name, fields, focusName],
151
231
  );
152
232
 
233
+ const handleRemove = useCallback(
234
+ (item: T, index: number) => {
235
+ const ret = onBeforeRemove?.(item, index);
236
+ void Promise.resolve(ret).then((b) => {
237
+ if (!isFalse(b)) {
238
+ remove(index);
239
+ onRemove?.(item, index);
240
+ }
241
+ });
242
+ },
243
+ [remove, onRemove, onBeforeRemove],
244
+ );
245
+
153
246
  useImperativeHandle(arrayRef, () => ({
154
247
  addItem: handleAdd,
155
248
  removeItem: remove,
@@ -159,7 +252,7 @@ export const ArrayInput = ({
159
252
  ({ children }: PropsWithChildren) => {
160
253
  if (enableSorting) {
161
254
  // pass id list to dnd context
162
- const idMap = fields.map((item) => item.id);
255
+ const idMap = fields.map((item) => item.__ID__);
163
256
  const handleDragEnd = (e: DragEndEvent) => {
164
257
  if (e.over && e.active.id !== e.over.id) {
165
258
  // find index of item and drop over
@@ -227,14 +320,60 @@ export const ArrayInput = ({
227
320
  // @ts-expect-error ignore
228
321
  SortableItem.displayName = "SortableItem";
229
322
 
323
+ const addButton = useMemo(
324
+ () => (
325
+ <div className="flex justify-end">
326
+ <Tooltip
327
+ color="danger-200"
328
+ content={
329
+ isString(error)
330
+ ? `${error}`
331
+ : (t(
332
+ error?.key,
333
+ error?.values
334
+ ? {
335
+ ...error?.values,
336
+ label: t(
337
+ error.values?.label ?? "form:badkey",
338
+ error.values?.path,
339
+ ),
340
+ }
341
+ : {},
342
+ ) as string)
343
+ }
344
+ >
345
+ <Button
346
+ size="sm"
347
+ aria-label="Add item"
348
+ icon={CoreIcons.insert}
349
+ onClick={() => handleAdd(onAdd?.())}
350
+ data-invalid={!!error}
351
+ disabled={!!disabled || readOnly || fields.length > maxItems}
352
+ className={classNames("fabric-addButton", "me-10")}
353
+ >
354
+ {addLabel ?? t("addArray")}
355
+ </Button>
356
+ </Tooltip>
357
+ </div>
358
+ ),
359
+ [error, fields, handleAdd, disabled, readOnly, addLabel, maxItems],
360
+ );
361
+
230
362
  return (
231
- <div className={classes.arrayInput}>
232
- <FieldWrapper {...rest}>
363
+ <div className={"fabric-arrayInput"}>
364
+ <FieldWrapper
365
+ {...rest}
366
+ appendLabel={
367
+ <div className="-me-2">
368
+ {!fixedList && onAdd && buttonPosition !== "bottom" && addButton}
369
+ </div>
370
+ }
371
+ >
233
372
  <Wrapper>
234
373
  {fields.map((item, index) => (
235
374
  <SortableItem
236
- key={item.id}
237
- id={item.id}
375
+ key={item.__ID__}
376
+ id={item.__ID__}
238
377
  className="flex items-center flex-nowrap gap-2 mb-1"
239
378
  >
240
379
  {typeof children === "function"
@@ -242,7 +381,7 @@ export const ArrayInput = ({
242
381
  index,
243
382
  name: `${name}.${index}`,
244
383
  })
245
- : Children.map(children, (child: AnyObject) =>
384
+ : cloneChildren(children, (child: AnyObject) =>
246
385
  cloneElement(
247
386
  child,
248
387
  Object.assign(
@@ -271,48 +410,24 @@ export const ArrayInput = ({
271
410
  color="danger"
272
411
  variant="link"
273
412
  className="self-end"
274
- disabled={disabled || readOnly || fields.length <= minItems}
275
- onClick={() => remove(index)}
413
+ disabled={
414
+ disabled ||
415
+ readOnly ||
416
+ fields.length <= minItems ||
417
+ canRemove?.({
418
+ item: item as T,
419
+ index,
420
+ lastItem: index + 1 === fields.length,
421
+ }) !== false
422
+ }
423
+ onClick={() => handleRemove(item as T, index)}
276
424
  />
277
425
  )}
278
426
  </SortableItem>
279
427
  ))}
280
428
  </Wrapper>
281
429
  </FieldWrapper>
282
- {!fixedList && onAdd && (
283
- <div className="flex justify-end">
284
- <Tooltip
285
- color="danger"
286
- content={
287
- isString(error)
288
- ? `${error}`
289
- : (t(
290
- error?.key,
291
- error?.values
292
- ? {
293
- ...error?.values,
294
- label: t(
295
- error.values?.label ?? "form:badkey",
296
- error.values?.path,
297
- ),
298
- }
299
- : {},
300
- ) as string)
301
- }
302
- >
303
- <Button
304
- aria-label="Add item"
305
- icon={CoreIcons.insert}
306
- onClick={() => handleAdd(onAdd())}
307
- data-invalid={!!error}
308
- disabled={!!disabled || readOnly}
309
- className={classNames(classes.addButton, "me-10")}
310
- >
311
- {addLabel ?? t("addArray")}
312
- </Button>
313
- </Tooltip>
314
- </div>
315
- )}
430
+ {!fixedList && onAdd && buttonPosition !== "top" && addButton}
316
431
  </div>
317
432
  );
318
433
  };