Skip to content

Commit b69cd34

Browse files
committed
First commit on new conditions handler
1 parent 146e2d3 commit b69cd34

File tree

7 files changed

+530
-27
lines changed

7 files changed

+530
-27
lines changed

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

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,63 @@ const fileSchema = {
99
fields: [
1010
{
1111
component: 'text-field',
12-
name: 'file-upload',
13-
type: 'file',
14-
label: 'file upload'
15-
}
16-
]
12+
name: 'field1',
13+
label: 'Field1',
14+
initialValue: '"abc" to trigger condition "cond1"',
15+
type: 'search',
16+
},
17+
{
18+
component: 'text-field',
19+
name: 'field2',
20+
label: 'Field2',
21+
initialValue: '"xyz" to trigger condition "cond1"',
22+
type: 'search',
23+
condition: {when: 'field2', is: 'aaa'},
24+
},
25+
{
26+
component: 'text-field',
27+
name: 'field3',
28+
label: 'Field3',
29+
initialValue: '"123" to trigger condition "cond2"',
30+
type: 'search',
31+
},
32+
{
33+
component: 'text-field',
34+
name: 'field4',
35+
label: 'Field4',
36+
initialValue: 'Visible when field3="aa" (old style condition)',
37+
},
38+
],
39+
conditions: {
40+
cond1: {
41+
when: 'field1',
42+
is: 'abc',
43+
then: {
44+
field3: {
45+
disabled: true,
46+
set: 'New value for field3',
47+
},
48+
field4: {
49+
visible: false,
50+
},
51+
},
52+
},
53+
cond2: {
54+
when: 'field3',
55+
is: '123',
56+
then: {
57+
field1: {
58+
hidden: true,
59+
},
60+
},
61+
},
62+
},
1763
};
1864

