Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
a831add
chore: barebones playground
brennj Jun 29, 2023
afade7d
feat: poc of json-logic
brennj Jun 30, 2023
23f6545
chore: more stable implementation
brennj Jul 3, 2023
c381186
chore: remove playground for now
brennj Jul 4, 2023
e6eef4e
chore: remove unneeded stuff
brennj Jul 4, 2023
9e2f73c
chore: more unneeded
brennj Jul 4, 2023
e9656af
chore: clean up package.json
brennj Jul 4, 2023
4766be6
chore: removed package by mistake
brennj Jul 4, 2023
f66c30f
chore: this isnt being used
brennj Jul 4, 2023
25291aa
chore: first test passing
brennj Jul 4, 2023
1614f3e
chore: filling out the validations
brennj Jul 4, 2023
9ccd4c0
chore: more test cases todo
brennj Jul 4, 2023
503b225
chore: proper name on the validations
brennj Jul 4, 2023
f1633bb
Merge remote-tracking branch 'origin/poc-json-logic' into poc-tests-j…
brennj Jul 4, 2023
c4034db
chore: remove console.log
brennj Jul 4, 2023
9040e53
Merge remote-tracking branch 'origin/poc-json-logic' into poc-tests-j…
brennj Jul 4, 2023
70cbaa9
chore: more Arithmetic
brennj Jul 4, 2023
f87c34f
boolean logic for ands and ors
brennj Jul 4, 2023
68bf668
chore: use object over array
brennj Jul 4, 2023
bcc1d6b
chore: think of some further test cases
brennj Jul 4, 2023
b032c1b
chore: validationMap kinda there
brennj Jul 5, 2023
ccbe54a
chore: it kinda works!
brennj Jul 5, 2023
8b95707
chore: current work
brennj Jul 5, 2023
a17f4c7
chore: looks like we might not need to evaluate initially for now
brennj Jul 5, 2023
382a2f8
chores: tests on conditional validations
brennj Jul 5, 2023
a3c27cc
chore: more tests
brennj Jul 5, 2023
e4099fb
chore: computed attributes kinda seem to work
brennj Jul 5, 2023
bb0269c
chore: everything fixed except conditionals
brennj Jul 6, 2023
1981ba5
chore: BOOM IT WORKS
brennj Jul 6, 2023
4a7469b
chore: everything back working!
brennj Jul 6, 2023
a28121a
chore: current progress and writing more tests
brennj Jul 6, 2023
ebb3dd3
chore: validations + computedValues anded together
brennj Jul 7, 2023
9483cf7
chore: first of fieldsets working
brennj Jul 7, 2023
e2a69e5
chore: more tests
brennj Jul 7, 2023
260a557
chore: error tests
brennj Jul 7, 2023
75dc8c7
chore: more error handling, throw on top lvl non existing vars
brennj Jul 7, 2023
cc42fee
chore: extended error checks for deeply nested rules
brennj Jul 7, 2023
2f2db07
chore: transferring schemas to own file
brennj Jul 7, 2023
056cd62
chore: finish moving schemas to own file
brennj Jul 7, 2023
b48160e
chore: top level can look in nested fields
brennj Jul 7, 2023
61b2343
chore: fix skipped test
brennj Jul 7, 2023
6b9cfc1
chore: fix other test
brennj Jul 7, 2023
4cde6d3
chore: deep var missing in fieldset test
brennj Jul 10, 2023
d5b0dcd
chore: throw on validations that do not exist
brennj Jul 10, 2023
0cc9926
chore: fix nested field attribute calc
brennj Jul 10, 2023
7dd6eb4
chore: able to apply nested object validations
brennj Jul 10, 2023
30fbd6f
chore: more error handling
brennj Jul 10, 2023
8e23efa
chore: MORE error handling
brennj Jul 10, 2023
413e905
chore: fix error string messaging
brennj Jul 10, 2023
36d433f
chore: fail on missing validation in if
brennj Jul 10, 2023
dedd8f1
chore: start of support for array validation
brennj Jul 10, 2023
b738856
chore: blocked fixing this test for now
brennj Jul 10, 2023
7fd3650
chore: errorMessage ref is out of scope for now
brennj Jul 10, 2023
8327b07
chore: some missing tests to do
brennj Jul 13, 2023
41f6d1e
chore: conditional computedAttributes didnt work
brennj Jul 13, 2023
5632f35
chore: calculated value conditionally wasnt working
brennj Jul 13, 2023
893f6e7
chore: some statement handling
brennj Jul 19, 2023
5baaff9
feat: exploring inline rules
brennj Jul 28, 2023
bb83706
chore: integrate with latest main
brennj Aug 9, 2023
e012445
chore: fix broken tests
brennj Aug 9, 2023
f5a9e83
chore: renames based on RFC feedback
brennj Aug 9, 2023
725b85c
chore: take const into account for allowed values
brennj Aug 9, 2023
50d7ff3
chore: use const + default for forced values
brennj Aug 10, 2023
fda543e
chore: replace inline values
brennj Aug 11, 2023
8405442
chore: fill out the todos
brennj Aug 11, 2023
80b27ff
chore: ensure you spread requiredValidations to build up together
brennj Aug 11, 2023
bba507b
chore: add missing little part to test
brennj Aug 11, 2023
0246af8
chore: fill out a min/max test
brennj Aug 11, 2023
6d78163
chore: ensure computed attributes are counted
brennj Aug 11, 2023
0274f9b
chore: error unit tests
brennj Aug 11, 2023
748d465
chore: clean up a little
brennj Aug 11, 2023
7546f2e
chore: code complete?
brennj Aug 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
459 changes: 235 additions & 224 deletions package-lock.json

Large diffs are not rendered by default.

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",
"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;
}
59 changes: 43 additions & 16 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 @@ -99,17 +100,22 @@ function removeInvalidAttributes(fields) {
*
* @returns {FieldParameters}
*/
function buildFieldParameters(name, fieldProperties, required = [], config = {}) {
function buildFieldParameters(name, fieldProperties, required = [], config = {}, validations) {
const { position } = pickXKey(fieldProperties, 'presentation') ?? {};
let fields;

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

if (inputType === supportedTypes.FIELDSET) {
// eslint-disable-next-line no-use-before-define
fields = getFieldsFromJSONSchema(fieldProperties, {
customProperties: get(config, `customProperties.${name}`, {}),
});
fields = getFieldsFromJSONSchema(
fieldProperties,
{
customProperties: get(config, `customProperties.${name}`, {}),
parentID: name,
},
validations
);
}

const result = {
Expand All @@ -136,15 +142,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 +194,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 +233,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 +266,7 @@ function buildField(fieldParams, config, scopedJsonSchema) {
...(hasCustomValidations && {
calculateCustomValidationProperties: calculateCustomValidationPropertiesClosure,
}),
...(getComputedAttributes && { getComputedAttributes }),
// field customization properties
...(customProperties && { fieldCustomization: customProperties }),
// base schema
Expand All @@ -267,13 +284,17 @@ 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 [];
}

const fieldParamsList = convertJSONSchemaPropertiesToFieldParameters(scopedJsonSchema, config);
const fieldParamsList = convertJSONSchemaPropertiesToFieldParameters(
scopedJsonSchema,
config,
validations
);

applyFieldsDependencies(fieldParamsList, scopedJsonSchema);

Expand All @@ -299,11 +320,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 +344,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