Skip to content

WIP: Advanced validation proof of concept #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
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
11 changes: 9 additions & 2 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 { getValidationsFromJSONSchema } from './jsonLogic';
import { buildYupSchema } from './yupSchema';

// Some type definitions (to be migrated into .d.ts file or TS Interfaces)
Expand Down Expand Up @@ -324,10 +325,16 @@ export function createHeadlessForm(jsonSchema, customConfig = {}) {

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

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
63 changes: 45 additions & 18 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ function compareFormValueWithSchemaValue(formValue, schemaValue) {
* @param {Object} formValues - form state
* @returns {Boolean}
*/
function checkIfConditionMatches(node, formValues, formFields) {
return Object.keys(node.if.properties).every((name) => {
function checkIfConditionMatches(node, formValues, formFields, validations) {
const propertiesMatch = Object.keys(node.if.properties ?? {}).every((name) => {
const currentProperty = node.if.properties[name];
const value = formValues[name];
const hasEmptyValue =
Expand Down Expand Up @@ -120,6 +120,16 @@ function checkIfConditionMatches(node, formValues, formFields) {
value
);
});

const validationsMatch = Object.entries(node.if['x-jsf-validations'] ?? {}).every(
([name, property]) => {
const currentValue = validations.evaluateRule(name, formValues);
if (Object.hasOwn(property, 'const') && currentValue === property.const) return true;
return false;
}
);

return propertiesMatch && validationsMatch;
}

/**
Expand Down Expand Up @@ -305,7 +315,7 @@ function updateField(field, requiredFields, node, formValues) {
* @param {Set} accRequired - set of required field names gathered by traversing the tree
* @returns {Object}
*/
function processNode(node, formValues, formFields, accRequired = new Set()) {
function processNode({ node, formValues, formFields, accRequired = new Set(), validations }) {
// Set initial required fields
const requiredFields = new Set(accRequired);

Expand All @@ -322,25 +332,27 @@ function processNode(node, formValues, formFields, accRequired = new Set()) {
});

if (node.if) {
const matchesCondition = checkIfConditionMatches(node, formValues, formFields);
const matchesCondition = checkIfConditionMatches(node, formValues, formFields, validations);
// BUG HERE (unreleated) - what if it matches but doesn't has a then,
// it should do nothing, but instead it jumps to node.else when it shouldn't.
if (matchesCondition && node.then) {
const { required: branchRequired } = processNode(
node.then,
const { required: branchRequired } = processNode({
node: node.then,
formValues,
formFields,
requiredFields
);
accRequired: requiredFields,
validations,
});

branchRequired.forEach((field) => requiredFields.add(field));
} else if (node.else) {
const { required: branchRequired } = processNode(
node.else,
const { required: branchRequired } = processNode({
node: node.else,
formValues,
formFields,
requiredFields
);
accRequired: requiredFields,
validations,
});
branchRequired.forEach((field) => requiredFields.add(field));
}
}
Expand All @@ -361,7 +373,15 @@ function processNode(node, formValues, formFields, accRequired = new Set()) {

if (node.allOf) {
node.allOf
.map((allOfNode) => processNode(allOfNode, formValues, formFields, requiredFields))
.map((allOfNode) =>
processNode({
node: allOfNode,
formValues,
formFields,
accRequired: requiredFields,
validations,
})
)
.forEach(({ required: allOfItemRequired }) => {
allOfItemRequired.forEach(requiredFields.add, requiredFields);
});
Expand All @@ -372,7 +392,12 @@ function processNode(node, formValues, formFields, accRequired = new Set()) {
const inputType = getInputType(nestedNode);
if (inputType === supportedTypes.FIELDSET) {
// It's a fieldset, which might contain scoped conditions
processNode(nestedNode, formValues[name] || {}, getField(name, formFields).fields);
processNode({
node: nestedNode,
formValues: formValues[name] || {},
formFields: getField(name, formFields).fields,
validations,
});
}
});
}
Expand Down Expand Up @@ -407,11 +432,11 @@ function clearValuesIfNotVisible(fields, formValues) {
* @param {Object} formValues - current values of the form
* @param {Object} jsonSchema - JSON schema object
*/
export function updateFieldsProperties(fields, formValues, jsonSchema) {
export function updateFieldsProperties(fields, formValues, jsonSchema, validations) {
if (!jsonSchema?.properties) {
return;
}
processNode(jsonSchema, formValues, fields);
processNode({ node: jsonSchema, formValues, formFields: fields, validations });
clearValuesIfNotVisible(fields, formValues);
}

Expand Down Expand Up @@ -474,6 +499,7 @@ export function extractParametersFromNode(schemaNode) {

const presentation = pickXKey(schemaNode, 'presentation') ?? {};
const errorMessage = pickXKey(schemaNode, 'errorMessage') ?? {};
const validations = schemaNode['x-jsf-validations'];

const node = omit(schemaNode, ['x-jsf-presentation', 'presentation']);

Expand Down Expand Up @@ -518,6 +544,7 @@ export function extractParametersFromNode(schemaNode) {

// Handle [name].presentation
...presentation,
validations: validations,
description: containsHTML(description)
? wrapWithSpan(description, {
class: 'jsf-description',
Expand Down Expand Up @@ -575,8 +602,8 @@ export function yupToFormErrors(yupError) {
* @param {JsfConfig} config - jsf config
* @returns {Function(values: Object): { YupError: YupObject, formErrors: Object }} Callback that returns Yup errors <YupObject>
*/
export const handleValuesChange = (fields, jsonSchema, config) => (values) => {
updateFieldsProperties(fields, values, jsonSchema);
export const handleValuesChange = (fields, jsonSchema, config, validations) => (values) => {
updateFieldsProperties(fields, values, jsonSchema, validations);

const lazySchema = lazy(() => buildCompleteYupSchema(fields, config));
let errors;
Expand Down
42 changes: 42 additions & 0 deletions src/jsonLogic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import jsonLogic from 'json-logic-js';

/**
* Parses the JSON schema to extract the advanced validation logic and returns a set of functionality to check the current status of said rules.
* @param {Object} schema - JSON schema node
* @param {Object} initialValues - form state
* @returns {Object}
*/
export function getValidationsFromJSONSchema(schema) {
const ruleMap = new Map();

const validationObject = Object.entries(schema?.['x-jsf-validations'] ?? {});
validationObject.forEach(([id, { rule }]) => {
ruleMap.set(id, { rule });
});

return {
ruleMap,
evaluateRule(id, values) {
const validation = ruleMap.get(id);
const answer = jsonLogic.apply(validation.rule, clean(values));
return answer;
},
};
}

function clean(values) {
return Object.entries(values).reduce((prev, [key, value]) => {
return { ...prev, [key]: value === undefined ? null : value };
}, {});
}

export function yupSchemaWithCustomJSONLogic(field, validation, id) {
return (yupSchema) =>
yupSchema.test(
`${field.name}-validation-${id}`,
validation.errorMessage ?? 'This field is invalid.',
(_, { parent }) => {
return jsonLogic.apply(validation.rule, parent);
}
);
}
Loading