Skip to content

Commit 6166bc0

Browse files
authored
Merge pull request #432 from rvsia/enhanceConditions
feat(renderer): enhance conditions
2 parents 8cd0173 + 094db98 commit 6166bc0

File tree

16 files changed

+1219
-77
lines changed

16 files changed

+1219
-77
lines changed

babel.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const camelToSnake = (string) => {
88

99
module.exports = {
1010
presets: ["@babel/preset-env", "@babel/preset-react"],
11-
plugins: ["@babel/plugin-syntax-dynamic-import", "lodash", "@babel/plugin-proposal-class-properties" ],
11+
plugins: ["@babel/plugin-transform-runtime", "@babel/plugin-syntax-dynamic-import", "lodash", "@babel/plugin-proposal-class-properties" ],
1212
env: {
1313
cjs: {
1414
plugins: [

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
}
3838
},
3939
"devDependencies": {
40+
"@babel/plugin-transform-runtime": "^7.9.0",
4041
"@khala/commit-analyzer-wildcard": "^2.4.1",
4142
"@khala/npm-release-monorepo": "^2.4.1",
4243
"@khala/wildcard-release-notes": "^2.4.1",

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const validatorMapper = {
3030
)
3131
};
3232

33+
// eslint-disable-next-line no-unused-vars
3334
const asyncValidatorSchema = {
3435
fields: [
3536
{
@@ -54,6 +55,44 @@ const asyncValidatorSchema = {
5455
]
5556
};
5657

58+
const conditionLogicDemo = {
59+
title: 'Testing Conditions',
60+
description: 'Write X in both',
61+
fields: [
62+
{
63+
name: 'text_box_1',
64+
label: 'Text Box 1',
65+
title: 'Text Box',
66+
component: 'text-field',
67+
clearOnUnmount: true,
68+
condition: {
69+
sequence: [
70+
{ when: 'a', is: 'x', then: { visible: true, set: { password: 'defaultPassword' } }, else: { set: { password: 'no password' } } },
71+
{ when: 'b', is: 'x', then: { visible: true, set: { text_box_1: 'defaultText' } } }
72+
]
73+
}
74+
},
75+
{
76+
name: 'a',
77+
label: 'a',
78+
title: 'a',
79+
component: 'text-field'
80+
},
81+
{
82+
name: 'b',
83+
label: 'b',
84+
title: 'b',
85+
component: 'text-field'
86+
},
87+
{
88+
name: 'password',
89+
label: 'password',
90+
title: 'password',
91+
component: 'text-field'
92+
}
93+
]
94+
};
95+
5796
const App = () => {
5897
// const [values, setValues] = useState({});
5998
return (
@@ -62,7 +101,7 @@ const App = () => {
62101
validatorMapper={validatorMapper}
63102
componentMapper={componentMapper}
64103
onSubmit={(values) => console.log(values)}
65-
schema={asyncValidatorSchema}
104+
schema={conditionLogicDemo}
66105
FormTemplate={FormTemplate}
67106
actionMapper={actionMapper}
68107
schemaValidatorMapper={{

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

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,23 @@ const checkFieldsArray = (obj, objectKey) => {
1616
}
1717
};
1818

19-
const checkCondition = (condition, fieldName) => {
19+
const checkConditionalAction = (type, action, fieldName) => {
20+
if (action.hasOwnProperty('visible') && typeof action.visible !== 'boolean') {
21+
throw new DefaultSchemaError(`
22+
Error occured in field definition with "name" property: "${fieldName}".
23+
'visible' property in action "${type}" has to be a boolean value! Received: ${typeof action.visible}.
24+
`);
25+
}
26+
27+
if (action.hasOwnProperty('set') && (typeof action.set !== 'object' || Array.isArray(action.set))) {
28+
throw new DefaultSchemaError(`
29+
Error occured in field definition with "name" property: "${fieldName}".
30+
'set' property in action "${type}" has to be a object! Received: ${typeof action.visible}, isArray: ${Array.isArray(action.set)}.
31+
`);
32+
}
33+
};
34+
35+
const checkCondition = (condition, fieldName, isRoot) => {
2036
/**
2137
* validate array condition
2238
*/
@@ -38,14 +54,48 @@ const checkCondition = (condition, fieldName) => {
3854
`);
3955
}
4056

57+
if (condition.hasOwnProperty('sequence') && !Array.isArray(condition.sequence)) {
58+
throw new DefaultSchemaError(`
59+
Error occured in field definition with "name" property: "${fieldName}".
60+
'sequence' property in a field condition must be an array! Received: ${typeof condition.sequence}.
61+
`);
62+
}
63+
64+
if (condition.hasOwnProperty('sequence') && !isRoot) {
65+
throw new DefaultSchemaError(`
66+
Error occured in field definition with "name" property: "${fieldName}".
67+
'sequence' condition has to be the root condition: " condition: { sequence: [ ... ]} "
68+
`);
69+
}
70+
71+
if ((condition.hasOwnProperty('then') || condition.hasOwnProperty('else')) && !isRoot) {
72+
throw new DefaultSchemaError(`
73+
Error occured in field definition with "name" property: "${fieldName}".
74+
'then', 'else' condition keys can be included only in root conditions or in a 'sequence'.
75+
`);
76+
}
77+
78+
if (condition.hasOwnProperty('then')) {
79+
checkConditionalAction('then', condition.then, fieldName);
80+
}
81+
82+
if (condition.hasOwnProperty('else')) {
83+
checkConditionalAction('else', condition.else, fieldName);
84+
}
85+
4186
if (typeof condition !== 'object') {
4287
throw new DefaultSchemaError(`
4388
Error occured in field definition with name: "${fieldName}".
4489
Field condition must be an object, received ${Array.isArray(condition) ? 'array' : typeof condition}!
4590
`);
4691
}
4792

48-
if (!condition.hasOwnProperty('and') && !condition.hasOwnProperty('or') && !condition.hasOwnProperty('not')) {
93+
if (
94+
!condition.hasOwnProperty('and') &&
95+
!condition.hasOwnProperty('or') &&
96+
!condition.hasOwnProperty('not') &&
97+
!condition.hasOwnProperty('sequence')
98+
) {
4999
if (!condition.hasOwnProperty('when')) {
50100
throw new DefaultSchemaError(`
51101
Error occured in field definition with "name" property: "${fieldName}".
@@ -85,6 +135,16 @@ const checkCondition = (condition, fieldName) => {
85135
Field condition must have "pattern" of instance "RegExp" or "string"! Instance received: [${condition.pattern.constructor.name}].
86136
`);
87137
}
138+
} else {
139+
['and', 'or', 'not'].forEach((key) => {
140+
if (condition.hasOwnProperty(key)) {
141+
checkCondition(condition[key], fieldName);
142+
}
143+
});
144+
145+
if (condition.hasOwnProperty('sequence')) {
146+
condition.sequence.forEach((item) => checkCondition(item, fieldName, 'root'));
147+
}
88148
}
89149
};
90150

@@ -211,7 +271,7 @@ const iterateOverFields = (fields, componentMapper, validatorTypes, actionTypes,
211271
}
212272

213273
if (field.hasOwnProperty('condition')) {
214-
checkCondition(field.condition, field.name);
274+
checkCondition(field.condition, field.name, 'root');
215275
}
216276

217277
if (field.hasOwnProperty('validate')) {

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

Lines changed: 126 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import React from 'react';
1+
import React, { useEffect, useReducer } from 'react';
22
import PropTypes from 'prop-types';
33
import lodashIsEmpty from 'lodash/isEmpty';
4-
import { FormSpy } from 'react-final-form';
54
import get from 'lodash/get';
5+
import isEqual from 'lodash/isEqual';
6+
7+
import useFormApi from '../files/use-form-api';
68

79
const isEmptyValue = (value) => (typeof value === 'number' || value === true ? false : lodashIsEmpty(value));
810

@@ -27,48 +29,156 @@ const fieldCondition = (value, { is, isNotEmpty, isEmpty, pattern, notMatch, fla
2729
};
2830

2931
export const parseCondition = (condition, values) => {
32+
let positiveResult = {
33+
visible: true,
34+
...condition.then,
35+
result: true
36+
};
37+
38+
let negativeResult = {
39+
visible: false,
40+
...condition.else,
41+
result: false
42+
};
43+
3044
if (Array.isArray(condition)) {
31-
return !condition.map((condition) => parseCondition(condition, values)).some((result) => result === false);
45+
return !condition.map((condition) => parseCondition(condition, values)).some(({ result }) => result === false) ? positiveResult : negativeResult;
3246
}
3347

3448
if (condition.and) {
35-
return condition.and.map((condition) => parseCondition(condition, values)).every((result) => result === true);
49+
return !condition.and.map((condition) => parseCondition(condition, values)).some(({ result }) => result === false)
50+
? positiveResult
51+
: negativeResult;
52+
}
53+
54+
if (condition.sequence) {
55+
return condition.sequence.reduce(
56+
(acc, curr) => {
57+
const result = parseCondition(curr, values);
58+
59+
return {
60+
sets: [...acc.sets, ...(result.set ? [result.set] : [])],
61+
visible: acc.visible || result.visible,
62+
result: acc.result || result.result
63+
};
64+
},
65+
{ ...negativeResult, sets: [] }
66+
);
3667
}
3768

3869
if (condition.or) {
39-
return condition.or.map((condition) => parseCondition(condition, values)).some((result) => result === true);
70+
return condition.or.map((condition) => parseCondition(condition, values)).some(({ result }) => result === true) ? positiveResult : negativeResult;
4071
}
4172

4273
if (condition.not) {
43-
return !parseCondition(condition.not, values);
74+
return !parseCondition(condition.not, values).result ? positiveResult : negativeResult;
4475
}
4576

4677
if (typeof condition.when === 'string') {
47-
return fieldCondition(get(values, condition.when), condition);
78+
return fieldCondition(get(values, condition.when), condition) ? positiveResult : negativeResult;
4879
}
4980

5081
if (Array.isArray(condition.when)) {
51-
return !!condition.when.map((fieldName) => fieldCondition(get(values, fieldName), condition)).find((condition) => !!condition);
82+
return condition.when.map((fieldName) => fieldCondition(get(values, fieldName), condition)).find((condition) => !!condition)
83+
? positiveResult
84+
: negativeResult;
5285
}
5386

54-
return false;
87+
return negativeResult;
5588
};
5689

57-
const Condition = ({ condition, children }) => <FormSpy>{({ values }) => (parseCondition(condition, values) ? children : null)}</FormSpy>;
90+
export const reducer = (state, { type, sets }) => {
91+
switch (type) {
92+
case 'formResetted':
93+
return {
94+
...state,
95+
initial: true
96+
};
97+
case 'rememberSets':
98+
return {
99+
...state,
100+
initial: false,
101+
sets
102+
};
103+
default:
104+
return state;
105+
}
106+
};
107+
108+
const Condition = React.memo(
109+
({ condition, children, values }) => {
110+
const formOptions = useFormApi();
111+
const dirty = formOptions.getState().dirty;
112+
113+
const [state, dispatch] = useReducer(reducer, {
114+
sets: [],
115+
initial: true
116+
});
117+
118+
const conditionResult = parseCondition(condition, values, formOptions);
119+
const setters = conditionResult.set ? [conditionResult.set] : conditionResult.sets;
120+
121+
useEffect(() => {
122+
if (!dirty) {
123+
dispatch({ type: 'formResetted' });
124+
}
125+
}, [dirty]);
126+
127+
useEffect(() => {
128+
if (setters && setters.length > 0 && (state.initial || !isEqual(setters, state.sets))) {
129+
setters.forEach((setter, index) => {
130+
if (setter && (state.initial || !isEqual(setter, state.sets[index]))) {
131+
setTimeout(() => {
132+
formOptions.batch(() => {
133+
Object.entries(setter).forEach(([name, value]) => {
134+
formOptions.change(name, value);
135+
});
136+
});
137+
});
138+
}
139+
});
140+
dispatch({ type: 'rememberSets', sets: setters });
141+
}
142+
}, [setters, state.initial]);
143+
144+
return conditionResult.visible ? children : null;
145+
},
146+
(a, b) => isEqual(a.values, b.values) && isEqual(a.condition, b.condition)
147+
);
58148

59149
const conditionProps = {
60-
when: PropTypes.string.isRequired,
61-
is: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.object, PropTypes.number, PropTypes.bool]).isRequired,
150+
when: PropTypes.string,
151+
is: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.object, PropTypes.number, PropTypes.bool]),
62152
isNotEmpty: PropTypes.bool,
63153
isEmpty: PropTypes.bool,
64-
children: PropTypes.oneOf([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired,
65154
pattern: PropTypes.oneOf([PropTypes.string, PropTypes.instanceOf(RegExp)]),
66-
notMatch: PropTypes.any
155+
notMatch: PropTypes.any,
156+
then: PropTypes.shape({
157+
visible: PropTypes.bool,
158+
set: PropTypes.object
159+
}),
160+
else: PropTypes.shape({
161+
visible: PropTypes.bool,
162+
set: PropTypes.object
163+
})
164+
};
165+
166+
const nestedConditions = {
167+
or: PropTypes.oneOfType([PropTypes.shape(conditionProps), PropTypes.arrayOf(PropTypes.shape(conditionProps))]),
168+
and: PropTypes.oneOfType([PropTypes.shape(conditionProps), PropTypes.arrayOf(PropTypes.shape(conditionProps))]),
169+
not: PropTypes.oneOfType([PropTypes.shape(conditionProps), PropTypes.arrayOf(PropTypes.shape(conditionProps))]),
170+
sequence: PropTypes.arrayOf(PropTypes.shape(conditionProps))
171+
};
172+
173+
const conditionsProps = {
174+
...conditionProps,
175+
...nestedConditions
67176
};
68177

69178
Condition.propTypes = {
70-
condition: PropTypes.oneOfType([PropTypes.shape(conditionProps), PropTypes.arrayOf(PropTypes.shape(conditionProps))]),
71-
children: PropTypes.oneOf([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired
179+
condition: PropTypes.oneOfType([PropTypes.shape(conditionsProps), PropTypes.arrayOf(PropTypes.shape(conditionsProps))]),
180+
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]).isRequired,
181+
values: PropTypes.object.isRequired
72182
};
73183

74184
export default Condition;

0 commit comments

Comments
 (0)