Skip to content

Commit e1b5f8d

Browse files
authored
Merge pull request #1424 from data-driven-forms/mapped-attibutes-condition
feat(renderer): allow condition mapping
2 parents c10cf30 + 12e1201 commit e1b5f8d

File tree

16 files changed

+373
-29
lines changed

16 files changed

+373
-29
lines changed

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ import mapper from './form-fields-mapper';
88

99
const schema = {
1010
fields: [
11+
{
12+
name: 'field1',
13+
label: 'Field 1',
14+
component: 'text-field',
15+
},
16+
{
17+
name: 'mapped-condition',
18+
label: 'Mapped Condition',
19+
component: 'text-field',
20+
condition: {
21+
mappedAttributes: {
22+
is: ['nameFn', 'John', 'Doe'],
23+
},
24+
when: 'field1',
25+
},
26+
},
1127
{
1228
name: 'formRadio',
1329
label: 'SelectSubForm',
@@ -107,12 +123,26 @@ const initialValues = {
107123
formRadio: 'form2',
108124
radioBtn2: 'stu',
109125
txtField3: 'data',
126+
field1: 'John',
110127
};
111128

112129
const App = () => {
113130
return (
114131
<div style={{ padding: 20 }}>
115-
<FormRenderer initialValues={initialValues} componentMapper={mapper} onSubmit={console.log} FormTemplate={FormTemplate} schema={schema} />
132+
<FormRenderer
133+
conditionMapper={{
134+
nameFn: (name, _surname) => {
135+
return (value, _conditionConfig) => {
136+
return value === name;
137+
};
138+
},
139+
}}
140+
initialValues={initialValues}
141+
componentMapper={mapper}
142+
onSubmit={console.log}
143+
FormTemplate={FormTemplate}
144+
schema={schema}
145+
/>
116146
</div>
117147
);
118148
};

packages/react-form-renderer/src/condition/condition.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export interface ConditionProp {
2424
}
2525

2626
export interface ConditionDefinition extends ConditionProp {
27+
mappedAttributes?: {
28+
is?: string;
29+
when?: string;
30+
set?: string;
31+
},
2732
or?: ConditionProp | ConditionProp[];
2833
and?: ConditionProp | ConditionProp[];
2934
not?: ConditionProp | ConditionProp[];

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useCallback, useEffect, useMemo, useReducer } from 'react';
1+
import { useCallback, useContext, useEffect, useMemo, useReducer } from 'react';
22
import PropTypes from 'prop-types';
33
import isEqual from 'lodash/isEqual';
44

55
import useFormApi from '../use-form-api';
66
import parseCondition from '../parse-condition';
7+
import RendererContext from '../renderer-context/renderer-context';
78

89
const setterValueCheck = (setterValue) => {
910
if (setterValue === null || Array.isArray(setterValue)) {
@@ -36,15 +37,18 @@ export const reducer = (state, { type, sets }) => {
3637
const Condition = ({ condition, children, field }) => {
3738
const formOptions = useFormApi();
3839
const formState = formOptions.getState();
39-
40+
const { conditionMapper } = useContext(RendererContext);
4041
const [state, dispatch] = useReducer(reducer, {
4142
sets: [],
4243
initial: true,
4344
});
4445

4546
// It is required to get the context state values from in order to get the latest state.
4647
// 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.
47-
const conditionResult = useMemo(() => parseCondition(condition, formState.values, field), [formState.values, condition, field]);
48+
const conditionResult = useMemo(
49+
() => parseCondition(condition, formState.values, field, conditionMapper),
50+
[formState.values, condition, field, conditionMapper]
51+
);
4852

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

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ const checkConditionalAction = (type, action, fieldName) => {
3232
}
3333
};
3434

35+
const requiredOneOf = ['is', 'isEmpty', 'isNotEmpty', 'pattern', 'greaterThan', 'greaterThanOrEqualTo', 'lessThan', 'lessThanOrEqualTo'];
36+
37+
const checkMappedAttributes = (condition) => {
38+
const hasStaticAttribute = requiredOneOf.some((key) => condition.hasOwnProperty(key));
39+
if (hasStaticAttribute) {
40+
return true;
41+
}
42+
43+
if (
44+
condition.hasOwnProperty('mappedAttributes') &&
45+
typeof condition.mappedAttributes === 'object' &&
46+
!Array.isArray(condition.mappedAttributes) &&
47+
condition.mappedAttributes !== null
48+
) {
49+
return requiredOneOf.some((key) => condition.mappedAttributes.hasOwnProperty(key));
50+
}
51+
};
52+
3553
const checkCondition = (condition, fieldName, isRoot) => {
3654
/**
3755
* validate array condition
@@ -96,30 +114,22 @@ const checkCondition = (condition, fieldName, isRoot) => {
96114
!condition.hasOwnProperty('not') &&
97115
!condition.hasOwnProperty('sequence')
98116
) {
99-
if (!condition.hasOwnProperty('when')) {
117+
const isWhenMapped = condition.hasOwnProperty('mappedAttributes') && condition.mappedAttributes?.hasOwnProperty('when');
118+
if (!condition.hasOwnProperty('when') && !isWhenMapped) {
100119
throw new DefaultSchemaError(`
101120
Error occured in field definition with "name" property: "${fieldName}".
102121
Field condition must have "when" property! Properties received: [${Object.keys(condition)}].
103122
`);
104123
}
105124

106-
if (!(typeof condition.when === 'string' || typeof condition.when === 'function' || Array.isArray(condition.when))) {
125+
if (!isWhenMapped && !(typeof condition.when === 'string' || typeof condition.when === 'function' || Array.isArray(condition.when))) {
107126
throw new DefaultSchemaError(`
108127
Error occured in field definition with name: "${fieldName}".
109128
Field condition property "when" must be of type "string", "function" or "array", ${typeof condition.when} received!].
110129
`);
111130
}
112131

113-
if (
114-
!condition.hasOwnProperty('is') &&
115-
!condition.hasOwnProperty('isEmpty') &&
116-
!condition.hasOwnProperty('isNotEmpty') &&
117-
!condition.hasOwnProperty('pattern') &&
118-
!condition.hasOwnProperty('greaterThan') &&
119-
!condition.hasOwnProperty('greaterThanOrEqualTo') &&
120-
!condition.hasOwnProperty('lessThan') &&
121-
!condition.hasOwnProperty('lessThanOrEqualTo')
122-
) {
132+
if (!checkMappedAttributes(condition)) {
123133
throw new DefaultSchemaError(`
124134
Error occured in field definition with name: "${fieldName}".
125135
Field condition must have one of "is", "isEmpty", "isNotEmpty", "pattern", "greaterThan", "greaterThanOrEqualTo", "lessThan", "lessThanOrEqualTo" property! Properties received: [${Object.keys(
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ConditionDefinition } from "../condition";
2+
3+
export interface ConditionMapper {
4+
[key: string]: (...args: any[]) => (value: any, conditionConfig: ConditionDefinition) => boolean;
5+
}

packages/react-form-renderer/src/form-renderer/form-renderer.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ActionMapper } from './action-mapper';
77
import SchemaValidatorMapper from '../common-types/schema-validator-mapper';
88
import { FormTemplateRenderProps } from '../common-types/form-template-render-props';
99
import { NoIndex } from '../common-types/no-index';
10+
import { ConditionMapper } from './condition-mapper';
1011

1112
export interface FormRendererProps<
1213
FormValues = Record<string, any>,
@@ -25,6 +26,7 @@ export interface FormRendererProps<
2526
FormTemplate?: ComponentType<FormTemplateProps> | FunctionComponent<FormTemplateProps>;
2627
validatorMapper?: ValidatorMapper;
2728
actionMapper?: ActionMapper;
29+
conditionMapper?: ConditionMapper;
2830
schemaValidatorMapper?: SchemaValidatorMapper;
2931
FormTemplateProps?: Partial<FormTemplateProps>;
3032
children?: ReactNode | ((props: FormTemplateRenderProps) => ReactNode);

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const FormRenderer = ({
4545
clearedValue,
4646
clearOnUnmount,
4747
componentMapper,
48+
conditionMapper = {},
4849
decorators,
4950
FormTemplate,
5051
FormTemplateProps,
@@ -148,6 +149,7 @@ const FormRenderer = ({
148149
componentMapper,
149150
validatorMapper: validatorMapperMerged,
150151
actionMapper,
152+
conditionMapper,
151153
formOptions: {
152154
registerInputFile,
153155
unRegisterInputFile,
@@ -220,6 +222,9 @@ FormRenderer.propTypes = {
220222
initialValues: PropTypes.object,
221223
decorators: PropTypes.array,
222224
mutators: PropTypes.object,
225+
conditionMapper: PropTypes.shape({
226+
[PropTypes.string]: PropTypes.func,
227+
}),
223228
};
224229

225230
FormRenderer.defaultProps = {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { AnyObject } from "../common-types/any-object";
22
import { ConditionDefinition } from "../condition";
33
import Field from "../common-types/field";
4+
import { ConditionMapper } from "../form-renderer/condition-mapper";
45

5-
export type ParseCondition = (condition: ConditionDefinition, values: AnyObject, Field: Field) => void;
6+
export type ParseCondition = (condition: ConditionDefinition, values: AnyObject, Field: Field, conditionMapper?: ConditionMapper) => void;
67
declare const parseCondition: ParseCondition
78
export default parseCondition;

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

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,39 @@ const fieldCondition = (value, config) => {
4343
return config.notMatch ? !isMatched : isMatched;
4444
};
4545

46-
export const parseCondition = (condition, values, field) => {
46+
const allowedMappedAttributes = ['when', 'is'];
47+
48+
export const unpackMappedCondition = (condition, conditionMapper) => {
49+
if (typeof condition.mappedAttributes !== 'object') {
50+
return condition;
51+
}
52+
53+
const { mappedAttributes } = condition;
54+
55+
const internalCondition = {
56+
...condition,
57+
mappedAttributes: undefined,
58+
};
59+
60+
Object.entries(mappedAttributes).forEach(([key, value]) => {
61+
if (!allowedMappedAttributes.includes(key)) {
62+
console.error(`Mapped condition attribute ${key} is not allowed! Allowed attributes are: ${allowedMappedAttributes.join(', ')}`);
63+
return;
64+
}
65+
66+
if (conditionMapper[value?.[0]]) {
67+
const [fnName, ...args] = value;
68+
const fn = conditionMapper[fnName];
69+
internalCondition[key] = fn(...args);
70+
} else {
71+
console.error(`Missing conditionMapper entry for ${value}!`);
72+
}
73+
});
74+
75+
return internalCondition;
76+
};
77+
78+
export const parseCondition = (condition, values, field, conditionMapper = {}) => {
4779
let positiveResult = {
4880
visible: true,
4981
...condition.then,
@@ -62,14 +94,16 @@ export const parseCondition = (condition, values, field) => {
6294
: negativeResult;
6395
}
6496

65-
if (condition.and) {
66-
return !condition.and.map((condition) => parseCondition(condition, values, field)).some(({ result }) => result === false)
97+
const conditionInternal = unpackMappedCondition(condition, conditionMapper);
98+
99+
if (conditionInternal.and) {
100+
return !conditionInternal.and.map((condition) => parseCondition(condition, values, field)).some(({ result }) => result === false)
67101
? positiveResult
68102
: negativeResult;
69103
}
70104

71-
if (condition.sequence) {
72-
return condition.sequence.reduce(
105+
if (conditionInternal.sequence) {
106+
return conditionInternal.sequence.reduce(
73107
(acc, curr) => {
74108
const result = parseCondition(curr, values, field);
75109

@@ -83,25 +117,25 @@ export const parseCondition = (condition, values, field) => {
83117
);
84118
}
85119

86-
if (condition.or) {
87-
return condition.or.map((condition) => parseCondition(condition, values, field)).some(({ result }) => result === true)
120+
if (conditionInternal.or) {
121+
return conditionInternal.or.map((condition) => parseCondition(condition, values, field)).some(({ result }) => result === true)
88122
? positiveResult
89123
: negativeResult;
90124
}
91125

92-
if (condition.not) {
93-
return !parseCondition(condition.not, values, field).result ? positiveResult : negativeResult;
126+
if (conditionInternal.not) {
127+
return !parseCondition(conditionInternal.not, values, field).result ? positiveResult : negativeResult;
94128
}
95129

96-
const finalWhen = typeof condition.when === 'function' ? condition.when(field) : condition.when;
130+
const finalWhen = typeof conditionInternal.when === 'function' ? conditionInternal.when(field) : conditionInternal.when;
97131

98132
if (typeof finalWhen === 'string') {
99-
return fieldCondition(get(values, finalWhen), condition) ? positiveResult : negativeResult;
133+
return fieldCondition(get(values, finalWhen), conditionInternal) ? positiveResult : negativeResult;
100134
}
101135

102136
if (Array.isArray(finalWhen)) {
103137
return finalWhen
104-
.map((fieldName) => fieldCondition(get(values, typeof fieldName === 'function' ? fieldName(field) : fieldName), condition))
138+
.map((fieldName) => fieldCondition(get(values, typeof fieldName === 'function' ? fieldName(field) : fieldName), conditionInternal))
105139
.find((condition) => !!condition)
106140
? positiveResult
107141
: negativeResult;

packages/react-form-renderer/src/renderer-context/renderer-context.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ActionMapper } from '../form-renderer';
66
import Field from '../common-types/field';
77
import { AnyObject } from '../common-types/any-object';
88
import Schema from '../common-types/schema';
9+
import { ConditionMapper } from '../form-renderer/condition-mapper';
910

1011
export interface FormOptions<FormValues = Record<string, any>, InitialFormValues = Partial<FormValues>>
1112
extends FormApi<FormValues, InitialFormValues> {
@@ -29,6 +30,7 @@ export interface RendererContextValue {
2930
validatorMapper: ValidatorMapper;
3031
actionMapper: ActionMapper;
3132
formOptions: FormOptions;
33+
conditionMapper: ConditionMapper;
3234
}
3335

3436
declare const RendererContext: React.Context<RendererContextValue>;

0 commit comments

Comments
 (0)