Skip to content

Commit 52618b2

Browse files
committed
chore: conditionals
1 parent f050bb8 commit 52618b2

File tree

7 files changed

+787
-102
lines changed

7 files changed

+787
-102
lines changed

src/calculateConditionalProperties.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import omit from 'lodash/omit';
44
import { extractParametersFromNode } from './helpers';
55
import { supportedTypes } from './internals/fields';
66
import { getFieldDescription, pickXKey } from './internals/helpers';
7+
import { calculateComputedAttributes } from './jsonLogic';
78
import { buildYupSchema } from './yupSchema';
89
/**
910
* @typedef {import('./createHeadlessForm').FieldParameters} FieldParameters
@@ -69,14 +70,14 @@ function rebuildFieldset(fields, property) {
6970
* @param {FieldParameters} fieldParams - field parameters
7071
* @returns {Function}
7172
*/
72-
export function calculateConditionalProperties(fieldParams, customProperties) {
73+
export function calculateConditionalProperties(fieldParams, customProperties, validations, config) {
7374
/**
7475
* Runs dynamic property calculation on a field based on a conditional that has been calculated
7576
* @param {Boolean} isRequired - if the field is required
7677
* @param {Object} conditionBranch - condition branch being applied
7778
* @returns {Object} updated field parameters
7879
*/
79-
return (isRequired, conditionBranch) => {
80+
return (isRequired, conditionBranch, __, _, formValues) => {
8081
// Check if the current field is conditionally declared in the schema
8182

8283
const conditionalProperty = conditionBranch?.properties?.[fieldParams.name];
@@ -98,17 +99,37 @@ export function calculateConditionalProperties(fieldParams, customProperties) {
9899
newFieldParams.fields = fieldSetFields;
99100
}
100101

102+
const { computedAttributes, ...restNewFieldParams } = newFieldParams;
103+
const calculatedComputedAttributes = computedAttributes
104+
? calculateComputedAttributes(newFieldParams, config)({ validations, formValues })
105+
: {};
106+
107+
const requiredValidations = [
108+
...(fieldParams.requiredValidations ?? []),
109+
...(restNewFieldParams.requiredValidations ?? []),
110+
];
111+
101112
const base = {
102113
isVisible: true,
103114
required: isRequired,
104115
...(presentation?.inputType && { type: presentation.inputType }),
105-
schema: buildYupSchema({
106-
...fieldParams,
107-
...newFieldParams,
108-
// If there are inner fields (case of fieldset) they need to be updated based on the condition
109-
fields: fieldSetFields,
110-
required: isRequired,
111-
}),
116+
...calculatedComputedAttributes,
117+
...(calculatedComputedAttributes.value
118+
? { value: calculatedComputedAttributes.value }
119+
: { value: undefined }),
120+
schema: buildYupSchema(
121+
{
122+
...fieldParams,
123+
...restNewFieldParams,
124+
...calculatedComputedAttributes,
125+
requiredValidations,
126+
// If there are inner fields (case of fieldset) they need to be updated based on the condition
127+
fields: fieldSetFields,
128+
required: isRequired,
129+
},
130+
config,
131+
validations
132+
),
112133
};
113134

114135
return omit(merge(base, presentation, newFieldParams), ['inputType']);

src/checkIfConditionMatches.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { hasProperty } from './utils';
77
* @param {Object} formValues - form state
88
* @returns {Boolean}
99
*/
10-
export function checkIfConditionMatches(node, formValues, formFields) {
11-
return Object.keys(node.if.properties).every((name) => {
10+
export function checkIfConditionMatches(node, formValues, formFields, validations) {
11+
return Object.keys(node.if.properties ?? {}).every((name) => {
1212
const currentProperty = node.if.properties[name];
1313
const value = formValues[name];
1414
const hasEmptyValue =
@@ -50,7 +50,8 @@ export function checkIfConditionMatches(node, formValues, formFields) {
5050
return checkIfConditionMatches(
5151
{ if: currentProperty },
5252
formValues[name],
53-
getField(name, formFields).fields
53+
getField(name, formFields).fields,
54+
validations
5455
);
5556
}
5657

@@ -68,3 +69,30 @@ export function checkIfConditionMatches(node, formValues, formFields) {
6869
);
6970
});
7071
}
72+
73+
export function checkIfMatchesValidationsAndComputedValues(
74+
node,
75+
formValues,
76+
validations,
77+
parentID
78+
) {
79+
const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => {
80+
const currentValue = validations
81+
.getScope(parentID)
82+
.evaluateValidationRuleInCondition(name, formValues);
83+
if (Object.hasOwn(property, 'const') && currentValue === property.const) return true;
84+
return false;
85+
});
86+
87+
const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every(
88+
([name, property]) => {
89+
const currentValue = validations
90+
.getScope(parentID)
91+
.evaluateComputedValueRuleInCondition(name, formValues);
92+
if (Object.hasOwn(property, 'const') && currentValue === property.const) return true;
93+
return false;
94+
}
95+
);
96+
97+
return computedValuesMatch && validationsMatch;
98+
}

src/createHeadlessForm.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,22 @@ function removeInvalidAttributes(fields) {
100100
*
101101
* @returns {FieldParameters}
102102
*/
103-
function buildFieldParameters(name, fieldProperties, required = [], config = {}) {
103+
function buildFieldParameters(name, fieldProperties, required = [], config = {}, validations) {
104104
const { position } = pickXKey(fieldProperties, 'presentation') ?? {};
105105
let fields;
106106

107107
const inputType = getInputType(fieldProperties, config.strictInputType, name);
108108

109109
if (inputType === supportedTypes.FIELDSET) {
110110
// eslint-disable-next-line no-use-before-define
111-
fields = getFieldsFromJSONSchema(fieldProperties, {
112-
customProperties: get(config, `customProperties.${name}`, {}),
113-
});
111+
fields = getFieldsFromJSONSchema(
112+
fieldProperties,
113+
{
114+
customProperties: get(config, `customProperties.${name}`, {}),
115+
parentID: name,
116+
},
117+
validations
118+
);
114119
}
115120

116121
const result = {
@@ -137,15 +142,16 @@ function buildFieldParameters(name, fieldProperties, required = [], config = {})
137142
*/
138143
function convertJSONSchemaPropertiesToFieldParameters(
139144
{ properties, required, 'x-jsf-order': order },
140-
config = {}
145+
config = {},
146+
validations
141147
) {
142148
const sortFields = (a, b) => sortByOrderOrPosition(a, b, order);
143149

144150
// Gather fields represented at the root of the node , sort them by
145151
// their position and then remove the position property (since it's no longer needed)
146152
return Object.entries(properties)
147153
.filter(([, value]) => typeof value === 'object')
148-
.map(([key, value]) => buildFieldParameters(key, value, required, config))
154+
.map(([key, value]) => buildFieldParameters(key, value, required, config, validations))
149155
.sort(sortFields)
150156
.map(({ position, ...fieldParams }) => fieldParams);
151157
}
@@ -233,7 +239,8 @@ function buildField(fieldParams, config, scopedJsonSchema, validations) {
233239

234240
const yupSchema = buildYupSchema(fieldParams, config, validations);
235241
const calculateConditionalFieldsClosure =
236-
fieldParams.isDynamic && calculateConditionalProperties(fieldParams, customProperties);
242+
fieldParams.isDynamic &&
243+
calculateConditionalProperties(fieldParams, customProperties, validations, config);
237244

238245
const calculateCustomValidationPropertiesClosure = calculateCustomValidationProperties(
239246
fieldParams,
@@ -283,7 +290,11 @@ function getFieldsFromJSONSchema(scopedJsonSchema, config, validations) {
283290
return [];
284291
}
285292

286-
const fieldParamsList = convertJSONSchemaPropertiesToFieldParameters(scopedJsonSchema, config);
293+
const fieldParamsList = convertJSONSchemaPropertiesToFieldParameters(
294+
scopedJsonSchema,
295+
config,
296+
validations
297+
);
287298

288299
applyFieldsDependencies(fieldParamsList, scopedJsonSchema);
289300

src/helpers.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { lazy } from 'yup';
88
import { checkIfConditionMatches } from './checkIfConditionMatches';
99
import { supportedTypes, getInputType } from './internals/fields';
1010
import { pickXKey } from './internals/helpers';
11+
import { processJSONLogicNode } from './jsonLogic';
1112
import { containsHTML, hasProperty, wrapWithSpan } from './utils';
1213
import { buildCompleteYupSchema, buildYupSchema } from './yupSchema';
1314

@@ -230,7 +231,13 @@ function updateField(field, requiredFields, node, formValues, validations, confi
230231

231232
// If field has a calculateConditionalProperties closure, run it and update the field properties
232233
if (field.calculateConditionalProperties) {
233-
const newFieldValues = field.calculateConditionalProperties(fieldIsRequired, node);
234+
const newFieldValues = field.calculateConditionalProperties(
235+
fieldIsRequired,
236+
node,
237+
validations,
238+
config,
239+
formValues
240+
);
234241
updateValues(newFieldValues);
235242
}
236243

@@ -284,7 +291,7 @@ export function processNode({
284291
});
285292

286293
if (node.if) {
287-
const matchesCondition = checkIfConditionMatches(node, formValues, formFields);
294+
const matchesCondition = checkIfConditionMatches(node, formValues, formFields, validations);
288295
// BUG HERE (unreleated) - what if it matches but doesn't has a then,
289296
// it should do nothing, but instead it jumps to node.else when it shouldn't.
290297
if (matchesCondition && node.then) {
@@ -358,6 +365,18 @@ export function processNode({
358365
});
359366
}
360367

368+
if (node['x-jsf-logic']) {
369+
const { required: requiredFromLogic } = processJSONLogicNode({
370+
node: node['x-jsf-logic'],
371+
formValues,
372+
formFields,
373+
accRequired: requiredFields,
374+
parentID,
375+
validations,
376+
});
377+
requiredFromLogic.forEach((field) => requiredFields.add(field));
378+
}
379+
361380
return {
362381
required: requiredFields,
363382
};
@@ -475,6 +494,9 @@ export function extractParametersFromNode(schemaNode) {
475494

476495
return omitBy(
477496
{
497+
const: node.const,
498+
// This is a "forced value" when both const and default are present.
499+
...(node.const && node.default ? { value: node.const } : {}),
478500
label: node.title,
479501
readOnly: node.readOnly,
480502
...(node.deprecated && {
@@ -572,7 +594,7 @@ export function yupToFormErrors(yupError) {
572594
export const handleValuesChange = (fields, jsonSchema, config, validations) => (values) => {
573595
updateFieldsProperties(fields, values, jsonSchema, validations);
574596

575-
const lazySchema = lazy(() => buildCompleteYupSchema(fields, config));
597+
const lazySchema = lazy(() => buildCompleteYupSchema(fields, config, validations));
576598
let errors;
577599

578600
try {

src/jsonLogic.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import jsonLogic from 'json-logic-js';
22

3+
import {
4+
checkIfConditionMatches,
5+
checkIfMatchesValidationsAndComputedValues,
6+
} from './checkIfConditionMatches';
7+
import { processNode } from './helpers';
38
import { buildYupSchema } from './yupSchema';
49

510
/**
@@ -249,6 +254,67 @@ function handleNestedObjectForComputedValues(values, formValues, parentID, valid
249254
);
250255
}
251256

257+
export function processJSONLogicNode({
258+
node,
259+
formFields,
260+
formValues,
261+
accRequired,
262+
parentID,
263+
validations,
264+
}) {
265+
const requiredFields = new Set(accRequired);
266+
267+
if (node.allOf) {
268+
node.allOf
269+
.map((allOfNode) =>
270+
processJSONLogicNode({ node: allOfNode, formValues, formFields, validations, parentID })
271+
)
272+
.forEach(({ required: allOfItemRequired }) => {
273+
allOfItemRequired.forEach(requiredFields.add, requiredFields);
274+
});
275+
}
276+
277+
if (node.if) {
278+
const matchesPropertyCondition = checkIfConditionMatches(
279+
node,
280+
formValues,
281+
formFields,
282+
validations
283+
);
284+
const matchesValidationsAndComputedValues = checkIfMatchesValidationsAndComputedValues(
285+
node,
286+
formValues,
287+
validations,
288+
parentID
289+
);
290+
291+
const isConditionMatch = matchesPropertyCondition && matchesValidationsAndComputedValues;
292+
293+
if (isConditionMatch && node.then) {
294+
const { required: branchRequired } = processNode({
295+
node: node.then,
296+
formValues,
297+
formFields,
298+
accRequired,
299+
validations,
300+
});
301+
branchRequired.forEach((field) => requiredFields.add(field));
302+
}
303+
if (!isConditionMatch && node.else) {
304+
const { required: branchRequired } = processNode({
305+
node: node.else,
306+
formValues,
307+
formFields,
308+
accRequired: requiredFields,
309+
validations,
310+
});
311+
branchRequired.forEach((field) => requiredFields.add(field));
312+
}
313+
}
314+
315+
return { required: requiredFields };
316+
}
317+
252318
function buildSampleEmptyObject(schema = {}) {
253319
const sample = {};
254320
if (typeof schema !== 'object' || !schema.properties) {

0 commit comments

Comments
 (0)