Skip to content

Commit dbe440c

Browse files
authored
Merge pull request #637 from data-driven-forms/add-field-array-ant
fix(ant): Added field array component
2 parents 06693e7 + 489febf commit dbe440c

File tree

3 files changed

+375
-3
lines changed

3 files changed

+375
-3
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const arraySchemaDDF = {
2+
title: 'FieldArray',
3+
fields: [
4+
{
5+
component: 'field-array',
6+
name: 'nicePeople',
7+
fieldKey: 'field_array',
8+
label: 'Nice people',
9+
description: 'This allow you to add nice people to the list dynamically',
10+
defaultItem: { name: 'enter a name', lastName: 'enter a last name' },
11+
fields: [
12+
{
13+
component: 'text-field',
14+
name: 'name',
15+
label: 'Name',
16+
placeholder: 'Borek',
17+
isRequired: true,
18+
validate: [
19+
{
20+
type: 'required'
21+
}
22+
]
23+
},
24+
{
25+
component: 'text-field',
26+
name: 'lastName',
27+
label: 'Last Name',
28+
placeholder: 'Stavitel'
29+
}
30+
]
31+
},
32+
{
33+
component: 'field-array',
34+
name: 'minItems',
35+
label: 'A list with a minimal number of items',
36+
validate: [{ type: 'min-items', threshold: 3 }],
37+
fields: [
38+
{
39+
component: 'text-field',
40+
label: 'Item'
41+
}
42+
]
43+
},
44+
{
45+
component: 'field-array',
46+
name: 'number',
47+
defaultItem: 5,
48+
label: 'Default value with initialValues set',
49+
fields: [
50+
{
51+
component: 'text-field',
52+
label: 'Item',
53+
type: 'number'
54+
}
55+
]
56+
},
57+
{
58+
component: 'field-array',
59+
name: 'minMax',
60+
minItems: 4,
61+
maxItems: 6,
62+
label: 'Min 4 item, max 6 items without validators',
63+
fields: [
64+
{
65+
component: 'text-field',
66+
isRequired: true,
67+
validate: [
68+
{
69+
type: 'required'
70+
}
71+
]
72+
}
73+
]
74+
}
75+
]
76+
};
77+
78+
export default arraySchemaDDF;

packages/ant-component-mapper/demo/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import dualListSelectSchema from './demo-schemas/dual-list-select-schema'
99
import { componentMapper, FormTemplate } from '../src';
1010
import wizardSchema from './demo-schemas/wizard-schema';
1111
import sliderSchema from './demo-schemas/slider-schema';
12+
import fieldArraySchema from './demo-schemas/field-array-schema';
1213

1314
const style = {
1415
position: 'relative',
@@ -22,7 +23,7 @@ const App = () => (
2223
componentMapper={componentMapper}
2324
FormTemplate={(props) => <FormTemplate layout='vertical' {...props} />}
2425
onSubmit={console.log}
25-
schema={sliderSchema}
26+
schema={fieldArraySchema}
2627
/>
2728
</div>
2829
);
Lines changed: 295 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,296 @@
1-
import React from 'react';
1+
import React, { useReducer } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useFieldApi, useFormApi, FieldArray } from '@data-driven-forms/react-form-renderer';
4+
import { Row, Col, Button, Typography, Space } from 'antd';
5+
import { UndoOutlined, RedoOutlined } from '@ant-design/icons';
26

