Skip to content
Closed
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
]
},
"dependencies": {
"json-logic-js": "^2.0.2",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm lost. Wasn't this PR all about new test cases? Where are they now that you closed the PR?

"lodash": "^4.17.21",
"randexp": "^0.5.3",
"yup": "^0.30.0"
Expand Down
39 changes: 30 additions & 9 deletions src/calculateConditionalProperties.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import omit from 'lodash/omit';
import { extractParametersFromNode } from './helpers';
import { supportedTypes } from './internals/fields';
import { getFieldDescription, pickXKey } from './internals/helpers';
import { calculateComputedAttributes } from './jsonLogic';
import { buildYupSchema } from './yupSchema';
/**
* @typedef {import('./createHeadlessForm').FieldParameters} FieldParameters
Expand Down Expand Up @@ -69,14 +70,14 @@ function rebuildFieldset(fields, property) {
* @param {FieldParameters} fieldParams - field parameters
* @returns {Function}
*/
export function calculateConditionalProperties(fieldParams, customProperties) {
export function calculateConditionalProperties(fieldParams, customProperties, validations, config) {
/**
* Runs dynamic property calculation on a field based on a conditional that has been calculated
* @param {Boolean} isRequired - if the field is required
* @param {Object} conditionBranch - condition branch being applied
* @returns {Object} updated field parameters
*/
return (isRequired, conditionBranch) => {
return (isRequired, conditionBranch, __, _, formValues) => {
// Check if the current field is conditionally declared in the schema

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

const { computedAttributes, ...restNewFieldParams } = newFieldParams;
const calculatedComputedAttributes = computedAttributes
? calculateComputedAttributes(newFieldParams, config)({ validations, formValues })
: {};

const requiredValidations = [
...(fieldParams.requiredValidations ?? []),
...(restNewFieldParams.requiredValidations ?? []),
];

const base = {
isVisible: true,
required: isRequired,
...(presentation?.inputType && { type: presentation.inputType }),
schema: buildYupSchema({
...fieldParams,
...newFieldParams,
// If there are inner fields (case of fieldset) they need to be updated based on the condition
fields: fieldSetFields,
required: isRequired,
}),
...calculatedComputedAttributes,
...(calculatedComputedAttributes.value
? { value: calculatedComputedAttributes.value }
: { value: undefined }),
schema: buildYupSchema(
{
...fieldParams,
...restNewFieldParams,
...calculatedComputedAttributes,
requiredValidations,
// If there are inner fields (case of fieldset) they need to be updated based on the condition
fields: fieldSetFields,
required: isRequired,
},
config,
validations
),
};

return omit(merge(base, presentation, newFieldParams), ['inputType']);
Expand Down
34 changes: 31 additions & 3 deletions src/checkIfConditionMatches.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { hasProperty } from './utils';
* @param {Object} formValues - form state
* @returns {Boolean}
*/
export function checkIfConditionMatches(node, formValues, formFields) {
return Object.keys(node.if.properties).every((name) => {
export function checkIfConditionMatches(node, formValues, formFields, validations) {
return Object.keys(node.if.properties ?? {}).every((name) => {
const currentProperty = node.if.properties[name];
const value = formValues[name];
const hasEmptyValue =
Expand Down Expand Up @@ -50,7 +50,8 @@ export function checkIfConditionMatches(node, formValues, formFields) {
return checkIfConditionMatches(
{ if: currentProperty },
formValues[name],
getField(name, formFields).fields
getField(name, formFields).fields,
validations
);
}

Expand All @@ -68,3 +69,30 @@ export function checkIfConditionMatches(node, formValues, formFields) {
);
});
}

export function checkIfMatchesValidationsAndComputedValues(
node,
formValues,
validations,
parentID
) {
const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => {
const currentValue = validations
.getScope(parentID)
.evaluateValidationRuleInCondition(name, formValues);
if (Object.hasOwn(property, 'const') && currentValue === property.const) return true;
return false;
});

const computedValuesMatch = Object.entries(node.if.computedValues ?? {}).every(
([name, property]) => {
const currentValue = validations
.getScope(parentID)
.evaluateComputedValueRuleInCondition(name, formValues);
if (Object.hasOwn(property, 'const') && currentValue === property.const) return true;
return false;
}
);

return computedValuesMatch && validationsMatch;
}
40 changes: 29 additions & 11 deletions src/createHeadlessForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getInputType,
} from './internals/fields';
import { pickXKey } from './internals/helpers';
import { calculateComputedAttributes, createValidationChecker } from './jsonLogic';
import { buildYupSchema } from './yupSchema';

// Some type definitions (to be migrated into .d.ts file or TS Interfaces)
Expand Down Expand Up @@ -136,15 +137,16 @@ function buildFieldParameters(name, fieldProperties, required = [], config = {})
*/
function convertJSONSchemaPropertiesToFieldParameters(
{ properties, required, 'x-jsf-order': order },
config = {}
config = {},
validations
) {
const sortFields = (a, b) => sortByOrderOrPosition(a, b, order);

// Gather fields represented at the root of the node , sort them by
// their position and then remove the position property (since it's no longer needed)
return Object.entries(properties)
.filter(([, value]) => typeof value === 'object')
.map(([key, value]) => buildFieldParameters(key, value, required, config))
.map(([key, value]) => buildFieldParameters(key, value, required, config, validations))
.sort(sortFields)
.map(({ position, ...fieldParams }) => fieldParams);
}
Expand Down Expand Up @@ -187,6 +189,10 @@ function applyFieldsDependencies(fieldsParameters, node) {
applyFieldsDependencies(fieldsParameters, condition);
});
}

if (node?.['x-jsf-logic']) {
applyFieldsDependencies(fieldsParameters, node['x-jsf-logic']);
}
}

/**
Expand Down Expand Up @@ -222,19 +228,24 @@ function getComposeFunctionForField(fieldParams, hasCustomizations) {
* @param {JsfConfig} config - parser config
* @returns {Object} field object
*/
function buildField(fieldParams, config, scopedJsonSchema) {
function buildField(fieldParams, config, scopedJsonSchema, validations) {
const customProperties = getCustomPropertiesForField(fieldParams, config);
const composeFn = getComposeFunctionForField(fieldParams, !!customProperties);

const yupSchema = buildYupSchema(fieldParams, config);
const yupSchema = buildYupSchema(fieldParams, config, validations);
const calculateConditionalFieldsClosure =
fieldParams.isDynamic && calculateConditionalProperties(fieldParams, customProperties);
fieldParams.isDynamic &&
calculateConditionalProperties(fieldParams, customProperties, validations, config);

const calculateCustomValidationPropertiesClosure = calculateCustomValidationProperties(
fieldParams,
customProperties
);

const getComputedAttributes =
Object.keys(fieldParams.computedAttributes).length > 0 &&
calculateComputedAttributes(fieldParams, config);

const hasCustomValidations =
!!customProperties &&
size(pick(customProperties, SUPPORTED_CUSTOM_VALIDATION_FIELD_PARAMS)) > 0;
Expand All @@ -250,6 +261,7 @@ function buildField(fieldParams, config, scopedJsonSchema) {
...(hasCustomValidations && {
calculateCustomValidationProperties: calculateCustomValidationPropertiesClosure,
}),
...(getComputedAttributes && { getComputedAttributes }),
// field customization properties
...(customProperties && { fieldCustomization: customProperties }),
// base schema
Expand All @@ -267,7 +279,7 @@ function buildField(fieldParams, config, scopedJsonSchema) {
* @param {JsfConfig} config - JSON-schema-form config
* @returns {ParserFields} ParserFields
*/
function getFieldsFromJSONSchema(scopedJsonSchema, config) {
function getFieldsFromJSONSchema(scopedJsonSchema, config, validations) {
if (!scopedJsonSchema) {
// NOTE: other type of verifications might be needed.
return [];
Expand Down Expand Up @@ -299,11 +311,11 @@ function getFieldsFromJSONSchema(scopedJsonSchema, config) {
addFieldText: fieldParams.addFieldText,
};

buildField(fieldParams, config, scopedJsonSchema).forEach((groupField) => {
buildField(fieldParams, config, scopedJsonSchema, validations).forEach((groupField) => {
fields.push(groupField);
});
} else {
fields.push(buildField(fieldParams, config, scopedJsonSchema));
fields.push(buildField(fieldParams, config, scopedJsonSchema, validations));
}
});

Expand All @@ -323,11 +335,17 @@ export function createHeadlessForm(jsonSchema, customConfig = {}) {
};

try {
const fields = getFieldsFromJSONSchema(jsonSchema, config);
const validations = createValidationChecker(jsonSchema);
const fields = getFieldsFromJSONSchema(jsonSchema, config, validations);

const handleValidation = handleValuesChange(fields, jsonSchema, config);
const handleValidation = handleValuesChange(fields, jsonSchema, config, validations);

updateFieldsProperties(fields, getPrefillValues(fields, config.initialValues), jsonSchema);
updateFieldsProperties(
fields,
getPrefillValues(fields, config.initialValues),
jsonSchema,
validations
);

return {
fields,
Expand Down
Loading