1965
const App = () => {
2066
// const [values, setValues] = useState({});
2167
return (
22-
<div style={{ padding: 20 }}>
68+
<div style={{padding: 20}}>
2369
<FormRenderer
2470
componentMapper={componentMapper}
2571
onSubmit={(values, ...args) => console.log(values, args)}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
conditionsMapper will remap a conditions object and create an object with each depending fieldName as a key.
3+
4+
Since one field can be involed in more than one condition, an array of condition references will be created under each fieldName key
5+
6+
Since more than one field can be involved in the same condition, the same condition might be referenced from
7+
several condition arrays.
8+
*/
9+
10+
export const conditionsMapper = ({conditions}) => {
11+
if (!conditions) return {};
12+
13+
function isObject(obj) {
14+
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
15+
}
16+
17+
function isArray(obj) {
18+
return Array.isArray(obj);
19+
}
20+
21+
function traverse({obj, fnc, key}) {
22+
fnc && fnc({obj, key});
23+
24+
if (isArray(obj)) {
25+
traverseArray({
26+
obj,
27+
fnc,
28+
key,
29+
});
30+
} else if (isObject(obj)) {
31+
traverseObject({
32+
obj,
33+
fnc,
34+
key,
35+
});
36+
}
37+
}
38+
39+
function traverseArray({obj, fnc, key}) {
40+
for (var index = 0, len = obj.length; index < len; index++) {
41+
const item = obj[index];
42+
traverse({
43+
obj: item,
44+
fnc,
45+
key: index,
46+
});
47+
}
48+
}
49+
50+
function traverseObject({obj, fnc, key}) {
51+
for (var index in obj) {
52+
if (obj.hasOwnProperty(index)) {
53+
const item = obj[index];
54+
traverse({
55+
obj: item,
56+
fnc,
57+
key: index,
58+
});
59+
}
60+
}
61+
}
62+
63+
const indexedConditions = {};
64+
const conditionArray = Object.entries(conditions);
65+
66+
conditionArray
67+
.map(([key, condition]) => {
68+
return {
69+
key: key,
70+
...condition,
71+
};
72+
})
73+
.forEach(condition => {
74+
traverse({
75+
obj: condition,
76+
fnc: ({obj, key}) => {
77+
if (key === 'when') {
78+
const fieldNames = isArray(obj) ? obj : [obj];
79+
fieldNames.map(fieldName => {
80+
indexedConditions[fieldName] = indexedConditions[fieldName] || [];
81+
indexedConditions[fieldName].push(condition);
82+
});
83+
}
84+
},
85+
});
86+
});
87+
88+
return indexedConditions;
89+
};

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

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useRef } from 'react';
1+
import React, {useState, useRef, useReducer} from 'react';
22
import Form from './form';
33
import arrayMutators from 'final-form-arrays';
44
import PropTypes from 'prop-types';
@@ -9,6 +9,9 @@ import renderForm from '../form-renderer/render-form';
99
import defaultSchemaValidator from './default-schema-validator';
1010
import SchemaErrorComponent from '../form-renderer/schema-error-component';
1111
import defaultValidatorMapper from './validator-mapper';
12+
import RegisterConditions from './register-conditions';
13+
import SetFieldValues from './set-field-values';
14+
import uiStateReducer from './ui-state-reducer';
1215

1316
const FormRenderer = ({
1417
componentMapper,
@@ -26,15 +29,25 @@ const FormRenderer = ({
2629
...props
2730
}) => {
2831
const [fileInputs, setFileInputs] = useState([]);
32+
const [uiState, dispatchCondition] = useReducer(uiStateReducer, {
33+
fields: {},
34+
setFieldValues: {},
35+
});
2936
const focusDecorator = useRef(createFocusDecorator());
3037
let schemaError;
3138

32-
const validatorMapperMerged = { ...defaultValidatorMapper, ...validatorMapper };
39+
const validatorMapperMerged = {...defaultValidatorMapper, ...validatorMapper};
3340

3441
try {
3542
const validatorTypes = Object.keys(validatorMapperMerged);
3643
const actionTypes = actionMapper ? Object.keys(actionMapper) : [];
37-
defaultSchemaValidator(schema, componentMapper, validatorTypes, actionTypes, schemaValidatorMapper);
44+
defaultSchemaValidator(
45+
schema,
46+
componentMapper,
47+
validatorTypes,
48+
actionTypes,
49+
schemaValidatorMapper
50+
);
3851
} catch (error) {
3952
schemaError = error;
4053
console.error(error);
@@ -45,18 +58,24 @@ const FormRenderer = ({
4558
return <SchemaErrorComponent name={schemaError.name} message={schemaError.message} />;
4659
}
4760

48-
const registerInputFile = (name) => setFileInputs((prevFiles) => [...prevFiles, name]);
61+
const registerInputFile = name => setFileInputs(prevFiles => [...prevFiles, name]);
4962

50-
const unRegisterInputFile = (name) => setFileInputs((prevFiles) => [...prevFiles.splice(prevFiles.indexOf(name))]);
63+
const unRegisterInputFile = name =>
64+
setFileInputs(prevFiles => [...prevFiles.splice(prevFiles.indexOf(name))]);
5165

5266
return (
5367
<Form
5468
{...props}
55-
onSubmit={(values, formApi, ...args) => onSubmit(values, { ...formApi, fileInputs }, ...args)}
56-
mutators={{ ...arrayMutators }}
69+
onSubmit={(values, formApi, ...args) => onSubmit(values, {...formApi, fileInputs}, ...args)}
70+
mutators={{...arrayMutators}}
5771
decorators={[focusDecorator.current]}
58-
subscription={{ pristine: true, submitting: true, valid: true, ...subscription }}
59-
render={({ handleSubmit, pristine, valid, form: { reset, mutators, getState, submit, ...form } }) => (
72+
subscription={{pristine: true, submitting: true, valid: true, ...subscription}}
73+
render={({
74+
handleSubmit,
75+
pristine,
76+
valid,
77+
form: {reset, mutators, getState, submit, registerField, ...form},
78+
}) => (
6079
<RendererContext.Provider
6180
value={{
6281
componentMapper,
@@ -73,6 +92,9 @@ const FormRenderer = ({
7392
reset();
7493
},
7594
getState,
95+
registerField,
96+
uiState,
97+
dispatchCondition,
7698
valid,
7799
clearedValue,
78100
submit,
@@ -81,11 +103,14 @@ const FormRenderer = ({
81103
clearOnUnmount,
82104
renderForm,
83105
...mutators,
84-
...form
85-
}
106+
...form,
107+
},
86108
}}
87109
>
110+
<RegisterConditions schema={schema} />
111+
<SetFieldValues />
88112
<FormTemplate formFields={renderForm(schema.fields)} schema={schema} />
113+
<div>{JSON.stringify(uiState, null, 2)}</div>
89114
</RendererContext.Provider>
90115
)}
91116
/>
@@ -98,34 +123,34 @@ FormRenderer.propTypes = {
98123
onReset: PropTypes.func,
99124
schema: PropTypes.object.isRequired,
100125
clearOnUnmount: PropTypes.bool,
101-
subscription: PropTypes.shape({ [PropTypes.string]: PropTypes.bool }),
126+
subscription: PropTypes.shape({[PropTypes.string]: PropTypes.bool}),
102127
clearedValue: PropTypes.any,
103128
componentMapper: PropTypes.shape({
104-
[PropTypes.string]: PropTypes.oneOfType([PropTypes.node, PropTypes.element, PropTypes.func])
129+
[PropTypes.string]: PropTypes.oneOfType([PropTypes.node, PropTypes.element, PropTypes.func]),
105130
}).isRequired,
106131
FormTemplate: PropTypes.func.isRequired,
107132
validatorMapper: PropTypes.shape({
108-
[PropTypes.string]: PropTypes.func
133+
[PropTypes.string]: PropTypes.func,
109134
}),
110135
actionMapper: PropTypes.shape({
111-
[PropTypes.string]: PropTypes.func
136+
[PropTypes.string]: PropTypes.func,
112137
}),
113138
schemaValidatorMapper: PropTypes.shape({
114139
components: PropTypes.shape({
115-
[PropTypes.string]: PropTypes.func
140+
[PropTypes.string]: PropTypes.func,
116141
}),
117142
validators: PropTypes.shape({
118-
[PropTypes.string]: PropTypes.func
143+
[PropTypes.string]: PropTypes.func,
119144
}),
120145
actions: PropTypes.shape({
121-
[PropTypes.string]: PropTypes.func
122-
})
123-
})
146+
[PropTypes.string]: PropTypes.func,
147+
}),
148+
}),
124149
};
125150

126151
FormRenderer.defaultProps = {
127152
initialValues: {},
128-
clearOnUnmount: false
153+
clearOnUnmount: false,
129154
};
130155

131156
export default FormRenderer;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, {useEffect} from 'react';
2+
import {useFormApi} from '../';
3+
import {Field} from 'react-final-form';
4+
5+
import {conditionsMapper} from './conditions-mapper';
6+
import {parseCondition} from '../form-renderer/condition2';
7+
8+
const RegisterConditions = ({schema}) => {
9+
const {getState, registerField, dispatchCondition} = useFormApi();
10+
11+
useEffect(() => {
12+
const indexedConditions = conditionsMapper({conditions: schema.conditions});
13+
console.log(indexedConditions);
14+
15+
//We need an array of conditions, including the fieldName
16+
const unsubscribeFields = Object.entries(indexedConditions)
17+
.map(([fieldName, fieldValue]) => {
18+
return {
19+
fieldName,
20+
...fieldValue,
21+
};
22+
})
23+
.map(field => {
24+
console.log('creating field-listener for condition parsing: ' + field.fieldName);
25+
26+
return registerField(
27+
field.fieldName,
28+
fieldState => {
29+
if (!fieldState || !fieldState.data || !fieldState.data.conditions) return;
30+
31+
console.log('Parsing conditions for field ' + field.fieldName);
32+
33+
fieldState.data.conditions.map(condition => {
34+
const conditionResult = parseCondition(condition, getState().values);
35+
dispatchCondition({
36+
type: 'conditionResult',
37+
source: condition.key,
38+
uiState: conditionResult.uiState,
39+
});
40+
});
41+
},
42+
{value: true, data: true},
43+
{
44+
data: {
45+
conditions: indexedConditions[field.fieldName]
46+
? indexedConditions[field.fieldName]
47+
: null,
48+
},
49+
}
50+
);
51+
});
52+
53+
return () => unsubscribeFields.map(unsubscribeField => unsubscribeField());
54+
}, [schema]);
55+
56+
return null;
57+
};
58+
59+
export default RegisterConditions;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React, {useEffect} from 'react';
2+
import lodashIsEmpty from 'lodash/isEmpty';
3+
4+
import {useFormApi} from '../';
5+
6+
const SetFieldValues = () => {
7+
const {batch, change, uiState, dispatchCondition} = useFormApi();
8+
useEffect(() => {
9+
if (lodashIsEmpty(uiState.setFieldValues)) return;
10+
11+
setTimeout(() => {
12+
batch(() => {
13+
Object.entries(uiState.setFieldValues).forEach(([name, value]) => {
14+
console.log('Setting new value for field ' + name);
15+
change(name, value);
16+
});
17+
dispatchCondition({type: 'fieldValuesUpdated'});
18+
});
19+
});
20+
}, [uiState.setFieldValues]);
21+
22+
return null;
23+
};
24+
25+
export default SetFieldValues;

0 commit comments

Comments
 (0)