Skip to content

Commit 14de368

Browse files
feat(renderer): enable condition set as a function
1 parent a947680 commit 14de368

File tree

8 files changed

+256
-53
lines changed

8 files changed

+256
-53
lines changed

packages/react-form-renderer/demo/index.js

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,59 +7,35 @@ import FormTemplate from './form-template';
77
import mapper from './form-fields-mapper';
88

99
const schema = {
10-
title: 'Sequence condition',
10+
title: 'Set action',
1111
fields: [
1212
{
1313
component: 'text-field',
14-
name: 'field-1',
15-
label: 'first name',
14+
name: 'useDefaultNickName',
15+
label: 'Do you want to use default nickname?',
16+
description: 'set: {} is used to reset the setter',
1617
},
1718
{
1819
component: 'text-field',
19-
name: 'field-2',
20-
label: 'last name',
21-
},
22-
{
23-
component: 'text-field',
24-
name: 'field-3',
25-
label: 'occupation',
20+
name: 'nickname',
21+
label: 'Nickname',
2622
condition: {
27-
sequence: [
28-
{
29-
and: [
30-
{ when: 'field-1', is: 'james' },
31-
{ when: 'field-2', is: 'bond' },
32-
],
33-
then: { set: { 'field-3': 'SPY' } },
34-
else: { visible: true },
35-
},
36-
{
37-
and: [
38-
{ when: 'field-1', is: 'steve' },
39-
{ when: 'field-2', is: 'jobs' },
40-
],
41-
then: { set: { 'field-3': 'CEO' } },
42-
else: { visible: true },
23+
when: 'useDefaultNickName',
24+
is: 'a',
25+
then: {
26+
set: (formState, getFieldState) => {
27+
return { nickname: formState.values.useDefaultNickName };
4328
},
44-
],
29+
},
30+
else: { visible: true, set: {} },
4531
},
4632
},
4733
],
4834
};
4935
const App = () => {
5036
return (
5137
<div style={{ padding: 20 }}>
52-
<FormRenderer
53-
initialValues={{
54-
'field-1': 'steve',
55-
'field-2': 'jobs',
56-
'field-3': 'RETIRED',
57-
}}
58-
componentMapper={mapper}
59-
onSubmit={console.log}
60-
FormTemplate={FormTemplate}
61-
schema={schema}
62-
/>
38+
<FormRenderer componentMapper={mapper} onSubmit={console.log} FormTemplate={FormTemplate} schema={schema} />
6339
</div>
6440
);
6541
};

packages/react-form-renderer/src/condition/condition.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import Field from "../common-types/field";
2-
2+
import type { FormState, FormApi } from "final-form";
3+
34
export interface ActionResolution {
45
visible?: boolean;
5-
set?: object; // TO DO specify this
6+
set?: object | ((formState:FormState<Record<string, any>>, getFieldState:FormApi["getFieldState"]) => object) ; // TO DO specify this
67
}
78

89
export type InnerWhenFunction = (currentField: string) => string;

packages/react-form-renderer/src/condition/condition.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
import React, { useEffect, useReducer } from 'react';
1+
import React, { useCallback, useEffect, useReducer } from 'react';
22
import PropTypes from 'prop-types';
33
import isEqual from 'lodash/isEqual';
44

55
import useFormApi from '../use-form-api';
66
import parseCondition from '../parse-condition';
77

8+
const setterValueCheck = (setterValue) => {
9+
if (setterValue === null || Array.isArray(setterValue)) {
10+
console.error('Received invalid setterValue. Expected object, received: ', setterValue);
11+
12+
return false;
13+
}
14+
15+
return typeof setterValue === 'object';
16+
};
17+
818
export const reducer = (state, { type, sets }) => {
919
switch (type) {
1020
case 'formResetted':
@@ -42,6 +52,12 @@ const Condition = React.memo(
4252
}
4353
}, [dirty]);
4454

55+
const setValue = useCallback((setter) => {
56+
Object.entries(setter).forEach(([name, value]) => {
57+
formOptions.change(name, value);
58+
});
59+
}, []);
60+
4561
useEffect(() => {
4662
if (setters && setters.length > 0 && (state.initial || !isEqual(setters, state.sets))) {
4763
setters.forEach((setter, index) => {
@@ -60,9 +76,17 @@ const Condition = React.memo(
6076
*/
6177
if (!meta || isFormModified || typeof meta.initial === 'undefined') {
6278
formOptions.batch(() => {
63-
Object.entries(setter).forEach(([name, value]) => {
64-
formOptions.change(name, value);
65-
});
79+
if (typeof setter !== 'function') {
80+
setValue(setter);
81+
} else {
82+
const setterValue = setter(formOptions.getState(), formOptions.getFieldState);
83+
84+
if (setterValueCheck(setterValue)) {
85+
setValue(setterValue);
86+
} else {
87+
console.error('Received invalid setterValue. Expected object, received: ', setterValue);
88+
}
89+
}
6690
});
6791
}
6892
});
@@ -101,11 +125,11 @@ const conditionProps = {
101125
notMatch: PropTypes.any,
102126
then: PropTypes.shape({
103127
visible: PropTypes.bool,
104-
set: PropTypes.object,
128+
set: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
105129
}),
106130
else: PropTypes.shape({
107131
visible: PropTypes.bool,
108-
set: PropTypes.object,
132+
set: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
109133
}),
110134
};
111135

packages/react-form-renderer/src/default-schema-validator/default-schema-validator.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const checkConditionalAction = (type, action, fieldName) => {
2424
`);
2525
}
2626

27-
if (action.hasOwnProperty('set') && (typeof action.set !== 'object' || Array.isArray(action.set))) {
27+
if (action.hasOwnProperty('set') && ((typeof action.set !== 'object' && typeof action.set !== 'function') || Array.isArray(action.set))) {
2828
throw new DefaultSchemaError(`
2929
Error occured in field definition with "name" property: "${fieldName}".
3030
'set' property in action "${type}" has to be a object! Received: ${typeof action.visible}, isArray: ${Array.isArray(action.set)}.

packages/react-form-renderer/src/tests/form-renderer/condition.test.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,150 @@ describe('condition test', () => {
531531
await waitFor(() => expect(screen.getByLabelText('field2')).toHaveValue('set with then'));
532532
});
533533

534+
it('should change fields value by set funciton', async () => {
535+
const schema = {
536+
fields: [
537+
{
538+
component: 'text-field',
539+
name: 'field1',
540+
label: 'field1',
541+
},
542+
{
543+
component: 'text-field',
544+
name: 'field2',
545+
label: 'field2',
546+
condition: {
547+
when: 'field1',
548+
is: 'foo',
549+
then: {
550+
set: (formState) => {
551+
return { field2: formState.values.field1 };
552+
},
553+
},
554+
else: { visible: true, set: {} },
555+
},
556+
},
557+
],
558+
};
559+
560+
render(<FormRenderer {...initialProps} schema={schema} />);
561+
expect(screen.getByLabelText('field2')).toHaveValue('');
562+
563+
await userEvent.type(screen.getByLabelText('field1'), 'foo');
564+
await waitFor(() => expect(screen.getByLabelText('field2')).toHaveValue('foo'));
565+
});
566+
567+
it('check the set function received valid arguments', async () => {
568+
const setSpy = jest.fn();
569+
570+
setSpy.mockImplementation(() => ({}));
571+
572+
const schema = {
573+
fields: [
574+
{
575+
component: 'text-field',
576+
name: 'field1',
577+
label: 'field1',
578+
},
579+
{
580+
component: 'text-field',
581+
name: 'field2',
582+
label: 'field2',
583+
condition: {
584+
when: 'field1',
585+
is: 'foo',
586+
then: {
587+
set: setSpy,
588+
},
589+
else: { visible: true, set: {} },
590+
},
591+
},
592+
],
593+
};
594+
595+
render(<FormRenderer {...initialProps} schema={schema} />);
596+
597+
await userEvent.type(screen.getByLabelText('field1'), 'foo');
598+
599+
await waitFor(() => expect(setSpy).toHaveBeenCalledTimes(1));
600+
601+
const expected = { active: 'field1', dirty: true };
602+
603+
await waitFor(() => expect(setSpy).toHaveBeenCalledWith(expect.objectContaining(expected), expect.any(Function)));
604+
});
605+
606+
it('check the object', async () => {
607+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
608+
609+
const schema = {
610+
fields: [
611+
{
612+
component: 'text-field',
613+
name: 'field1',
614+
label: 'field1',
615+
},
616+
{
617+
component: 'text-field',
618+
name: 'field2',
619+
label: 'field2',
620+
condition: {
621+
when: 'field1',
622+
is: 'foo',
623+
then: {
624+
set: (formState) => {
625+
return null;
626+
},
627+
},
628+
else: { visible: true, set: {} },
629+
},
630+
},
631+
],
632+
};
633+
634+
render(<FormRenderer {...initialProps} schema={schema} />);
635+
636+
await userEvent.type(screen.getByLabelText('field1'), 'foo');
637+
638+
await waitFor(() => {
639+
expect(errorSpy).toHaveBeenCalled();
640+
expect(console.error.mock.calls[0][0]).toContain('Received invalid setterValue. Expected object, received: ');
641+
});
642+
});
643+
644+
it('check the getFieldState object', async () => {
645+
const schema = {
646+
fields: [
647+
{
648+
component: 'text-field',
649+
name: 'field1',
650+
label: 'field1',
651+
},
652+
{
653+
component: 'text-field',
654+
name: 'field2',
655+
label: 'field2',
656+
condition: {
657+
when: 'field1',
658+
is: 'foo',
659+
then: {
660+
set: (_formState, getFieldState) => {
661+
return { field2: getFieldState('field1').value };
662+
},
663+
},
664+
else: { visible: true, set: {} },
665+
},
666+
},
667+
],
668+
};
669+
render(<FormRenderer {...initialProps} schema={schema} />);
670+
671+
await userEvent.type(screen.getByLabelText('field1'), 'foo');
672+
673+
await waitFor(() => {
674+
expect(screen.getByLabelText('field2')).toHaveValue('foo');
675+
});
676+
});
677+
534678
describe('reducer', () => {
535679
it('returns default', () => {
536680
const initialState = {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
import FormRenderer from '@data-driven-forms/react-form-renderer/form-renderer';
3+
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
4+
5+
import Checkbox from '@data-driven-forms/mui-component-mapper/checkbox';
6+
import TextField from '@data-driven-forms/mui-component-mapper/text-field';
7+
import FormTemplate from '@data-driven-forms/mui-component-mapper/form-template';
8+
9+
const schema = {
10+
fields: [
11+
{
12+
component: componentTypes.TEXT_FIELD,
13+
name: 'firstname',
14+
label: 'Firstname',
15+
description: 'Type Bob to set nickname to Bob',
16+
},
17+
{
18+
component: componentTypes.TEXT_FIELD,
19+
name: 'nickname',
20+
label: 'Nickname',
21+
condition: {
22+
when: 'firstname',
23+
is: 'Bob',
24+
then: {
25+
set: (formState) => {
26+
return { nickname: formState.values.firstname };
27+
},
28+
},
29+
else: { visible: true, set: {} },
30+
},
31+
},
32+
],
33+
};
34+
35+
const componentMapper = {
36+
[componentTypes.CHECKBOX]: Checkbox,
37+
[componentTypes.TEXT_FIELD]: TextField,
38+
};
39+
40+
const SetFunction = () => <FormRenderer FormTemplate={FormTemplate} componentMapper={componentMapper} schema={schema} onSubmit={console.log} />;
41+
42+
export default SetFunction;

packages/react-renderer-demo/src/next.config.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,16 @@ module.exports = withBundleAnalyzer(
5757
...config.resolve.alias,
5858
...(process.env.DEPLOY === 'true'
5959
? {
60-
"@emotion/react": path.resolve(__dirname, '../node_modules/@emotion/react'),
61-
"@emotion/server": path.resolve(__dirname, '../node_modules/@emotion/server'),
62-
"@emotion/styled": path.resolve(__dirname, '../node_modules/@emotion/styled'),
60+
'@emotion/react': path.resolve(__dirname, '../node_modules/@emotion/react'),
61+
'@emotion/server': path.resolve(__dirname, '../node_modules/@emotion/server'),
62+
'@emotion/styled': path.resolve(__dirname, '../node_modules/@emotion/styled'),
6363
}
6464
: {
6565
react: path.resolve(__dirname, '../../../node_modules/react'),
6666
'react-dom': path.resolve(__dirname, '../../../node_modules/react-dom'),
67-
"@emotion/react": path.resolve(__dirname, '../../../node_modules/@emotion/react'),
68-
"@emotion/server": path.resolve(__dirname, '../../../node_modules/@emotion/server'),
69-
"@emotion/styled": path.resolve(__dirname, '../../../node_modules/@emotion/styled'),
67+
'@emotion/react': path.resolve(__dirname, '../../../node_modules/@emotion/react'),
68+
'@emotion/server': path.resolve(__dirname, '../../../node_modules/@emotion/server'),
69+
'@emotion/styled': path.resolve(__dirname, '../../../node_modules/@emotion/styled'),
7070
}),
7171

7272
'@docs/doc-components': path.resolve(__dirname, './doc-components'),

0 commit comments

Comments
 (0)