@react-fabric/form 1.0.0-beta.6 → 1.0.0-beta.60
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} +66 -30
- package/{src/input/File.module.css → css/_file.css} +21 -6
- package/css/styles.css +27 -0
- package/dist/cjs/builder/FileField.js +40 -28
- 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 +145 -0
- package/dist/cjs/builder/components/SchemaField.js +414 -0
- package/dist/cjs/form/Controller.js +46 -1
- package/dist/cjs/form/Form.js +70 -9
- 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 +12 -2
- package/dist/cjs/input/ArrayInput.js +126 -36
- package/dist/cjs/input/AvatarInput.js +43 -23
- package/dist/cjs/input/Checkbox.js +16 -6
- package/dist/cjs/input/ColorInput.js +113 -58
- package/dist/cjs/input/ColorPicker.js +60 -76
- package/dist/cjs/input/DateInput.js +197 -0
- package/dist/cjs/input/Field.js +10 -4
- package/dist/cjs/input/File.js +14 -7
- package/dist/cjs/input/Hidden.js +13 -2
- package/dist/cjs/input/Input.js +24 -10
- package/dist/cjs/input/Number.js +22 -10
- package/dist/cjs/input/Password.js +22 -12
- package/dist/cjs/input/Radio.js +12 -3
- package/dist/cjs/input/RadioGroup.js +10 -1
- package/dist/cjs/input/{Range.js → RangeSlider.js} +16 -8
- package/dist/cjs/input/Search.js +30 -19
- package/dist/cjs/input/Slider.js +12 -6
- package/dist/cjs/input/Switch.js +20 -9
- package/dist/cjs/input/Textarea.js +29 -8
- package/dist/cjs/internal/ErrorIcon.js +1 -1
- package/dist/cjs/internal/FieldWrapper.js +11 -4
- package/dist/cjs/internal/InputWrapper.js +27 -20
- package/dist/cjs/select/AutoComplete.js +21 -11
- package/dist/cjs/select/DualList.js +34 -12
- package/dist/cjs/select/List.js +68 -22
- package/dist/cjs/select/Options.js +49 -42
- package/dist/cjs/select/Select.js +40 -10
- package/dist/cjs/select/useSelect.js +22 -11
- package/dist/cjs/types/schema.js +43 -0
- package/dist/cjs/utils/eventHandlers.js +15 -5
- package/dist/esm/builder/FileField.js +42 -30
- 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 +145 -0
- package/dist/esm/builder/components/SchemaField.js +414 -0
- package/dist/esm/form/Controller.js +46 -1
- package/dist/esm/form/Form.js +72 -12
- 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 +6 -2
- package/dist/esm/input/ArrayInput.js +126 -36
- package/dist/esm/input/AvatarInput.js +44 -24
- package/dist/esm/input/Checkbox.js +16 -6
- package/dist/esm/input/ColorInput.js +115 -60
- package/dist/esm/input/ColorPicker.js +60 -76
- package/dist/esm/input/DateInput.js +197 -0
- package/dist/esm/input/Field.js +11 -5
- package/dist/esm/input/File.js +16 -9
- package/dist/esm/input/Hidden.js +13 -2
- package/dist/esm/input/Input.js +26 -12
- package/dist/esm/input/Number.js +24 -12
- package/dist/esm/input/Password.js +24 -14
- package/dist/esm/input/Radio.js +12 -3
- package/dist/esm/input/RadioGroup.js +11 -2
- package/dist/esm/input/{Range.js → RangeSlider.js} +16 -8
- package/dist/esm/input/Search.js +32 -21
- package/dist/esm/input/Slider.js +12 -6
- package/dist/esm/input/Switch.js +20 -9
- package/dist/esm/input/Textarea.js +30 -9
- package/dist/esm/internal/ErrorIcon.js +1 -1
- package/dist/esm/internal/FieldWrapper.js +11 -4
- package/dist/esm/internal/InputWrapper.js +27 -20
- package/dist/esm/select/AutoComplete.js +21 -11
- package/dist/esm/select/DualList.js +34 -12
- package/dist/esm/select/List.js +69 -23
- package/dist/esm/select/Options.js +49 -42
- package/dist/esm/select/Select.js +40 -10
- package/dist/esm/select/useSelect.js +23 -12
- package/{src/input/ColorPicker.module.css → dist/esm/types/schema.js} +18 -27
- package/dist/esm/utils/eventHandlers.js +15 -5
- 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 +48 -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 +6 -3
- package/dist/types/input/ArrayInput.d.ts +69 -12
- package/dist/types/input/AvatarInput.d.ts +23 -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 +13 -1
- 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 +8 -1
- package/dist/types/input/RangeSlider.d.ts +67 -0
- package/dist/types/input/Search.d.ts +14 -1
- package/dist/types/input/Slider.d.ts +8 -2
- package/dist/types/input/Switch.d.ts +10 -2
- 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/Options.d.ts +4 -3
- package/dist/types/select/Select.d.ts +9 -1
- package/dist/types/select/useSelect.d.ts +10 -6
- package/dist/types/types/index.d.ts +34 -5
- package/dist/types/types/schema.d.ts +78 -33
- package/dist/types/utils/eventHandlers.d.ts +2 -2
- package/package.json +22 -20
- package/src/builder/FileField.tsx +62 -30
- 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 +161 -0
- package/src/builder/components/SchemaField.tsx +436 -0
- package/src/form/Controller.tsx +48 -3
- package/src/form/Form.tsx +80 -8
- 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 +12 -3
- package/src/input/ArrayInput.tsx +199 -64
- package/src/input/AvatarInput.tsx +67 -19
- package/src/input/Checkbox.tsx +16 -6
- package/src/input/ColorInput.tsx +131 -71
- package/src/input/ColorPicker.tsx +54 -72
- package/src/input/DateInput.tsx +260 -0
- package/src/input/Field.tsx +15 -11
- package/src/input/File.tsx +16 -14
- package/src/input/Hidden.tsx +17 -2
- package/src/input/Input.tsx +27 -13
- package/src/input/Number.tsx +22 -13
- package/src/input/Password.tsx +24 -16
- package/src/input/Radio.tsx +12 -3
- package/src/input/RadioGroup.tsx +12 -3
- package/src/input/{Range.tsx → RangeSlider.tsx} +17 -9
- package/src/input/Search.tsx +33 -22
- package/src/input/Slider.tsx +20 -11
- package/src/input/Switch.tsx +17 -6
- package/src/input/Textarea.tsx +34 -9
- package/src/internal/ErrorIcon.tsx +1 -1
- package/src/internal/FieldWrapper.tsx +8 -4
- package/src/internal/InputWrapper.tsx +49 -27
- package/src/select/AutoComplete.tsx +14 -3
- package/src/select/DualList.tsx +31 -16
- package/src/select/List.tsx +52 -8
- package/src/select/Options.tsx +50 -46
- package/src/select/Select.tsx +25 -5
- package/src/select/useSelect.tsx +26 -9
- package/src/types/index.ts +37 -6
- package/src/types/schema.ts +96 -41
- package/src/utils/eventHandlers.ts +14 -4
- 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/dist/types/input/Range.d.ts +0 -59
|
@@ -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
|
@@ -24,17 +24,26 @@
|
|
|
24
24
|
import "./i18n";
|
|
25
25
|
|
|
26
26
|
export { Controller } from "./form/Controller";
|
|
27
|
-
export { Form, type FormRef } from "./form/Form";
|
|
27
|
+
export { Form, useForm, type FormRef } from "./form/Form";
|
|
28
|
+
|
|
29
|
+
export { SchemaEditor } from "./builder/SchemaEditor";
|
|
28
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,33 @@
|
|
|
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,
|
|
38
|
+
useEffect,
|
|
33
39
|
useImperativeHandle,
|
|
34
40
|
useMemo,
|
|
41
|
+
useState,
|
|
35
42
|
type PropsWithChildren,
|
|
36
|
-
type
|
|
37
|
-
type RefObject,
|
|
43
|
+
type Ref,
|
|
38
44
|
} from "react";
|
|
39
45
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
|
40
46
|
import { useTranslation } from "react-i18next";
|
|
41
47
|
import { FieldWrapper } from "../internal/FieldWrapper";
|
|
42
|
-
import classes from "./ArrayInput.module.css";
|
|
43
48
|
|
|
44
49
|
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
45
50
|
import { CSS } from "@dnd-kit/utilities";
|
|
46
|
-
|
|
51
|
+
|
|
52
|
+
export interface ArrayInputProps<T extends AnyObject = string> {
|
|
47
53
|
/**
|
|
48
54
|
* input label
|
|
49
55
|
*/
|
|
@@ -77,6 +83,10 @@ export interface ArrayInputProps {
|
|
|
77
83
|
* minimum required length
|
|
78
84
|
*/
|
|
79
85
|
minItems?: number;
|
|
86
|
+
/**
|
|
87
|
+
* maximum length
|
|
88
|
+
*/
|
|
89
|
+
maxItems?: number;
|
|
80
90
|
/**
|
|
81
91
|
* field name to focus on add
|
|
82
92
|
*/
|
|
@@ -87,23 +97,35 @@ export interface ArrayInputProps {
|
|
|
87
97
|
enableSorting?: boolean;
|
|
88
98
|
children:
|
|
89
99
|
| React.ReactNode
|
|
90
|
-
| ((props: { index: number; name: string }) => ReactNode);
|
|
100
|
+
| ((props: { index: number; name: string; item: T }) => React.ReactNode);
|
|
91
101
|
/**
|
|
92
102
|
* add item button label
|
|
93
103
|
*/
|
|
94
104
|
addLabel?: string;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
fixedList?: boolean;
|
|
99
|
-
arrayRef?: RefObject<{
|
|
100
|
-
addItem: (item: AnyObject) => void;
|
|
105
|
+
buttonPosition?: "top" | "bottom" | "both";
|
|
106
|
+
arrayRef?: Ref<{
|
|
107
|
+
addItem: (item: T) => void;
|
|
101
108
|
removeItem: (idx: number) => void;
|
|
102
109
|
}>;
|
|
103
110
|
/**
|
|
104
|
-
*
|
|
111
|
+
* add new item
|
|
112
|
+
*/
|
|
113
|
+
onAdd?: () => T;
|
|
114
|
+
/**
|
|
115
|
+
* on item remove
|
|
105
116
|
*/
|
|
106
|
-
|
|
117
|
+
onRemove?: (item: T, idx: number) => void;
|
|
118
|
+
/**
|
|
119
|
+
* before item remove
|
|
120
|
+
*/
|
|
121
|
+
onBeforeRemove?: (item: T, idx: number) => Promise<boolean> | boolean;
|
|
122
|
+
/**
|
|
123
|
+
* can remove item
|
|
124
|
+
*/
|
|
125
|
+
canRemove?:
|
|
126
|
+
| false
|
|
127
|
+
| "newonly"
|
|
128
|
+
| ((props: { item: T; index: number; lastItem: boolean }) => boolean);
|
|
107
129
|
|
|
108
130
|
/**
|
|
109
131
|
* field width
|
|
@@ -119,20 +141,63 @@ export interface ArrayInputProps {
|
|
|
119
141
|
labelWidth?: string;
|
|
120
142
|
}
|
|
121
143
|
|
|
122
|
-
|
|
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>({
|
|
123
184
|
name,
|
|
124
185
|
children,
|
|
125
186
|
addLabel,
|
|
126
|
-
fixedList,
|
|
127
187
|
focusName = "",
|
|
188
|
+
buttonPosition = "bottom",
|
|
128
189
|
onAdd,
|
|
190
|
+
onRemove,
|
|
191
|
+
onBeforeRemove,
|
|
192
|
+
canRemove,
|
|
129
193
|
arrayRef,
|
|
130
194
|
enableSorting,
|
|
131
195
|
disabled = false,
|
|
132
196
|
readOnly = false,
|
|
133
197
|
minItems = 0,
|
|
198
|
+
maxItems = Number.MAX_SAFE_INTEGER,
|
|
134
199
|
...rest
|
|
135
|
-
}: ArrayInputProps) => {
|
|
200
|
+
}: ArrayInputProps<T>) => {
|
|
136
201
|
const { t } = useTranslation("form");
|
|
137
202
|
const form = useFormContext();
|
|
138
203
|
|
|
@@ -141,9 +206,15 @@ export const ArrayInput = ({
|
|
|
141
206
|
|
|
142
207
|
const { fields, append, remove, move } = useFieldArray({
|
|
143
208
|
name,
|
|
209
|
+
keyName: "__ID__",
|
|
144
210
|
control: form.control,
|
|
145
211
|
});
|
|
146
212
|
|
|
213
|
+
const [initialList, setInitialList] = useState<string[]>([]);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
setInitialList(fields.map((f) => f.__ID__));
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
147
218
|
const error = useMemo<AnyObject>(
|
|
148
219
|
() =>
|
|
149
220
|
form.formState.errors[name]?.message ??
|
|
@@ -151,8 +222,27 @@ export const ArrayInput = ({
|
|
|
151
222
|
[form.formState.errors],
|
|
152
223
|
);
|
|
153
224
|
|
|
225
|
+
const removeCheck = useCallback(
|
|
226
|
+
(item: T, index: number) => {
|
|
227
|
+
if (canRemove === false) return false;
|
|
228
|
+
if (canRemove === "newonly") {
|
|
229
|
+
return !initialList.includes((item as any).__ID__);
|
|
230
|
+
}
|
|
231
|
+
if (typeof canRemove === "function") {
|
|
232
|
+
return canRemove({
|
|
233
|
+
item,
|
|
234
|
+
index,
|
|
235
|
+
lastItem: index + 1 === fields.length,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
},
|
|
240
|
+
[canRemove, initialList, fields],
|
|
241
|
+
);
|
|
242
|
+
|
|
154
243
|
const handleAdd = useCallback(
|
|
155
|
-
(item
|
|
244
|
+
(item?: T) => {
|
|
245
|
+
if (!item) return;
|
|
156
246
|
append(item);
|
|
157
247
|
setTimeout(() => {
|
|
158
248
|
form.setFocus(
|
|
@@ -163,16 +253,30 @@ export const ArrayInput = ({
|
|
|
163
253
|
[name, fields, focusName],
|
|
164
254
|
);
|
|
165
255
|
|
|
256
|
+
const handleRemove = useCallback(
|
|
257
|
+
(item: T, index: number) => {
|
|
258
|
+
const ret = onBeforeRemove?.(item, index);
|
|
259
|
+
void Promise.resolve(ret).then((b) => {
|
|
260
|
+
if (!isFalse(b)) {
|
|
261
|
+
remove(index);
|
|
262
|
+
onRemove?.(item, index);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
[remove, onRemove, onBeforeRemove],
|
|
267
|
+
);
|
|
268
|
+
|
|
166
269
|
useImperativeHandle(arrayRef, () => ({
|
|
167
270
|
addItem: handleAdd,
|
|
168
271
|
removeItem: remove,
|
|
169
272
|
}));
|
|
170
273
|
|
|
171
274
|
const Wrapper = useCallback(
|
|
172
|
-
|
|
275
|
+
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
276
|
+
({ children }: { children: React.ReactNode }) => {
|
|
173
277
|
if (enableSorting) {
|
|
174
278
|
// pass id list to dnd context
|
|
175
|
-
const idMap = fields.map((item) => item.
|
|
279
|
+
const idMap = fields.map((item) => item.__ID__);
|
|
176
280
|
const handleDragEnd = (e: DragEndEvent) => {
|
|
177
281
|
if (e.over && e.active.id !== e.over.id) {
|
|
178
282
|
// find index of item and drop over
|
|
@@ -240,22 +344,81 @@ export const ArrayInput = ({
|
|
|
240
344
|
// @ts-expect-error ignore
|
|
241
345
|
SortableItem.displayName = "SortableItem";
|
|
242
346
|
|
|
347
|
+
const addButton = useMemo(
|
|
348
|
+
() => (
|
|
349
|
+
<div className="flex justify-end">
|
|
350
|
+
<Tooltip
|
|
351
|
+
color="danger-200"
|
|
352
|
+
content={
|
|
353
|
+
isString(error)
|
|
354
|
+
? `${error}`
|
|
355
|
+
: (t(
|
|
356
|
+
error?.key,
|
|
357
|
+
error?.values
|
|
358
|
+
? {
|
|
359
|
+
...error?.values,
|
|
360
|
+
label: t(
|
|
361
|
+
error.values?.label ?? "form:badkey",
|
|
362
|
+
error.values?.path,
|
|
363
|
+
),
|
|
364
|
+
}
|
|
365
|
+
: {},
|
|
366
|
+
) as string)
|
|
367
|
+
}
|
|
368
|
+
>
|
|
369
|
+
<Button
|
|
370
|
+
size="sm"
|
|
371
|
+
aria-label="Add item"
|
|
372
|
+
icon={CoreIcons.insert}
|
|
373
|
+
onClick={() => handleAdd(onAdd?.())}
|
|
374
|
+
data-invalid={!!error}
|
|
375
|
+
disabled={!!disabled || readOnly || fields.length > maxItems}
|
|
376
|
+
className={classNames(
|
|
377
|
+
"fabric-addButton",
|
|
378
|
+
canRemove !== false && "me-10",
|
|
379
|
+
)}
|
|
380
|
+
>
|
|
381
|
+
{addLabel ?? t("form:addArray")}
|
|
382
|
+
</Button>
|
|
383
|
+
</Tooltip>
|
|
384
|
+
</div>
|
|
385
|
+
),
|
|
386
|
+
[
|
|
387
|
+
error,
|
|
388
|
+
fields,
|
|
389
|
+
handleAdd,
|
|
390
|
+
canRemove,
|
|
391
|
+
disabled,
|
|
392
|
+
readOnly,
|
|
393
|
+
addLabel,
|
|
394
|
+
maxItems,
|
|
395
|
+
],
|
|
396
|
+
);
|
|
397
|
+
|
|
243
398
|
return (
|
|
244
|
-
<div className={
|
|
245
|
-
<FieldWrapper
|
|
399
|
+
<div className={"fabric-arrayInput"}>
|
|
400
|
+
<FieldWrapper
|
|
401
|
+
{...rest}
|
|
402
|
+
appendLabel={
|
|
403
|
+
<div className="-me-2">
|
|
404
|
+
{onAdd && buttonPosition !== "bottom" && addButton}
|
|
405
|
+
</div>
|
|
406
|
+
}
|
|
407
|
+
>
|
|
246
408
|
<Wrapper>
|
|
247
409
|
{fields.map((item, index) => (
|
|
248
410
|
<SortableItem
|
|
249
|
-
key={item.
|
|
250
|
-
id={item.
|
|
411
|
+
key={item.__ID__}
|
|
412
|
+
id={item.__ID__}
|
|
251
413
|
className="flex items-center flex-nowrap gap-2 mb-1"
|
|
252
414
|
>
|
|
253
415
|
{typeof children === "function"
|
|
254
416
|
? children({
|
|
255
417
|
index,
|
|
418
|
+
item: item as any,
|
|
256
419
|
name: `${name}.${index}`,
|
|
257
420
|
})
|
|
258
|
-
:
|
|
421
|
+
: cloneChildren(children, (child: AnyObject) =>
|
|
259
422
|
cloneElement(
|
|
260
423
|
child,
|
|
261
424
|
Object.assign(
|
|
@@ -277,55 +440,27 @@ export const ArrayInput = ({
|
|
|
277
440
|
),
|
|
278
441
|
),
|
|
279
442
|
)}
|
|
280
|
-
{
|
|
443
|
+
{canRemove !== false && (
|
|
281
444
|
<Button
|
|
282
445
|
aria-label="Remove item"
|
|
283
446
|
icon={CoreIcons.remove}
|
|
284
447
|
color="danger"
|
|
285
448
|
variant="link"
|
|
286
449
|
className="self-end"
|
|
287
|
-
disabled={
|
|
288
|
-
|
|
450
|
+
disabled={
|
|
451
|
+
disabled ||
|
|
452
|
+
readOnly ||
|
|
453
|
+
fields.length <= minItems ||
|
|
454
|
+
!removeCheck(item as any, index)
|
|
455
|
+
}
|
|
456
|
+
onClick={() => handleRemove(item as any, index)}
|
|
289
457
|
/>
|
|
290
458
|
)}
|
|
291
459
|
</SortableItem>
|
|
292
460
|
))}
|
|
293
461
|
</Wrapper>
|
|
294
462
|
</FieldWrapper>
|
|
295
|
-
{
|
|
296
|
-
<div className="flex justify-end">
|
|
297
|
-
<Tooltip
|
|
298
|
-
color="danger"
|
|
299
|
-
content={
|
|
300
|
-
isString(error)
|
|
301
|
-
? `${error}`
|
|
302
|
-
: (t(
|
|
303
|
-
error?.key,
|
|
304
|
-
error?.values
|
|
305
|
-
? {
|
|
306
|
-
...error?.values,
|
|
307
|
-
label: t(
|
|
308
|
-
error.values?.label ?? "form:badkey",
|
|
309
|
-
error.values?.path,
|
|
310
|
-
),
|
|
311
|
-
}
|
|
312
|
-
: {},
|
|
313
|
-
) as string)
|
|
314
|
-
}
|
|
315
|
-
>
|
|
316
|
-
<Button
|
|
317
|
-
aria-label="Add item"
|
|
318
|
-
icon={CoreIcons.insert}
|
|
319
|
-
onClick={() => handleAdd(onAdd())}
|
|
320
|
-
data-invalid={!!error}
|
|
321
|
-
disabled={!!disabled || readOnly}
|
|
322
|
-
className={classNames(classes.addButton, "me-10")}
|
|
323
|
-
>
|
|
324
|
-
{addLabel ?? t("addArray")}
|
|
325
|
-
</Button>
|
|
326
|
-
</Tooltip>
|
|
327
|
-
</div>
|
|
328
|
-
)}
|
|
463
|
+
{onAdd && buttonPosition !== "top" && addButton}
|
|
329
464
|
</div>
|
|
330
465
|
);
|
|
331
466
|
};
|