Skip to content

New conditions handler (defined globally for schema instead of locally for field) #652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
59 changes: 53 additions & 6 deletions packages/react-form-renderer/demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,64 @@ const fileSchema = {
fields: [
{
component: 'text-field',
name: 'file-upload',
type: 'file',
label: 'file upload'
}
]
name: 'field1',
label: 'Field1 (try "abc")',
},
{
component: 'text-field',
name: 'field2',
label: 'Field2 (try "xyz")',
},
{
component: 'text-field',
name: 'field3',
label: 'Field3 (try "123")',
},
{
component: 'text-field',
name: 'field4',
label: 'Field4',
},
],
conditions: {
cond1: {
when: 'field1',
is: 'abc',
then: {
field4: {
disabled: true,
set: 'New value for field4',
},
field3: {
disabled: true,
},
},
},
cond2: {
when: 'field3',
is: '123',
then: {
field3: {
visible: false,
},
},
},
cond3: {
when: 'field2',
is: 'xyz',
then: {
field3: {
visible: false,
},
},
},
},
};

const App = () => {
// const [values, setValues] = useState({});
return (
<div style={{ padding: 20 }}>
<div style={{padding: 20}}>
<FormRenderer
componentMapper={componentMapper}
onSubmit={(values, ...args) => console.log(values, args)}
Expand Down
89 changes: 89 additions & 0 deletions packages/react-form-renderer/src/files/conditions-mapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
conditionsMapper will remap a conditions object and create an object with each depending fieldName as a key.

Since one field can be involed in more than one condition, an array of condition references will be created under each fieldName key

Since more than one field can be involved in the same condition, the same condition might be referenced from
several condition arrays.
*/

export const conditionsMapper = ({conditions}) => {
if (!conditions) return {};

function isObject(obj) {
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
}

function isArray(obj) {
return Array.isArray(obj);
}

function traverse({obj, fnc, key}) {
fnc && fnc({obj, key});

if (isArray(obj)) {
traverseArray({
obj,
fnc,
key,
});
} else if (isObject(obj)) {
traverseObject({
obj,
fnc,
key,
});
}
}

function traverseArray({obj, fnc, key}) {
for (var index = 0, len = obj.length; index < len; index++) {
const item = obj[index];
traverse({
obj: item,
fnc,
key: index,
});
}
}

function traverseObject({obj, fnc, key}) {
for (var index in obj) {
if (obj.hasOwnProperty(index)) {
const item = obj[index];
traverse({
obj: item,
fnc,
key: index,
});
}
}
}

const indexedConditions = {};
const conditionArray = Object.entries(conditions);

conditionArray
.map(([key, condition]) => {
return {
key: key,
...condition,
};
})
.forEach(condition => {
traverse({
obj: condition,
fnc: ({obj, key}) => {
if (key === 'when') {
const fieldNames = isArray(obj) ? obj : [obj];
fieldNames.map(fieldName => {
indexedConditions[fieldName] = indexedConditions[fieldName] || [];
indexedConditions[fieldName].push(condition);
});
}
},
});
});

return indexedConditions;
};
67 changes: 46 additions & 21 deletions packages/react-form-renderer/src/files/form-renderer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, {useState, useRef, useReducer} from 'react';
import Form from './form';
import arrayMutators from 'final-form-arrays';
import PropTypes from 'prop-types';
Expand All @@ -9,6 +9,9 @@ import renderForm from '../form-renderer/render-form';
import defaultSchemaValidator from './default-schema-validator';
import SchemaErrorComponent from '../form-renderer/schema-error-component';
import defaultValidatorMapper from './validator-mapper';
import RegisterConditions from './register-conditions';
import SetFieldValues from './set-field-values';
import uiStateReducer from './ui-state-reducer';

const FormRenderer = ({
componentMapper,
Expand All @@ -26,15 +29,25 @@ const FormRenderer = ({
...props
}) => {
const [fileInputs, setFileInputs] = useState([]);
const [uiState, dispatchUIState] = useReducer(uiStateReducer, {
fields: {},
setFieldValues: {},
});
const focusDecorator = useRef(createFocusDecorator());
let schemaError;

const validatorMapperMerged = { ...defaultValidatorMapper, ...validatorMapper };
const validatorMapperMerged = {...defaultValidatorMapper, ...validatorMapper};

try {
const validatorTypes = Object.keys(validatorMapperMerged);
const actionTypes = actionMapper ? Object.keys(actionMapper) : [];
defaultSchemaValidator(schema, componentMapper, validatorTypes, actionTypes, schemaValidatorMapper);
defaultSchemaValidator(
schema,
componentMapper,
validatorTypes,
actionTypes,
schemaValidatorMapper
);
} catch (error) {
schemaError = error;
console.error(error);
Expand All @@ -45,18 +58,24 @@ const FormRenderer = ({
return <SchemaErrorComponent name={schemaError.name} message={schemaError.message} />;
}

const registerInputFile = (name) => setFileInputs((prevFiles) => [...prevFiles, name]);
const registerInputFile = name => setFileInputs(prevFiles => [...prevFiles, name]);

const unRegisterInputFile = (name) => setFileInputs((prevFiles) => [...prevFiles.splice(prevFiles.indexOf(name))]);
const unRegisterInputFile = name =>
setFileInputs(prevFiles => [...prevFiles.splice(prevFiles.indexOf(name))]);

return (
<Form
{...props}
onSubmit={(values, formApi, ...args) => onSubmit(values, { ...formApi, fileInputs }, ...args)}
mutators={{ ...arrayMutators }}
onSubmit={(values, formApi, ...args) => onSubmit(values, {...formApi, fileInputs}, ...args)}
mutators={{...arrayMutators}}
decorators={[focusDecorator.current]}
subscription={{ pristine: true, submitting: true, valid: true, ...subscription }}
render={({ handleSubmit, pristine, valid, form: { reset, mutators, getState, submit, ...form } }) => (
subscription={{pristine: true, submitting: true, valid: true, ...subscription}}
render={({
handleSubmit,
pristine,
valid,
form: {reset, mutators, getState, submit, registerField, ...form},
}) => (
<RendererContext.Provider
value={{
componentMapper,
Expand All @@ -73,6 +92,9 @@ const FormRenderer = ({
reset();
},
getState,
registerField,
uiState,
dispatchUIState,
valid,
clearedValue,
submit,
Expand All @@ -81,11 +103,14 @@ const FormRenderer = ({
clearOnUnmount,
renderForm,
...mutators,
...form
}
...form,
},
}}
>
<RegisterConditions schema={schema} />
<SetFieldValues />
<FormTemplate formFields={renderForm(schema.fields)} schema={schema} />
<pre>{JSON.stringify(uiState, null, 2)}</pre>
</RendererContext.Provider>
)}
/>
Expand All @@ -98,34 +123,34 @@ FormRenderer.propTypes = {
onReset: PropTypes.func,
schema: PropTypes.object.isRequired,
clearOnUnmount: PropTypes.bool,
subscription: PropTypes.shape({ [PropTypes.string]: PropTypes.bool }),
subscription: PropTypes.shape({[PropTypes.string]: PropTypes.bool}),
clearedValue: PropTypes.any,
componentMapper: PropTypes.shape({
[PropTypes.string]: PropTypes.oneOfType([PropTypes.node, PropTypes.element, PropTypes.func])
[PropTypes.string]: PropTypes.oneOfType([PropTypes.node, PropTypes.element, PropTypes.func]),
}).isRequired,
FormTemplate: PropTypes.func.isRequired,
validatorMapper: PropTypes.shape({
[PropTypes.string]: PropTypes.func
[PropTypes.string]: PropTypes.func,
}),
actionMapper: PropTypes.shape({
[PropTypes.string]: PropTypes.func
[PropTypes.string]: PropTypes.func,
}),
schemaValidatorMapper: PropTypes.shape({
components: PropTypes.shape({
[PropTypes.string]: PropTypes.func
[PropTypes.string]: PropTypes.func,
}),
validators: PropTypes.shape({
[PropTypes.string]: PropTypes.func
[PropTypes.string]: PropTypes.func,
}),
actions: PropTypes.shape({
[PropTypes.string]: PropTypes.func
})
})
[PropTypes.string]: PropTypes.func,
}),
}),
};

FormRenderer.defaultProps = {
initialValues: {},
clearOnUnmount: false
clearOnUnmount: false,
};

export default FormRenderer;
73 changes: 73 additions & 0 deletions packages/react-form-renderer/src/files/register-conditions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, {useEffect} from 'react';
import {useFormApi} from '../';
import {Field} from 'react-final-form';

import {conditionsMapper} from './conditions-mapper';
import {parseCondition} from '../form-renderer/condition2';

const RegisterConditions = ({schema}) => {
const {getState, registerField, dispatchUIState} = useFormApi();

useEffect(() => {
const indexedConditions = conditionsMapper({conditions: schema.conditions});
console.log(indexedConditions);

//We need an array of conditions, including the fieldName
const unsubscribeFields = Object.entries(indexedConditions)
.map(([fieldName, fieldValue]) => {
return {
fieldName,
...fieldValue,
};
})
.map(field => {
console.log('creating field-listener for condition parsing: ' + field.fieldName);

return registerField(
field.fieldName,
fieldState => {
if (!fieldState || !fieldState.data || !fieldState.data.conditions) return;

console.log('Parsing conditions for field ' + field.fieldName);

fieldState.data.conditions.map(condition => {
const conditionResult = parseCondition(condition, getState().values);
const {
uiState: {add, remove},
} = conditionResult;

if (add) {
dispatchUIState({
type: 'addUIState',
source: condition.key,
uiState: add,
});
}

if (remove) {
dispatchUIState({
type: 'removeUIState',
source: condition.key,
uiState: remove,
});
}
});
},
{value: true, data: true},
{
data: {
conditions: indexedConditions[field.fieldName]
? indexedConditions[field.fieldName]
: null,
},
}
);
});

return () => unsubscribeFields.map(unsubscribeField => unsubscribeField());
}, [schema]);

return null;
};

export default RegisterConditions;
Loading