3-
export default () => <div>not implemented</div>;
7+
import AntForm from '../common/form-wrapper';
8+
9+
const ArrayItem = ({
10+
fields,
11+
fieldIndex,
12+
name,
13+
remove,
14+
length,
15+
minItems,
16+
removeLabel,
17+
ArrayItemProps,
18+
FieldsContainerProps,
19+
RemoveContainerProps,
20+
RemoveButtonProps
21+
}) => {
22+
const { renderForm } = useFormApi();
23+
24+
const editedFields = fields.map((field, index) => {
25+
const computedName = field.name ? `${name}.${field.name}` : name;
26+
return { ...field, name: computedName, key: `${computedName}-${index}` };
27+
});
28+
29+
return (
30+
<Row {...ArrayItemProps}>
31+
<Col span={24} {...FieldsContainerProps}>
32+
{renderForm([editedFields])}
33+
</Col>
34+
<Col span={24} {...RemoveContainerProps}>
35+
<Button type="primary" danger {...RemoveButtonProps} onClick={() => remove(fieldIndex)} disabled={length <= minItems}>
36+
{removeLabel}
37+
</Button>
38+
</Col>
39+
</Row>
40+
);
41+
};
42+
43+
ArrayItem.propTypes = {
44+
name: PropTypes.string,
45+
fieldIndex: PropTypes.number.isRequired,
46+
fields: PropTypes.arrayOf(PropTypes.object),
47+
remove: PropTypes.func.isRequired,
48+
length: PropTypes.number,
49+
minItems: PropTypes.number,
50+
removeLabel: PropTypes.node.isRequired,
51+
ArrayItemProps: PropTypes.object.isRequired,
52+
FieldsContainerProps: PropTypes.object.isRequired,
53+
RemoveContainerProps: PropTypes.object.isRequired,
54+
RemoveButtonProps: PropTypes.object.isRequired
55+
};
56+
57+
const defaultButtonLabels = {
58+
add: 'ADD',
59+
remove: 'REMOVE'
60+
};
61+
62+
const initialState = {
63+
index: 0,
64+
history: []
65+
};
66+
67+
export const reducer = (state, { type, action }) => {
68+
switch (type) {
69+
case 'redo':
70+
return {
71+
...state,
72+
index: state.index + 1
73+
};
74+
case 'action':
75+
return {
76+
index: state.index + 1,
77+
history: [...state.history.slice(0, state.index), action]
78+
};
79+
case 'undo':
80+
return {
81+
...state,
82+
index: state.index - 1
83+
};
84+
case 'resetHistory':
85+
return {
86+
...state,
87+
history: state.history.slice(0, state.index)
88+
};
89+
default:
90+
return state;
91+
}
92+
};
93+
94+
const DynamicArray = ({ ...props }) => {
95+
const {
96+
arrayValidator,
97+
label,
98+
description,
99+
fields: formFields,
100+
defaultItem,
101+
meta,
102+
minItems,
103+
maxItems,
104+
noItemsMessage,
105+
FormFieldGridProps,
106+
FormControlProps,
107+
buttonLabels,
108+
validateOnMount,
109+
isRequired,
110+
helperText,
111+
// customization props
112+
FormItemProps,
113+
ArrayItemProps,
114+
FieldsContainerProps,
115+
RemoveContainerProps,
116+
RemoveButtonProps,
117+
FieldArrayRowProps,
118+
FieldArrayRowCol,
119+
FieldArrayHeaderProps,
120+
FieldArrayLabelProps,
121+
FieldArrayButtonsProps,
122+
UndoButtonProps,
123+
RedoButtonProps,
124+
AddButtonProps,
125+
FieldArrayDescriptionProps,
126+
NoItemsMessageProps,
127+
ErrorMessageProps,
128+
...rest
129+
} = useFieldApi(props);
130+
const [state, dispatch] = useReducer(reducer, initialState);
131+
132+
const combinedButtonLabels = {
133+
...defaultButtonLabels,
134+
...buttonLabels
135+
};
136+
137+
const { dirty, submitFailed, error } = meta;
138+
const isError = (dirty || submitFailed) && error && typeof error === 'string';
139+
return (
140+
<AntForm
141+
meta={{ ...meta, error: typeof error === 'object' ? error.name : error }}
142+
validateOnMount={validateOnMount}
143+
helperText={helperText}
144+
FormItemProps={FormItemProps}
145+
isRequired={isRequired}
146+
>
147+
<FieldArray key={rest.input.name} name={rest.input.name} validate={arrayValidator}>
148+
{({ fields: { map, value = [], push, remove } }) => {
149+
const pushWrapper = () => {
150+
dispatch({ type: 'resetHistory' });
151+
push(defaultItem);
152+
};
153+
154+
const removeWrapper = (index) => {
155+
dispatch({ type: 'action', action: { action: 'remove', value: value[index] } });
156+
remove(index);
157+
};
158+
159+
const undo = () => {
160+
push(state.history[state.index - 1].value);
161+
dispatch({ type: 'undo' });
162+
};
163+
164+
const redo = () => {
165+
remove(value.length - 1);
166+
dispatch({ type: 'redo' });
167+
};
168+
169+
return (
170+
<Row gutter={[0, 16]} {...FieldArrayRowProps}>
171+
<Col span={24} {...FieldArrayRowCol}>
172+
<Row justify="space-between" {...FieldArrayHeaderProps}>
173+
<Col>
174+
{label && (
175+
<Typography.Title level={4} {...FieldArrayLabelProps}>
176+
{label}
177+
</Typography.Title>
178+
)}
179+
</Col>
180+
<Col>
181+
<Space {...FieldArrayButtonsProps}>
182+
<Button type="default" icon={<UndoOutlined />} {...UndoButtonProps} onClick={undo} disabled={state.index === 0} />
183+
<Button
184+
type="default"
185+
icon={<RedoOutlined />}
186+
{...RedoButtonProps}
187+
onClick={redo}
188+
disabled={state.index === state.history.length}
189+
/>
190+
<Button type="primary" {...AddButtonProps} onClick={pushWrapper} disabled={value.length >= maxItems}>
191+
{combinedButtonLabels.add}
192+
</Button>
193+
</Space>
194+
</Col>
195+
</Row>
196+
</Col>
197+
{description && (
198+
<Col span={24}>
199+
<Typography.Text {...FieldArrayDescriptionProps}>{description}</Typography.Text>
200+
</Col>
201+
)}
202+
<Col span={24}>
203+
<Row gutter={[0, 16]}>
204+
{value.length <= 0 ? (
205+
typeof noItemsMessage === 'string' ? (
206+
<Typography.Text {...NoItemsMessageProps}>{noItemsMessage}</Typography.Text>
207+
) : (
208+
React.cloneElement(noItemsMessage, NoItemsMessageProps)
209+
)
210+
) : (
211+
map((name, index) => (
212+
<Col span={24} key={name}>
213+
<ArrayItem
214+
fields={formFields}
215+
name={name}
216+
fieldIndex={index}
217+
remove={removeWrapper}
218+
length={value.length}
219+
minItems={minItems}
220+
removeLabel={combinedButtonLabels.remove}
221+
ArrayItemProps={ArrayItemProps}
222+
FieldsContainerProps={FieldsContainerProps}
223+
RemoveContainerProps={RemoveContainerProps}
224+
RemoveButtonProps={RemoveButtonProps}
225+
/>
226+
</Col>
227+
))
228+
)}
229+
</Row>
230+
</Col>
231+
{isError && (
232+
<Col span={12}>
233+
<Typography.Text type="danger" {...ErrorMessageProps}>
234+
{typeof error === 'object' ? error.name : error}
235+
</Typography.Text>
236+
</Col>
237+
)}
238+
</Row>
239+
);
240+
}}
241+
</FieldArray>
242+
</AntForm>
243+
);
244+
};
245+
246+
DynamicArray.propTypes = {
247+
label: PropTypes.node,
248+
description: PropTypes.node,
249+
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
250+
defaultItem: PropTypes.any,
251+
minItems: PropTypes.number,
252+
maxItems: PropTypes.number,
253+
noItemsMessage: PropTypes.node,
254+
buttonLabels: PropTypes.object,
255+
// customization props
256+
FormItemProps: PropTypes.object,
257+
ArrayItemProps: PropTypes.object,
258+
FieldsContainerProps: PropTypes.object,
259+
RemoveContainerProps: PropTypes.object,
260+
RemoveButtonProps: PropTypes.object,
261+
FieldArrayRowProps: PropTypes.object,
262+
FieldArrayRowCol: PropTypes.object,
263+
FieldArrayHeaderProps: PropTypes.object,
264+
FieldArrayLabelProps: PropTypes.object,
265+
FieldArrayButtonsProps: PropTypes.object,
266+
UndoButtonProps: PropTypes.object,
267+
RedoButtonProps: PropTypes.object,
268+
AddButtonProps: PropTypes.object,
269+
FieldArrayDescriptionProps: PropTypes.object,
270+
NoItemsMessageProps: PropTypes.object,
271+
ErrorMessageProps: PropTypes.object
272+
};
273+
274+
DynamicArray.defaultProps = {
275+
maxItems: Infinity,
276+
minItems: 0,
277+
noItemsMessage: 'No items added',
278+
FormItemProps: {},
279+
ArrayItemProps: {},
280+
FieldsContainerProps: {},
281+
RemoveContainerProps: {},
282+
RemoveButtonProps: {},
283+
FieldArrayRowProps: {},
284+
FieldArrayRowCol: {},
285+
FieldArrayHeaderProps: {},
286+
FieldArrayLabelProps: {},
287+
FieldArrayButtonsProps: {},
288+
UndoButtonProps: {},
289+
RedoButtonProps: {},
290+
AddButtonProps: {},
291+
FieldArrayDescriptionProps: {},
292+
NoItemsMessageProps: {},
293+
ErrorMessageProps: {}
294+
};
295+
296+
export default DynamicArray;

0 commit comments

Comments
 (0)