Skip to content
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

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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
73 changes: 67 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,78 @@ 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")',
hideField: true,
},
{
component: 'text-field',
name: 'field3',
label: 'Field3 (try "123")',
},
{
component: 'text-field',
name: 'field4',
label: 'Field4',
},

{
component: 'text-field',
name: 'field5',
label: 'Field5',
condition: {
when: 'field1',
is: 'cba',
},
},
],
conditions: {
cond1: {
when: 'fieldx',
is: 'abc',
then: {
field4: {
disabled: true,
set: 'New value for field4',
},
field3: {
disabled: true,
},
field2: {
visible: 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
85 changes: 85 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,85 @@
/*
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.
*/

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

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

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

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}) {
obj.forEach(([key, item]) => {
traverse({
obj: item,
fnc,
key,
});
});
}

function traverseObject({obj, fnc, key}) {
Object.entries(obj).forEach(([key, item]) => {
traverse({
obj: item,
fnc,
key,
});
});
}

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;
Loading