Skip to content

Commit

Permalink
feat(renderer): allow condition mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
Hyperkid123 committed Nov 7, 2023
1 parent c10cf30 commit 24db50a
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 17 deletions.
31 changes: 30 additions & 1 deletion packages/react-form-renderer/demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@ import mapper from './form-fields-mapper';

const schema = {
fields: [
{
name: 'field1',
label: 'Field 1',
component: 'text-field',
},
{
name: 'mapped-condition',
label: 'Mapped Condition',
component: 'text-field',
condition: {
mappedAttributes: {
is: 'nameFn',
},
when: 'field1',
is: ['John'],
},
},
{
name: 'formRadio',
label: 'SelectSubForm',
Expand Down Expand Up @@ -107,12 +124,24 @@ const initialValues = {
formRadio: 'form2',
radioBtn2: 'stu',
txtField3: 'data',
field1: 'John'
};

const App = () => {
return (
<div style={{ padding: 20 }}>
<FormRenderer initialValues={initialValues} componentMapper={mapper} onSubmit={console.log} FormTemplate={FormTemplate} schema={schema} />
<FormRenderer
conditionMapper={{
nameFn: (value, conditionConfig) => {
return value === 'John';
},
}}
initialValues={initialValues}
componentMapper={mapper}
onSubmit={console.log}
FormTemplate={FormTemplate}
schema={schema}
/>
</div>
);
};
Expand Down
5 changes: 5 additions & 0 deletions packages/react-form-renderer/src/condition/condition.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export interface ConditionProp {
}

export interface ConditionDefinition extends ConditionProp {
mappedAttributes?: {
is?: string;
when?: string;
set?: string;
},
or?: ConditionProp | ConditionProp[];
and?: ConditionProp | ConditionProp[];
not?: ConditionProp | ConditionProp[];
Expand Down
10 changes: 7 additions & 3 deletions packages/react-form-renderer/src/condition/condition.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useCallback, useEffect, useMemo, useReducer } from 'react';
import { useCallback, useContext, useEffect, useMemo, useReducer } from 'react';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';

import useFormApi from '../use-form-api';
import parseCondition from '../parse-condition';
import RendererContext from '../renderer-context/renderer-context';

const setterValueCheck = (setterValue) => {
if (setterValue === null || Array.isArray(setterValue)) {
Expand Down Expand Up @@ -36,15 +37,18 @@ export const reducer = (state, { type, sets }) => {
const Condition = ({ condition, children, field }) => {
const formOptions = useFormApi();
const formState = formOptions.getState();

const { conditionMapper } = useContext(RendererContext);
const [state, dispatch] = useReducer(reducer, {
sets: [],
initial: true,
});

// It is required to get the context state values from in order to get the latest state.
// Using the trigger values can cause issues with the radio field as each input is registered separately to state and does not yield the actual field value.
const conditionResult = useMemo(() => parseCondition(condition, formState.values, field), [formState.values, condition, field]);
const conditionResult = useMemo(
() => parseCondition(condition, formState.values, field, conditionMapper),
[formState.values, condition, field, conditionMapper]
);

const setters = conditionResult.set ? [conditionResult.set] : conditionResult.sets;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FieldState } from "final-form";

export interface ConditionMapper {
[key: string]: (args: any[], fieldState: FieldState<any>) => boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ActionMapper } from './action-mapper';
import SchemaValidatorMapper from '../common-types/schema-validator-mapper';
import { FormTemplateRenderProps } from '../common-types/form-template-render-props';
import { NoIndex } from '../common-types/no-index';
import { ConditionMapper } from './condition-mapper';

export interface FormRendererProps<
FormValues = Record<string, any>,
Expand All @@ -25,6 +26,7 @@ export interface FormRendererProps<
FormTemplate?: ComponentType<FormTemplateProps> | FunctionComponent<FormTemplateProps>;
validatorMapper?: ValidatorMapper;
actionMapper?: ActionMapper;
conditionMapper?: ConditionMapper;
schemaValidatorMapper?: SchemaValidatorMapper;
FormTemplateProps?: Partial<FormTemplateProps>;
children?: ReactNode | ((props: FormTemplateRenderProps) => ReactNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const FormRenderer = ({
clearedValue,
clearOnUnmount,
componentMapper,
conditionMapper = {},
decorators,
FormTemplate,
FormTemplateProps,
Expand Down Expand Up @@ -148,6 +149,7 @@ const FormRenderer = ({
componentMapper,
validatorMapper: validatorMapperMerged,
actionMapper,
conditionMapper,
formOptions: {
registerInputFile,
unRegisterInputFile,
Expand Down Expand Up @@ -220,6 +222,9 @@ FormRenderer.propTypes = {
initialValues: PropTypes.object,
decorators: PropTypes.array,
mutators: PropTypes.object,
conditionMapper: PropTypes.shape({
[PropTypes.string]: PropTypes.func,
}),
};

FormRenderer.defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AnyObject } from "../common-types/any-object";
import { ConditionDefinition } from "../condition";
import Field from "../common-types/field";
import { ConditionMapper } from "../form-renderer/condition-mapper";

export type ParseCondition = (condition: ConditionDefinition, values: AnyObject, Field: Field) => void;
export type ParseCondition = (condition: ConditionDefinition, values: AnyObject, Field: Field, conditionMapper?: ConditionMapper) => void;
declare const parseCondition: ParseCondition
export default parseCondition;
56 changes: 44 additions & 12 deletions packages/react-form-renderer/src/parse-condition/parse-condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,37 @@ const fieldCondition = (value, config) => {
return config.notMatch ? !isMatched : isMatched;
};

export const parseCondition = (condition, values, field) => {
const allowedMappedAttributes = ['when', 'is'];

export const unpackMappedCondition = (condition, conditionMapper) => {
if (typeof condition.mappedAttributes !== 'object') {
return condition;
}

const { mappedAttributes } = condition;

const internalCondition = {
...condition,
mappedAttributes: undefined,
};

Object.entries(mappedAttributes).forEach(([key, value]) => {
if (!allowedMappedAttributes.includes(key)) {
console.error(`Mapped condition attribute ${key} is not allowed! Allowed attributes are: ${allowedMappedAttributes.join(', ')}`);
return;
}

if (conditionMapper[value]) {
internalCondition[key] = conditionMapper[value];
} else {
console.error(`Missing conditionMapper entry for ${value}!`);
}
});

return internalCondition;
};

export const parseCondition = (condition, values, field, conditionMapper = {}) => {
let positiveResult = {
visible: true,
...condition.then,
Expand All @@ -62,14 +92,16 @@ export const parseCondition = (condition, values, field) => {
: negativeResult;
}

if (condition.and) {
return !condition.and.map((condition) => parseCondition(condition, values, field)).some(({ result }) => result === false)
const conditionInternal = unpackMappedCondition(condition, conditionMapper);

if (conditionInternal.and) {
return !conditionInternal.and.map((condition) => parseCondition(condition, values, field)).some(({ result }) => result === false)
? positiveResult
: negativeResult;
}

if (condition.sequence) {
return condition.sequence.reduce(
if (conditionInternal.sequence) {
return conditionInternal.sequence.reduce(
(acc, curr) => {
const result = parseCondition(curr, values, field);

Expand All @@ -83,25 +115,25 @@ export const parseCondition = (condition, values, field) => {
);
}

if (condition.or) {
return condition.or.map((condition) => parseCondition(condition, values, field)).some(({ result }) => result === true)
if (conditionInternal.or) {
return conditionInternal.or.map((condition) => parseCondition(condition, values, field)).some(({ result }) => result === true)
? positiveResult
: negativeResult;
}

if (condition.not) {
return !parseCondition(condition.not, values, field).result ? positiveResult : negativeResult;
if (conditionInternal.not) {
return !parseCondition(conditionInternal.not, values, field).result ? positiveResult : negativeResult;
}

const finalWhen = typeof condition.when === 'function' ? condition.when(field) : condition.when;
const finalWhen = typeof conditionInternal.when === 'function' ? conditionInternal.when(field) : conditionInternal.when;

if (typeof finalWhen === 'string') {
return fieldCondition(get(values, finalWhen), condition) ? positiveResult : negativeResult;
return fieldCondition(get(values, finalWhen), conditionInternal) ? positiveResult : negativeResult;
}

if (Array.isArray(finalWhen)) {
return finalWhen
.map((fieldName) => fieldCondition(get(values, typeof fieldName === 'function' ? fieldName(field) : fieldName), condition))
.map((fieldName) => fieldCondition(get(values, typeof fieldName === 'function' ? fieldName(field) : fieldName), conditionInternal))
.find((condition) => !!condition)
? positiveResult
: negativeResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ActionMapper } from '../form-renderer';
import Field from '../common-types/field';
import { AnyObject } from '../common-types/any-object';
import Schema from '../common-types/schema';
import { ConditionMapper } from '../form-renderer/condition-mapper';

export interface FormOptions<FormValues = Record<string, any>, InitialFormValues = Partial<FormValues>>
extends FormApi<FormValues, InitialFormValues> {
Expand All @@ -29,6 +30,7 @@ export interface RendererContextValue {
validatorMapper: ValidatorMapper;
actionMapper: ActionMapper;
formOptions: FormOptions;
conditionMapper: ConditionMapper;
}

declare const RendererContext: React.Context<RendererContextValue>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,4 +684,71 @@ describe('parseCondition', () => {
expect(parseCondition(condition, values)).toEqual(negativeResult);
});
});

describe('mapped attributes', () => {
const conditionMapper = {
whenFn: jest.fn().mockImplementation(() => 'x'),
isFn: jest.fn().mockImplementation((value) => value === true),
setFn: jest.fn().mockImplementation((value) => ({ y: value === true ? 'yes' : 'no' })),
};

positiveResult = { visible: true, result: true };
negativeResult = { visible: false, result: false };

[positiveResult, negativeResult].forEach((conditionResult) => {
const values = {
x: true,
};
it(`maps attribute - when - ${conditionResult.result ? 'positive' : 'negative'}`, () => {
const condition = {
mappedAttributes: {
when: 'whenFn',
},
is: conditionResult.result,
};

expect(parseCondition(condition, values, undefined, conditionMapper)).toEqual(conditionResult);
});

it(`maps attribute - is - ${conditionResult.result ? 'positive' : 'negative'}`, () => {
const condition = {
mappedAttributes: {
is: 'isFn',
},
when: 'x',
};
expect(parseCondition(condition, { x: conditionResult.result }, undefined, conditionMapper)).toEqual(conditionResult);
});
});

it('should log an error if conditionMapper is missing mapped attribute', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const conditionMapper = {};
const condition = {
mappedAttributes: {
when: 'whenFn',
},
is: true,
};
expect(parseCondition(condition, { x: true }, undefined, conditionMapper)).toEqual(negativeResult);
expect(errorSpy).toHaveBeenCalledWith('Missing conditionMapper entry for whenFn!');
errorSpy.mockRestore();
});

it('should log an error if mapped attribute is not allowed', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const conditionMapper = {};
const condition = {
mappedAttributes: {
when: 'whenFn',
is: 'isFn',
not: 'notFn',
},
is: true,
};
expect(parseCondition(condition, { x: true }, undefined, conditionMapper)).toEqual(negativeResult);
expect(errorSpy).toHaveBeenCalledWith('Mapped condition attribute not is not allowed! Allowed attributes are: when, is');
errorSpy.mockRestore();
});
});
});

0 comments on commit 24db50a

Please sign in to comment.