autoforma 1.0.0
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/.github/workflows/npm_test.yml +27 -0
- package/.nvmrc +1 -0
- package/.prettierrc.mjs +35 -0
- package/.storybook/main.ts +17 -0
- package/.storybook/preview.tsx +18 -0
- package/.storybook/vitest.setup.ts +9 -0
- package/.stylelintignore +1 -0
- package/.stylelintrc.json +28 -0
- package/.yarn/releases/yarn-4.9.1.cjs +948 -0
- package/.yarnrc.yml +3 -0
- package/README.md +157 -0
- package/eslint.config.js +11 -0
- package/index.html +16 -0
- package/package.json +87 -0
- package/postcss.config.cjs +14 -0
- package/src/components/AutoForm.tsx +201 -0
- package/src/components/FieldRender.tsx +217 -0
- package/src/components/fields/ArrayField.tsx +59 -0
- package/src/components/fields/CheckField.tsx +23 -0
- package/src/components/fields/DateField.tsx +26 -0
- package/src/components/fields/DateTimeField.tsx +26 -0
- package/src/components/fields/NumberField.tsx +20 -0
- package/src/components/fields/ObjectField.tsx +41 -0
- package/src/components/fields/SelectField.tsx +23 -0
- package/src/components/fields/TextAreaField.tsx +25 -0
- package/src/components/fields/TextField.tsx +20 -0
- package/src/components/fields/TimeField.tsx +32 -0
- package/src/favicon.svg +1 -0
- package/src/index.ts +3 -0
- package/src/stories/AutoForm.stories.tsx +473 -0
- package/src/stories/Fields.stories.tsx +190 -0
- package/src/theme.ts +5 -0
- package/src/types/custom-render.ts +22 -0
- package/src/types/field.ts +32 -0
- package/src/types/form.ts +8 -0
- package/src/vite-env.d.ts +1 -0
- package/storybook-static/assets/AutoForm-CEr4m2za.js +53 -0
- package/storybook-static/assets/AutoForm.stories-CtOmRKOV.js +164 -0
- package/storybook-static/assets/Color-YHDXOIA2-pc5-Ao7n.js +1 -0
- package/storybook-static/assets/DocsRenderer-CFRXHY34-D-_1VvMG.js +575 -0
- package/storybook-static/assets/Fields.stories-CoRkpMbJ.js +29 -0
- package/storybook-static/assets/chunk-XP5HYGXS-BpfKkqn7.js +1 -0
- package/storybook-static/assets/entry-preview-CkHoqRUh.js +2 -0
- package/storybook-static/assets/entry-preview-docs-hFR7sEJd.js +46 -0
- package/storybook-static/assets/get-contrast-color-Db6nT3bs.js +1 -0
- package/storybook-static/assets/iframe-CzpwBD2i.js +211 -0
- package/storybook-static/assets/index-B8rYRX2K.js +1 -0
- package/storybook-static/assets/index-BQQLSK9g.js +1 -0
- package/storybook-static/assets/index-CXQShRbs.js +8 -0
- package/storybook-static/assets/index-D4lIrffr.js +9 -0
- package/storybook-static/assets/index-Domkg0jQ.js +240 -0
- package/storybook-static/assets/index-DrFu-skq.js +6 -0
- package/storybook-static/assets/index-DsJinFGm.js +9 -0
- package/storybook-static/assets/jsx-runtime-D_zvdyIk.js +9 -0
- package/storybook-static/assets/preview-B8lJiyuQ.js +34 -0
- package/storybook-static/assets/preview-BBWR9nbA.js +1 -0
- package/storybook-static/assets/preview-BOfrPh1e.js +2 -0
- package/storybook-static/assets/preview-BWzBA1C2.js +396 -0
- package/storybook-static/assets/preview-C2Qr78FS.js +4 -0
- package/storybook-static/assets/preview-CvbIS5ZJ.js +1 -0
- package/storybook-static/assets/preview-DD_OYowb.js +1 -0
- package/storybook-static/assets/preview-DGUiP6tS.js +7 -0
- package/storybook-static/assets/preview-DioFdWiE.css +1 -0
- package/storybook-static/assets/preview-Dyg8NwXV.js +1 -0
- package/storybook-static/assets/react-18-Dhjvimrl.js +25 -0
- package/storybook-static/assets/test-utils--_O2wM1y.js +9 -0
- package/storybook-static/favicon.svg +1 -0
- package/storybook-static/iframe.html +666 -0
- package/storybook-static/index.html +181 -0
- package/storybook-static/index.json +1 -0
- package/storybook-static/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/project.json +1 -0
- package/storybook-static/sb-addons/chromatic-com-storybook-9/manager-bundle.js +331 -0
- package/storybook-static/sb-addons/chromatic-com-storybook-9/manager-bundle.js.LEGAL.txt +51 -0
- package/storybook-static/sb-addons/docs-11/manager-bundle.js +242 -0
- package/storybook-static/sb-addons/essentials-actions-2/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-backgrounds-3/manager-bundle.js +12 -0
- package/storybook-static/sb-addons/essentials-controls-1/manager-bundle.js +402 -0
- package/storybook-static/sb-addons/essentials-measure-6/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-outline-7/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-toolbars-5/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-viewport-4/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/onboarding-8/manager-bundle.js +127 -0
- package/storybook-static/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
- package/storybook-static/sb-addons/storybook-experimental-addon-test-10/manager-bundle.js +223 -0
- package/storybook-static/sb-common-assets/favicon.svg +1 -0
- package/storybook-static/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/sb-manager/globals-module-info.js +1051 -0
- package/storybook-static/sb-manager/globals-runtime.js +41591 -0
- package/storybook-static/sb-manager/globals.js +48 -0
- package/storybook-static/sb-manager/runtime.js +12048 -0
- package/test-utils/index.ts +5 -0
- package/test-utils/render.tsx +11 -0
- package/text +64 -0
- package/tsconfig.json +25 -0
- package/vite.config.mjs +19 -0
- package/vitest.setup.mjs +29 -0
- package/vitest.shims.d.ts +1 -0
- package/vitest.workspace.js +30 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import AutoForm from '@/components/AutoForm';
|
|
2
|
+
import FieldRender, { getArrayOptions } from '@/components/FieldRender';
|
|
3
|
+
import { CustomFieldRender } from '@/types/custom-render';
|
|
4
|
+
import { FieldSchema } from '@/types/field';
|
|
5
|
+
import { Accordion, Button, Grid, Group, Modal, Table, Text } from '@mantine/core';
|
|
6
|
+
import { useDisclosure } from '@mantine/hooks';
|
|
7
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
8
|
+
|
|
9
|
+
// ------------------------------
|
|
10
|
+
// Schema Definitions
|
|
11
|
+
// ------------------------------
|
|
12
|
+
|
|
13
|
+
const schema: FieldSchema[] = [
|
|
14
|
+
{
|
|
15
|
+
type: 'check',
|
|
16
|
+
label: 'Enabled',
|
|
17
|
+
name: 'enabled',
|
|
18
|
+
initialValue: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: 'object',
|
|
22
|
+
name: 'personalInformation',
|
|
23
|
+
label: 'Personal Information',
|
|
24
|
+
fields: [
|
|
25
|
+
{
|
|
26
|
+
type: 'text',
|
|
27
|
+
name: 'firstName',
|
|
28
|
+
label: 'First Name',
|
|
29
|
+
initialValue: 'Saji',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
name: 'lastName',
|
|
34
|
+
label: 'Last Name',
|
|
35
|
+
initialValue: 'Nael',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'select',
|
|
39
|
+
name: 'gender',
|
|
40
|
+
label: 'Gender',
|
|
41
|
+
data: [
|
|
42
|
+
{ label: 'Male', value: 'male' },
|
|
43
|
+
{ label: 'Female', value: 'female' },
|
|
44
|
+
],
|
|
45
|
+
initialValue: 'male',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'object',
|
|
51
|
+
name: 'accountInformation',
|
|
52
|
+
label: 'Account Information',
|
|
53
|
+
fields: [
|
|
54
|
+
{
|
|
55
|
+
type: 'text',
|
|
56
|
+
name: 'username',
|
|
57
|
+
label: 'Username',
|
|
58
|
+
initialValue: 'saji-nael',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
name: 'email',
|
|
63
|
+
label: 'Email',
|
|
64
|
+
initialValue: 'saji.nael.98@gmail.com',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
type: 'date',
|
|
68
|
+
name: 'dateOfJoin',
|
|
69
|
+
label: 'Date Of Join',
|
|
70
|
+
initialValue: new Date(),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: 'object',
|
|
76
|
+
name: 'contactInformation',
|
|
77
|
+
label: 'Contact Information',
|
|
78
|
+
fields: [
|
|
79
|
+
{
|
|
80
|
+
type: 'array',
|
|
81
|
+
name: 'phones',
|
|
82
|
+
label: 'Phones',
|
|
83
|
+
fields: [
|
|
84
|
+
{
|
|
85
|
+
type: 'text',
|
|
86
|
+
name: 'phoneNo',
|
|
87
|
+
label: 'Phone NO',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const schemaWithValidation: FieldSchema[] = [
|
|
96
|
+
...schema.slice(0, 3),
|
|
97
|
+
{
|
|
98
|
+
...schema[3],
|
|
99
|
+
fields: [
|
|
100
|
+
{
|
|
101
|
+
...schema[3].fields[0],
|
|
102
|
+
required: true,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// ------------------------------
|
|
109
|
+
// Storybook Meta
|
|
110
|
+
// ------------------------------
|
|
111
|
+
|
|
112
|
+
const meta: Meta<typeof AutoForm> = {
|
|
113
|
+
title: 'components/AutoForm',
|
|
114
|
+
component: AutoForm,
|
|
115
|
+
tags: ['autodocs'],
|
|
116
|
+
args: {
|
|
117
|
+
onSubmit: (values) => alert(JSON.stringify(values, null, 2)),
|
|
118
|
+
schema,
|
|
119
|
+
container: (form, onSubmit, readOnly) => (
|
|
120
|
+
<>
|
|
121
|
+
<Grid>{form}</Grid>
|
|
122
|
+
{!readOnly && (
|
|
123
|
+
<Group justify="flex-end">
|
|
124
|
+
<Button onClick={onSubmit}>Submit</Button>
|
|
125
|
+
</Group>
|
|
126
|
+
)}
|
|
127
|
+
</>
|
|
128
|
+
),
|
|
129
|
+
fieldContainer: (field, fieldSchema) => (
|
|
130
|
+
<Grid.Col span={{ base: 12, md: fieldSchema.type === 'object' ? 12 : 6 }}>{field}</Grid.Col>
|
|
131
|
+
),
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
argTypes: {
|
|
135
|
+
values: {
|
|
136
|
+
description: 'Initial form values. Useful for editing or setting default values.',
|
|
137
|
+
control: false,
|
|
138
|
+
},
|
|
139
|
+
schema: {
|
|
140
|
+
description:
|
|
141
|
+
'Array of field definitions used to render the form. Each item defines one input field.',
|
|
142
|
+
control: false,
|
|
143
|
+
},
|
|
144
|
+
onSubmit: {
|
|
145
|
+
description: 'Function called with form values when the form is submitted.',
|
|
146
|
+
control: false,
|
|
147
|
+
},
|
|
148
|
+
container: {
|
|
149
|
+
description:
|
|
150
|
+
'Function that receives the rendered form and the onSubmit handler. Used to wrap the form with layout and controls.',
|
|
151
|
+
control: false,
|
|
152
|
+
},
|
|
153
|
+
fieldContainer: {
|
|
154
|
+
description: 'Optional function to wrap each individual field with custom layout or styling.',
|
|
155
|
+
control: false,
|
|
156
|
+
},
|
|
157
|
+
customRender: {
|
|
158
|
+
description:
|
|
159
|
+
'Optional custom render function to override the default field rendering based on field type.',
|
|
160
|
+
control: false,
|
|
161
|
+
},
|
|
162
|
+
validate: {
|
|
163
|
+
description:
|
|
164
|
+
'Optional validation logic. Receives values and returns an object with field errors or nulls.',
|
|
165
|
+
control: false,
|
|
166
|
+
},
|
|
167
|
+
readOnly: {
|
|
168
|
+
description: 'If true, all fields will be rendered in read-only mode.',
|
|
169
|
+
control: { type: 'boolean' },
|
|
170
|
+
},
|
|
171
|
+
onFieldChange: {
|
|
172
|
+
description:
|
|
173
|
+
'Optional function triggered on field change. You can return modified values dynamically.',
|
|
174
|
+
control: false,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
parameters: {
|
|
178
|
+
docs: {
|
|
179
|
+
description: {
|
|
180
|
+
component: `
|
|
181
|
+
### 📄 AutoForm
|
|
182
|
+
|
|
183
|
+
\`AutoForm\` is a dynamic, schema-driven form component designed for full control and customization.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### 🧩 AutoFormProps Overview
|
|
188
|
+
|
|
189
|
+
| Prop | Type | Description |
|
|
190
|
+
|------------------|----------------------------------------------------------------------|-------------|
|
|
191
|
+
| \`schema\` | \`FieldSchema[]\` | Array of field definitions |
|
|
192
|
+
| \`onSubmit\` | \`(values: Record<string, any>) => void\` | Called when form is submitted |
|
|
193
|
+
| \`values\` | \`Record<string, any>\` | Initial values |
|
|
194
|
+
| \`readOnly\` | \`true?\` | Makes the form non-editable |
|
|
195
|
+
| \`validate\` | \`FormValidateInput<Record<string, any>>\` | Optional validation function |
|
|
196
|
+
| \`onFieldChange\` | \`(name, value, values) => Record<string, any>\` | Callback when a field changes |
|
|
197
|
+
| \`container\` | \`(formNode, onSubmit, readOnly) => React.ReactNode\` | Customize the outer layout of the form |
|
|
198
|
+
| \`fieldContainer\` | \`(fieldNode, fieldSchema) => React.ReactNode\` | Customize how each field is rendered |
|
|
199
|
+
| \`customRender\` | \`(field, value, error, onChange, values, options, readOnly) => JSX\` | Advanced per-field rendering |
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### 🧱 Layout Customization
|
|
204
|
+
|
|
205
|
+
#### 🔲 \`container\`
|
|
206
|
+
|
|
207
|
+
Use the \`container\` prop to wrap the full form, including layout, submit buttons, etc.
|
|
208
|
+
|
|
209
|
+
#### 🔳 \`fieldContainer\`
|
|
210
|
+
|
|
211
|
+
With \`fieldContainer\`, you can wrap **each field** with your own layout:
|
|
212
|
+
|
|
213
|
+
\`\`\`tsx
|
|
214
|
+
fieldContainer: (field, fieldSchema) => (
|
|
215
|
+
<Grid.Col span={{ base: 12, md: fieldSchema.type === 'object' ? 12 : 6 }}>
|
|
216
|
+
{field}
|
|
217
|
+
</Grid.Col>
|
|
218
|
+
)
|
|
219
|
+
\`\`\`
|
|
220
|
+
|
|
221
|
+
This allows **full control per field** — spacing, layout, responsiveness, etc.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
### 🧠 Advanced Rendering with \`customRender\`
|
|
226
|
+
|
|
227
|
+
\`customRender\` gives you full control over how a field is rendered.
|
|
228
|
+
|
|
229
|
+
#### 📦 Example
|
|
230
|
+
|
|
231
|
+
\`\`\`tsx
|
|
232
|
+
customRender: (field, objValue, error, onObjChange, formValues, options, readOnly) => {
|
|
233
|
+
if (field.type === 'object') {
|
|
234
|
+
return (
|
|
235
|
+
<Accordion defaultValue={field.name}>
|
|
236
|
+
<Accordion.Item value={field.name}>
|
|
237
|
+
<Accordion.Control>{field.label}</Accordion.Control>
|
|
238
|
+
<Accordion.Panel>
|
|
239
|
+
<Grid>
|
|
240
|
+
{field.fields?.map((childField) => {
|
|
241
|
+
function onChange(name: string, value: any) {
|
|
242
|
+
objValue[name] = value;
|
|
243
|
+
onObjChange(field.name, objValue);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (childField.name === 'phones') {
|
|
247
|
+
return (
|
|
248
|
+
<CustomPhoneList
|
|
249
|
+
key={childField.name}
|
|
250
|
+
field={childField}
|
|
251
|
+
value={objValue[childField.name]}
|
|
252
|
+
onChange={onChange}
|
|
253
|
+
error={error}
|
|
254
|
+
formValues={formValues}
|
|
255
|
+
options={options}
|
|
256
|
+
readOnly={readOnly}
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<Grid.Col key={childField.name} span={6}>
|
|
263
|
+
<FieldRender
|
|
264
|
+
field={childField}
|
|
265
|
+
formValues={formValues}
|
|
266
|
+
value={objValue[childField.name]}
|
|
267
|
+
onChange={onChange}
|
|
268
|
+
readOnly={readOnly}
|
|
269
|
+
/>
|
|
270
|
+
</Grid.Col>
|
|
271
|
+
);
|
|
272
|
+
})}
|
|
273
|
+
</Grid>
|
|
274
|
+
</Accordion.Panel>
|
|
275
|
+
</Accordion.Item>
|
|
276
|
+
</Accordion>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
\`\`\`
|
|
282
|
+
|
|
283
|
+
You can use this to:
|
|
284
|
+
- Nest fields inside custom layouts
|
|
285
|
+
- Open modals, drawers, or complex editors
|
|
286
|
+
- Show/hide fields dynamically
|
|
287
|
+
- Render fields as fully custom components
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
`,
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export default meta;
|
|
297
|
+
|
|
298
|
+
// ------------------------------
|
|
299
|
+
// Stories
|
|
300
|
+
// ------------------------------
|
|
301
|
+
|
|
302
|
+
type Story = StoryObj<typeof AutoForm>;
|
|
303
|
+
|
|
304
|
+
export const Standard: Story = {};
|
|
305
|
+
|
|
306
|
+
export const ReadOnly: Story = {
|
|
307
|
+
args: { readOnly: true },
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export const FormValidation: Story = {
|
|
311
|
+
args: {
|
|
312
|
+
schema: schemaWithValidation,
|
|
313
|
+
validate: {
|
|
314
|
+
contactInformation: {
|
|
315
|
+
phones: {
|
|
316
|
+
phoneNo: (value: string) => {
|
|
317
|
+
const regex = /^(059|056)\d{7}$/;
|
|
318
|
+
return regex.test(value)
|
|
319
|
+
? null
|
|
320
|
+
: "Invalid phone number. It must be exactly 10 digits long and start with '059' or '056'.";
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const CustomizedForm: Story = {
|
|
329
|
+
args: {
|
|
330
|
+
schema,
|
|
331
|
+
customRender: (field, objValue, error, onObjChange, formValues, options, readOnly) => {
|
|
332
|
+
if (field.type === 'object') {
|
|
333
|
+
return (
|
|
334
|
+
<Grid.Col span={12}>
|
|
335
|
+
<Accordion defaultValue={field.name} variant="contained">
|
|
336
|
+
<Accordion.Item value={field.name}>
|
|
337
|
+
<Accordion.Control>{field.label}</Accordion.Control>
|
|
338
|
+
<Accordion.Panel>
|
|
339
|
+
<Grid>
|
|
340
|
+
{field.fields?.map((field) => {
|
|
341
|
+
const onChange = (name: string, value: any) => {
|
|
342
|
+
objValue[name] = value;
|
|
343
|
+
onObjChange(field.name, objValue);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
if (field.name === 'phones') {
|
|
347
|
+
return (
|
|
348
|
+
<CustomPhoneList
|
|
349
|
+
key={field.name}
|
|
350
|
+
error={error}
|
|
351
|
+
field={field}
|
|
352
|
+
formValues={formValues}
|
|
353
|
+
onChange={onChange}
|
|
354
|
+
value={objValue[field.name]}
|
|
355
|
+
options={options}
|
|
356
|
+
readOnly={readOnly}
|
|
357
|
+
/>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<Grid.Col
|
|
363
|
+
key={field.name}
|
|
364
|
+
span={{ base: 12, md: field.type === 'object' ? 12 : 6 }}
|
|
365
|
+
>
|
|
366
|
+
<FieldRender
|
|
367
|
+
field={field}
|
|
368
|
+
formValues={formValues}
|
|
369
|
+
value={objValue[field.name]}
|
|
370
|
+
error={undefined}
|
|
371
|
+
readOnly={readOnly}
|
|
372
|
+
onChange={onChange}
|
|
373
|
+
/>
|
|
374
|
+
</Grid.Col>
|
|
375
|
+
);
|
|
376
|
+
})}
|
|
377
|
+
</Grid>
|
|
378
|
+
</Accordion.Panel>
|
|
379
|
+
</Accordion.Item>
|
|
380
|
+
</Accordion>
|
|
381
|
+
</Grid.Col>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// ------------------------------
|
|
390
|
+
// Custom Field Renderer
|
|
391
|
+
// ------------------------------
|
|
392
|
+
|
|
393
|
+
const CustomPhoneList: React.FC<CustomFieldRender> = (props) => {
|
|
394
|
+
const [visible, { open, close }] = useDisclosure(false);
|
|
395
|
+
const { error, field, formValues, onChange, value, options, readOnly } = props;
|
|
396
|
+
|
|
397
|
+
const arrayOptions = getArrayOptions(field.name, onChange, value);
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<>
|
|
401
|
+
<FieldRender
|
|
402
|
+
field={field}
|
|
403
|
+
formValues={formValues}
|
|
404
|
+
onChange={onChange}
|
|
405
|
+
value={value}
|
|
406
|
+
error={error}
|
|
407
|
+
readOnly={readOnly}
|
|
408
|
+
customRender={() => (
|
|
409
|
+
<>
|
|
410
|
+
<Modal title="Add Phone" onClose={close} opened={visible}>
|
|
411
|
+
<AutoForm
|
|
412
|
+
schema={field.fields as FieldSchema[]}
|
|
413
|
+
fieldContainer={(field, fieldSchema) => (
|
|
414
|
+
<Grid.Col span={{ base: 12, md: fieldSchema.type === 'object' ? 12 : 6 }}>
|
|
415
|
+
{field}
|
|
416
|
+
</Grid.Col>
|
|
417
|
+
)}
|
|
418
|
+
container={(form, onSubmit, readOnly) => (
|
|
419
|
+
<>
|
|
420
|
+
<Grid>{form}</Grid>
|
|
421
|
+
{!readOnly && (
|
|
422
|
+
<Group justify="flex-end">
|
|
423
|
+
<Button onClick={onSubmit}>Submit</Button>
|
|
424
|
+
</Group>
|
|
425
|
+
)}
|
|
426
|
+
</>
|
|
427
|
+
)}
|
|
428
|
+
onSubmit={(values) => {
|
|
429
|
+
arrayOptions?.addElement(values);
|
|
430
|
+
close();
|
|
431
|
+
}}
|
|
432
|
+
/>
|
|
433
|
+
</Modal>
|
|
434
|
+
|
|
435
|
+
<Button onClick={open} mb="md">
|
|
436
|
+
Add Phone
|
|
437
|
+
</Button>
|
|
438
|
+
|
|
439
|
+
{value.length > 0 ? (
|
|
440
|
+
<Table withColumnBorders withRowBorders withTableBorder striped>
|
|
441
|
+
<Table.Thead>
|
|
442
|
+
<Table.Tr>
|
|
443
|
+
<Table.Th>Phone NO.</Table.Th>
|
|
444
|
+
<Table.Th>Action</Table.Th>
|
|
445
|
+
</Table.Tr>
|
|
446
|
+
</Table.Thead>
|
|
447
|
+
<Table.Tbody>
|
|
448
|
+
{value.map((phone, index) => (
|
|
449
|
+
<Table.Tr key={index}>
|
|
450
|
+
<Table.Td>{phone.phoneNo}</Table.Td>
|
|
451
|
+
<Table.Td>
|
|
452
|
+
<Button
|
|
453
|
+
variant="light"
|
|
454
|
+
size="xs"
|
|
455
|
+
color="red"
|
|
456
|
+
onClick={() => arrayOptions.removeElement(index)}
|
|
457
|
+
>
|
|
458
|
+
Delete
|
|
459
|
+
</Button>
|
|
460
|
+
</Table.Td>
|
|
461
|
+
</Table.Tr>
|
|
462
|
+
))}
|
|
463
|
+
</Table.Tbody>
|
|
464
|
+
</Table>
|
|
465
|
+
) : (
|
|
466
|
+
<Text>No phones are inserted</Text>
|
|
467
|
+
)}
|
|
468
|
+
</>
|
|
469
|
+
)}
|
|
470
|
+
/>
|
|
471
|
+
</>
|
|
472
|
+
);
|
|
473
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Button, Grid, Group } from '@mantine/core';
|
|
3
|
+
import AutoForm from '@/components/AutoForm';
|
|
4
|
+
import { FieldSchema } from '@/types/field';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// ------------------------------
|
|
8
|
+
// Schema Definitions
|
|
9
|
+
// ------------------------------
|
|
10
|
+
const textFieldSchema: FieldSchema[] = [
|
|
11
|
+
{
|
|
12
|
+
type: 'text',
|
|
13
|
+
name: 'firstName',
|
|
14
|
+
label: 'First Name',
|
|
15
|
+
initialValue: 'John',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const numberFieldSchema: FieldSchema[] = [
|
|
20
|
+
{
|
|
21
|
+
type: 'number',
|
|
22
|
+
name: 'age',
|
|
23
|
+
label: 'Age',
|
|
24
|
+
initialValue: 30,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const selectFieldSchema: FieldSchema[] = [
|
|
29
|
+
{
|
|
30
|
+
type: 'select',
|
|
31
|
+
name: 'gender',
|
|
32
|
+
label: 'Gender',
|
|
33
|
+
data: [
|
|
34
|
+
{ label: 'Male', value: 'male' },
|
|
35
|
+
{ label: 'Female', value: 'female' },
|
|
36
|
+
],
|
|
37
|
+
initialValue: 'male',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const checkFieldSchema: FieldSchema[] = [
|
|
42
|
+
{
|
|
43
|
+
type: 'check',
|
|
44
|
+
name: 'enabled',
|
|
45
|
+
label: 'Enabled',
|
|
46
|
+
initialValue: true,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const dateFieldSchema: FieldSchema[] = [
|
|
51
|
+
{
|
|
52
|
+
type: 'date',
|
|
53
|
+
name: 'birthDate',
|
|
54
|
+
label: 'Birth Date',
|
|
55
|
+
initialValue: new Date(),
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const arrayFieldSchema: FieldSchema[] = [
|
|
60
|
+
{
|
|
61
|
+
type: 'array',
|
|
62
|
+
name: 'tags',
|
|
63
|
+
label: 'Tags',
|
|
64
|
+
fields: [
|
|
65
|
+
{
|
|
66
|
+
type: 'text',
|
|
67
|
+
name: 'tag',
|
|
68
|
+
label: 'Tag',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const objectFieldSchema: FieldSchema[] = [
|
|
75
|
+
{
|
|
76
|
+
type: 'object',
|
|
77
|
+
name: 'address',
|
|
78
|
+
label: 'Address',
|
|
79
|
+
fields: [
|
|
80
|
+
{
|
|
81
|
+
type: 'text',
|
|
82
|
+
name: 'street',
|
|
83
|
+
label: 'Street',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'text',
|
|
87
|
+
name: 'city',
|
|
88
|
+
label: 'City',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// ------------------------------
|
|
95
|
+
// Storybook Meta
|
|
96
|
+
// ------------------------------
|
|
97
|
+
|
|
98
|
+
const meta: Meta<typeof AutoForm> = {
|
|
99
|
+
title: 'components/Fields',
|
|
100
|
+
component: AutoForm,
|
|
101
|
+
tags: ['autodocs'],
|
|
102
|
+
args: {
|
|
103
|
+
onSubmit: (values) => alert(JSON.stringify(values, null, 2)),
|
|
104
|
+
container: (form, onSubmit, readOnly) => (
|
|
105
|
+
<>
|
|
106
|
+
<Grid>{form}</Grid>
|
|
107
|
+
{!readOnly && (
|
|
108
|
+
<Group justify="flex-end">
|
|
109
|
+
<Button onClick={onSubmit}>Submit</Button>
|
|
110
|
+
</Group>
|
|
111
|
+
)}
|
|
112
|
+
</>
|
|
113
|
+
),
|
|
114
|
+
fieldContainer: (field, fieldSchema) => (
|
|
115
|
+
<Grid.Col span={{ base: 12, md: fieldSchema.type === 'object' ? 12 : 6 }}>{field}</Grid.Col>
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
argTypes: {
|
|
119
|
+
onSubmit: {
|
|
120
|
+
table: {
|
|
121
|
+
disable: true,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
container: {
|
|
125
|
+
table: {
|
|
126
|
+
disable: true,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
fieldContainer: {
|
|
130
|
+
table: {
|
|
131
|
+
disable: true,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
schema: {
|
|
135
|
+
description:
|
|
136
|
+
'Array of field definitions used to render the form. Each item defines one input field.',
|
|
137
|
+
control: false,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default meta;
|
|
143
|
+
|
|
144
|
+
// ------------------------------
|
|
145
|
+
// Stories
|
|
146
|
+
// ------------------------------
|
|
147
|
+
|
|
148
|
+
type Story = StoryObj<typeof AutoForm>;
|
|
149
|
+
|
|
150
|
+
export const TextField: Story = {
|
|
151
|
+
args: {
|
|
152
|
+
schema: textFieldSchema,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const NumberField: Story = {
|
|
157
|
+
args: {
|
|
158
|
+
schema: numberFieldSchema,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const SelectField: Story = {
|
|
163
|
+
args: {
|
|
164
|
+
schema: selectFieldSchema,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const CheckField: Story = {
|
|
169
|
+
args: {
|
|
170
|
+
schema: checkFieldSchema,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const DateField: Story = {
|
|
175
|
+
args: {
|
|
176
|
+
schema: dateFieldSchema,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const ArrayField: Story = {
|
|
181
|
+
args: {
|
|
182
|
+
schema: arrayFieldSchema,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const ObjectField: Story = {
|
|
187
|
+
args: {
|
|
188
|
+
schema: objectFieldSchema,
|
|
189
|
+
},
|
|
190
|
+
};
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// types/custom-render.ts
|
|
2
|
+
import { ArrayFieldOptions, FieldSchema } from './field';
|
|
3
|
+
|
|
4
|
+
export type FieldRenderCustomRender = (
|
|
5
|
+
field: FieldSchema,
|
|
6
|
+
value: any,
|
|
7
|
+
error: React.ReactNode | Record<string, React.ReactNode>,
|
|
8
|
+
onChange: (name: string, value: any) => void,
|
|
9
|
+
formValues: Record<string, any>,
|
|
10
|
+
options?: ArrayFieldOptions,
|
|
11
|
+
readOnly?: true
|
|
12
|
+
) => React.ReactNode;
|
|
13
|
+
|
|
14
|
+
export interface CustomFieldRender {
|
|
15
|
+
field: FieldSchema;
|
|
16
|
+
value: any;
|
|
17
|
+
error: React.ReactNode | Record<string, React.ReactNode>;
|
|
18
|
+
onChange: (name: string, value: any) => void;
|
|
19
|
+
formValues: Record<string, any>;
|
|
20
|
+
options?: ArrayFieldOptions;
|
|
21
|
+
readOnly?: true;
|
|
22
|
+
}
|