@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.
- package/{src/input/ArrayInput.module.css → css/_arrayInput.css} +8 -8
- package/css/_colorPicker.css +65 -0
- package/{src/internal/Field.module.css → css/_field.css} +64 -29
- package/{src/input/File.module.css → css/_file.css} +21 -6
- package/css/styles.css +27 -0
- package/dist/cjs/builder/FileField.js +37 -25
- package/dist/cjs/builder/FormField.js +50 -34
- package/dist/cjs/builder/SchemaEditor.js +72 -0
- package/dist/cjs/builder/components/AddSchemaFields.js +62 -0
- package/dist/cjs/builder/components/SchemaArrayForm.js +143 -0
- package/dist/cjs/builder/components/SchemaField.js +414 -0
- package/dist/cjs/form/Controller.js +46 -1
- package/dist/cjs/form/Form.js +43 -6
- package/dist/cjs/hooks/useFileUploader.js +16 -0
- package/dist/cjs/hooks/useFormBuilder.js +57 -15
- package/dist/cjs/i18n/ar.json.js +5 -4
- package/dist/cjs/i18n/en.json.js +5 -5
- package/dist/cjs/index.js +11 -2
- package/dist/cjs/input/ArrayInput.js +100 -33
- package/dist/cjs/input/AvatarInput.js +17 -8
- package/dist/cjs/input/Checkbox.js +15 -5
- package/dist/cjs/input/ColorInput.js +108 -56
- package/dist/cjs/input/ColorPicker.js +11 -35
- package/dist/cjs/input/DateInput.js +195 -0
- package/dist/cjs/input/Field.js +10 -4
- package/dist/cjs/input/File.js +14 -7
- package/dist/cjs/input/Hidden.js +11 -1
- package/dist/cjs/input/Input.js +19 -7
- package/dist/cjs/input/Number.js +17 -7
- package/dist/cjs/input/Password.js +16 -8
- package/dist/cjs/input/Radio.js +11 -2
- package/dist/cjs/input/RadioGroup.js +14 -3
- package/dist/cjs/input/{Range.js → RangeSlider.js} +14 -6
- package/dist/cjs/input/Search.js +25 -16
- package/dist/cjs/input/Slider.js +10 -4
- package/dist/cjs/input/Switch.js +39 -7
- package/dist/cjs/input/Textarea.js +23 -4
- package/dist/cjs/internal/ErrorIcon.js +1 -1
- package/dist/cjs/internal/FieldWrapper.js +11 -4
- package/dist/cjs/internal/InputWrapper.js +19 -16
- package/dist/cjs/select/AutoComplete.js +19 -8
- package/dist/cjs/select/DualList.js +15 -1
- package/dist/cjs/select/List.js +45 -20
- package/dist/cjs/select/Options.js +2 -2
- package/dist/cjs/select/Select.js +14 -4
- package/dist/cjs/select/useSelect.js +10 -6
- package/dist/cjs/types/schema.js +43 -0
- package/dist/cjs/utils/eventHandlers.js +11 -5
- package/dist/cjs/utils/getColor.js +6 -0
- package/dist/esm/builder/FileField.js +39 -27
- package/dist/esm/builder/FormField.js +50 -34
- package/dist/esm/builder/SchemaEditor.js +72 -0
- package/dist/esm/builder/components/AddSchemaFields.js +62 -0
- package/dist/esm/builder/components/SchemaArrayForm.js +143 -0
- package/dist/esm/builder/components/SchemaField.js +414 -0
- package/dist/esm/form/Controller.js +46 -1
- package/dist/esm/form/Form.js +43 -6
- package/dist/esm/hooks/useFileUploader.js +16 -0
- package/dist/esm/hooks/useFormBuilder.js +55 -13
- package/dist/esm/i18n/ar.json.js +5 -4
- package/dist/esm/i18n/en.json.js +5 -5
- package/dist/esm/index.js +5 -1
- package/dist/esm/input/ArrayInput.js +100 -33
- package/dist/esm/input/AvatarInput.js +17 -8
- package/dist/esm/input/Checkbox.js +15 -5
- package/dist/esm/input/ColorInput.js +110 -58
- package/dist/esm/input/ColorPicker.js +11 -35
- package/dist/esm/input/DateInput.js +195 -0
- package/dist/esm/input/Field.js +11 -5
- package/dist/esm/input/File.js +16 -9
- package/dist/esm/input/Hidden.js +11 -1
- package/dist/esm/input/Input.js +21 -9
- package/dist/esm/input/Number.js +19 -9
- package/dist/esm/input/Password.js +18 -10
- package/dist/esm/input/Radio.js +11 -2
- package/dist/esm/input/RadioGroup.js +15 -4
- package/dist/esm/input/{Range.js → RangeSlider.js} +14 -6
- package/dist/esm/input/Search.js +27 -18
- package/dist/esm/input/Slider.js +10 -4
- package/dist/esm/input/Switch.js +41 -9
- package/dist/esm/input/Textarea.js +24 -5
- package/dist/esm/internal/ErrorIcon.js +1 -1
- package/dist/esm/internal/FieldWrapper.js +11 -4
- package/dist/esm/internal/InputWrapper.js +19 -16
- package/dist/esm/select/AutoComplete.js +19 -8
- package/dist/esm/select/DualList.js +15 -1
- package/dist/esm/select/List.js +46 -21
- package/dist/esm/select/Options.js +2 -2
- package/dist/esm/select/Select.js +14 -4
- package/dist/esm/select/useSelect.js +11 -7
- package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
- package/dist/esm/utils/eventHandlers.js +11 -5
- package/dist/esm/utils/getColor.js +6 -1
- package/dist/types/builder/FileField.d.ts +4 -2
- package/dist/types/builder/FormField.d.ts +3 -1
- package/dist/types/builder/SchemaEditor.d.ts +9 -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 +10 -0
- package/dist/types/form/Controller.d.ts +47 -2
- package/dist/types/form/Form.d.ts +37 -2
- package/dist/types/hooks/useFileUploader.d.ts +16 -0
- package/dist/types/hooks/useFormBuilder.d.ts +32 -1
- package/dist/types/index.d.ts +5 -2
- package/dist/types/input/ArrayInput.d.ts +78 -6
- package/dist/types/input/AvatarInput.d.ts +14 -3
- package/dist/types/input/Checkbox.d.ts +13 -4
- package/dist/types/input/ColorInput.d.ts +15 -2
- package/dist/types/input/ColorPicker.d.ts +11 -1
- package/dist/types/input/DateInput.d.ts +28 -0
- package/dist/types/input/Field.d.ts +5 -1
- package/dist/types/input/File.d.ts +9 -0
- package/dist/types/input/Hidden.d.ts +8 -0
- package/dist/types/input/Input.d.ts +9 -1
- package/dist/types/input/Number.d.ts +9 -1
- package/dist/types/input/Password.d.ts +7 -1
- package/dist/types/input/Radio.d.ts +9 -1
- package/dist/types/input/RadioGroup.d.ts +26 -3
- package/dist/types/input/{Range.d.ts → RangeSlider.d.ts} +10 -2
- package/dist/types/input/Search.d.ts +14 -1
- package/dist/types/input/Slider.d.ts +7 -1
- package/dist/types/input/Switch.d.ts +16 -4
- package/dist/types/input/Textarea.d.ts +15 -1
- package/dist/types/internal/FieldWrapper.d.ts +1 -1
- package/dist/types/internal/InputWrapper.d.ts +1 -1
- package/dist/types/ranger/index.d.ts +2 -2
- package/dist/types/select/AutoComplete.d.ts +8 -1
- package/dist/types/select/DualList.d.ts +17 -2
- package/dist/types/select/List.d.ts +9 -1
- package/dist/types/select/Select.d.ts +9 -1
- package/dist/types/select/useSelect.d.ts +7 -6
- package/dist/types/types/index.d.ts +28 -7
- package/dist/types/types/schema.d.ts +78 -33
- package/dist/types/utils/eventHandlers.d.ts +1 -1
- package/package.json +15 -12
- package/src/builder/FileField.tsx +53 -27
- package/src/builder/FormField.tsx +52 -32
- package/src/builder/SchemaEditor.tsx +79 -0
- package/src/builder/components/AddSchemaFields.tsx +74 -0
- package/src/builder/components/SchemaArrayForm.tsx +158 -0
- package/src/builder/components/SchemaField.tsx +436 -0
- package/src/form/Controller.tsx +48 -3
- package/src/form/Form.tsx +45 -5
- package/src/hooks/useFileUploader.ts +16 -0
- package/src/hooks/useFormBuilder.tsx +58 -15
- package/src/i18n/ar.json +5 -4
- package/src/i18n/en.json +5 -5
- package/src/index.ts +11 -2
- package/src/input/ArrayInput.tsx +169 -54
- package/src/input/AvatarInput.tsx +20 -8
- package/src/input/Checkbox.tsx +15 -5
- package/src/input/ColorInput.tsx +126 -69
- package/src/input/ColorPicker.tsx +12 -36
- package/src/input/DateInput.tsx +259 -0
- package/src/input/Field.tsx +15 -11
- package/src/input/File.tsx +16 -14
- package/src/input/Hidden.tsx +12 -1
- package/src/input/Input.tsx +23 -10
- package/src/input/Number.tsx +18 -10
- package/src/input/Password.tsx +19 -12
- package/src/input/Radio.tsx +11 -2
- package/src/input/RadioGroup.tsx +35 -4
- package/src/input/{Range.tsx → RangeSlider.tsx} +13 -5
- package/src/input/Search.tsx +29 -19
- package/src/input/Slider.tsx +10 -4
- package/src/input/Switch.tsx +52 -7
- package/src/input/Textarea.tsx +29 -5
- package/src/internal/ErrorIcon.tsx +1 -1
- package/src/internal/FieldWrapper.tsx +8 -4
- package/src/internal/InputWrapper.tsx +36 -20
- package/src/select/AutoComplete.tsx +12 -1
- package/src/select/DualList.tsx +19 -0
- package/src/select/List.tsx +49 -8
- package/src/select/Options.tsx +1 -1
- package/src/select/Select.tsx +21 -3
- package/src/select/useSelect.tsx +13 -5
- package/src/types/index.ts +34 -8
- package/src/types/schema.ts +96 -41
- package/src/utils/eventHandlers.ts +9 -3
- 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
|
@@ -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:
|
|
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={
|
|
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
|
|
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:
|
|
31
|
-
let yupField: AnyObject = yup.string();
|
|
32
|
-
if (field.datatype ===
|
|
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 ===
|
|
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 ===
|
|
41
|
-
field.datatype ===
|
|
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 ===
|
|
55
|
+
if (field.datatype === DATA_TYPES.RANGE) yupField = yup.array(yupField);
|
|
49
56
|
}
|
|
50
|
-
if (field.datatype ===
|
|
51
|
-
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 ===
|
|
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
|
-
|
|
87
|
-
|
|
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 ===
|
|
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
|
-
"
|
|
5
|
-
"
|
|
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
|
-
"
|
|
5
|
-
"
|
|
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 {
|
|
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 {
|
|
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";
|
package/src/input/ArrayInput.tsx
CHANGED
|
@@ -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 {
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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?:
|
|
100
|
-
addItem: (item:
|
|
109
|
+
arrayRef?: Ref<{
|
|
110
|
+
addItem: (item: T) => void;
|
|
101
111
|
removeItem: (idx: number) => void;
|
|
102
112
|
}>;
|
|
103
113
|
/**
|
|
104
|
-
*
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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={
|
|
232
|
-
<FieldWrapper
|
|
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.
|
|
237
|
-
id={item.
|
|
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
|
-
:
|
|
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={
|
|
275
|
-
|
|
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
|
};
|