Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
87d2d9b
feat: JSON Logic skeleton and plumbing setup
brennj Jun 29, 2023
d2cc213
chore: support barebones computedAttrs
brennj Aug 22, 2023
cf8f9bd
chore: fix errors
brennj Aug 31, 2023
2eca83f
chore: fix mess ups from rebase
brennj Aug 31, 2023
b305047
chore: feedback from PR
brennj Sep 1, 2023
edbc56f
chore: pass logic down at updateFieldsProperties to prevent bugs
brennj Sep 1, 2023
55ed296
Release 0.5.0-dev.20230901130231
brennj Sep 1, 2023
54f7c42
Revert "Release 0.5.0-dev.20230901130231"
brennj Sep 4, 2023
2710f51
feat: JSON Logic skeleton and plumbing setup
brennj Jun 29, 2023
da20a93
chore: support barebones computedAttrs
brennj Aug 22, 2023
195825d
chore: computed string attributes
brennj Aug 23, 2023
9118b09
chore: fix tests
brennj Sep 4, 2023
5f32f21
chore: consistency for curly braces
brennj Sep 4, 2023
a40da7b
chore: remove unneeded code for now
brennj Sep 4, 2023
35d0fd1
feat: JSON Logic skeleton and plumbing setup
brennj Jun 29, 2023
57b90af
chore: fix tests
brennj Sep 4, 2023
a186d13
chore: review errors
brennj Sep 4, 2023
aa3432b
chore: fix up code for fixtures
brennj Sep 4, 2023
40679e5
chore: add a bunch of docs to try and make things clearer
brennj Sep 4, 2023
8d54d43
chore: add example to docs
brennj Sep 4, 2023
df9b1a6
chore: higher level console check
brennj Sep 5, 2023
81402ed
chore: use cases to clean up error tests
brennj Sep 5, 2023
827ba1b
chore: add more tests for missing vars
brennj Sep 5, 2023
02a0ff5
chore: use switch statement instead
brennj Sep 5, 2023
8cc22e0
chore: add code comments why schemas fail
brennj Sep 5, 2023
36e6845
feat: JSON Logic skeleton and plumbing setup
brennj Jun 29, 2023
70a1430
chore: changes
brennj Sep 5, 2023
d03b369
chore: add bad operator handling
brennj Sep 5, 2023
9813400
chore: fix bad naming
brennj Sep 5, 2023
2d6eac7
chore: matching after merging
brennj Sep 5, 2023
63e9424
chore: test field to be explicit in test
brennj Sep 5, 2023
deb9d6c
chore: remove unused schema
brennj Sep 5, 2023
6f14ac4
chore: fix bad var name
brennj Sep 5, 2023
2beb330
chore: restore code after merge conflicts
brennj Sep 6, 2023
dbaba0f
chore: fix jsdocs
brennj Sep 6, 2023
83a258e
chore: rename requiredValidations -> jsonLogicValidations
brennj Sep 6, 2023
b1e6772
chore: rename checkIfConditionMatches -> checkIfConditionMatchesPrope…
brennj Sep 6, 2023
6fbb9e1
chore: some small refactor
brennj Sep 6, 2023
3fd0092
chore: can remove need for nulls
brennj Sep 7, 2023
ae07dbf
chore: add missing isVisible checks
brennj Sep 7, 2023
42cf4ab
chore: assert field c is invisible
brennj Sep 7, 2023
482a82e
Merge remote-tracking branch 'origin/main' into inline-rule-handling
brennj Sep 13, 2023
a85d588
chore: more fixing inline rule verbage
brennj Sep 13, 2023
1f15939
Merge remote-tracking branch 'origin/inline-rule-handling' into condi…
brennj Sep 13, 2023
b99db16
Merge remote-tracking branch 'origin/main' into conditionals-json-logic
brennj Sep 13, 2023
e9628bf
Merge remote-tracking branch 'origin/inline-rule-handling' into condi…
brennj Sep 13, 2023
b36e5bd
chore: remove need for default: 0
brennj Sep 13, 2023
ee18568
chore: add note to internal issue
brennj Sep 13, 2023
528fd31
Merge remote-tracking branch 'origin/main' into conditionals-json-logic
brennj Sep 18, 2023
2e52614
chore: add comment to track NaN bug
brennj Sep 18, 2023
df233cc
Release 0.6.5-dev.20230918083235
brennj Sep 18, 2023
7efc061
Revert "Release 0.6.5-dev.20230918083235"
brennj Sep 18, 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
63 changes: 47 additions & 16 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 @@ -64,19 +65,29 @@ function rebuildFieldset(fields, property) {
}

/**
* Builds a function that updates the fields properties based on the form values and the
* dependencies the field has on the current schema.
* @param {FieldParameters} fieldParams - field parameters
* @returns {Function}
* Builds a function that updates the field properties based on the form values,
* schema dependencies, and conditional logic.
*
* @param {Object} params - Parameters
* @param {Object} params.fieldParams - Current field parameters
* @param {Object} params.customProperties - Custom field properties from schema
* @param {Object} params.logic - JSON-logic
* @param {Object} params.config - Form configuration
*
* @returns {Function} A function that calculates conditional properties
*/
export function calculateConditionalProperties(fieldParams, customProperties) {
export function calculateConditionalProperties({ fieldParams, customProperties, logic, 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
*
* @param {Object} params - Parameters
* @param {Boolean} params.isRequired - If field is required
* @param {Object} params.conditionBranch - Condition branch
* @param {Object} params.formValues - Current form values
*
* @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 +109,37 @@ export function calculateConditionalProperties(fieldParams, customProperties) {
newFieldParams.fields = fieldSetFields;
}

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

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

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,
jsonLogicValidations,
// If there are inner fields (case of fieldset) they need to be updated based on the condition
fields: fieldSetFields,
required: isRequired,
},
config,
logic
),
};

return omit(merge(base, presentation, newFieldParams), ['inputType']);
Expand Down
29 changes: 25 additions & 4 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 checkIfConditionMatchesProperties(node, formValues, formFields, logic) {
return Object.keys(node.if.properties ?? {}).every((name) => {
const currentProperty = node.if.properties[name];
const value = formValues[name];
const hasEmptyValue =
Expand Down Expand Up @@ -47,10 +47,11 @@ export function checkIfConditionMatches(node, formValues, formFields) {
}

if (currentProperty.properties) {
return checkIfConditionMatches(
return checkIfConditionMatchesProperties(
{ if: currentProperty },
formValues[name],
getField(name, formFields).fields
getField(name, formFields).fields,
logic
);
}

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

export function checkIfMatchesValidationsAndComputedValues(node, formValues, logic, parentID) {
const validationsMatch = Object.entries(node.if.validations ?? {}).every(([name, property]) => {
const currentValue = logic.getScope(parentID).applyValidationRuleInCondition(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 = logic
.getScope(parentID)
.applyComputedValueRuleInCondition(name, formValues);
if (Object.hasOwn(property, 'const') && currentValue === property.const) return true;
return false;
}
);

return computedValuesMatch && validationsMatch;
}
16 changes: 11 additions & 5 deletions src/createHeadlessForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,22 @@ function removeInvalidAttributes(fields) {
*
* @returns {FieldParameters}
*/
function buildFieldParameters(name, fieldProperties, required = [], config = {}) {
function buildFieldParameters(name, fieldProperties, required = [], config = {}, logic) {
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,
},
logic
);
}

const result = {
Expand Down Expand Up @@ -235,7 +240,8 @@ function buildField(fieldParams, config, scopedJsonSchema, logic) {

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

const calculateCustomValidationPropertiesClosure = calculateCustomValidationProperties(
fieldParams,
Expand Down
27 changes: 22 additions & 5 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import omitBy from 'lodash/omitBy';
import set from 'lodash/set';
import { lazy } from 'yup';

import { checkIfConditionMatches } from './checkIfConditionMatches';
import { checkIfConditionMatchesProperties } from './checkIfConditionMatches';
import { supportedTypes, getInputType } from './internals/fields';
import { pickXKey } from './internals/helpers';
import { processJSONLogicNode } from './jsonLogic';
import { containsHTML, hasProperty, wrapWithSpan } from './utils';
import { buildCompleteYupSchema, buildYupSchema } from './yupSchema';

Expand Down Expand Up @@ -240,7 +241,11 @@ function updateField(field, requiredFields, node, formValues, logic, config) {

// If field has a calculateConditionalProperties closure, run it and update the field properties
if (field.calculateConditionalProperties) {
const newFieldValues = field.calculateConditionalProperties(fieldIsRequired, node);
const newFieldValues = field.calculateConditionalProperties({
isRequired: fieldIsRequired,
conditionBranch: node,
formValues,
});
updateValues(newFieldValues);
}

Expand Down Expand Up @@ -294,7 +299,7 @@ export function processNode({
});

if (node.if) {
const matchesCondition = checkIfConditionMatches(node, formValues, formFields, logic);
const matchesCondition = checkIfConditionMatchesProperties(node, formValues, formFields, logic);
// 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) {
Expand Down Expand Up @@ -368,6 +373,18 @@ export function processNode({
});
}

if (node['x-jsf-logic']) {
const { required: requiredFromLogic } = processJSONLogicNode({
node: node['x-jsf-logic'],
formValues,
formFields,
accRequired: requiredFields,
parentID,
logic,
});
requiredFromLogic.forEach((field) => requiredFields.add(field));
}

return {
required: requiredFields,
};
Expand Down Expand Up @@ -465,7 +482,7 @@ export function extractParametersFromNode(schemaNode) {

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

// This is when a forced value is computed.
Expand Down Expand Up @@ -516,7 +533,7 @@ export function extractParametersFromNode(schemaNode) {

// Handle [name].presentation
...presentation,
requiredValidations,
jsonLogicValidations,
computedAttributes: decoratedComputedAttributes,
description: containsHTML(description)
? wrapWithSpan(description, {
Expand Down
68 changes: 66 additions & 2 deletions src/jsonLogic.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import jsonLogic from 'json-logic-js';

import {
checkIfConditionMatchesProperties,
checkIfMatchesValidationsAndComputedValues,
} from './checkIfConditionMatches';
import { processNode } from './helpers';
import { buildYupSchema } from './yupSchema';

/**
Expand Down Expand Up @@ -90,7 +95,10 @@ function createValidationsScope(schema) {
});

function validate(rule, values) {
return jsonLogic.apply(rule, replaceUndefinedValuesWithNulls(values));
return jsonLogic.apply(
rule,
replaceUndefinedValuesWithNulls({ ...sampleEmptyObject, ...values })
);
}

return {
Expand Down Expand Up @@ -126,7 +134,7 @@ function createValidationsScope(schema) {
*/
function replaceUndefinedValuesWithNulls(values = {}) {
return Object.entries(values).reduce((prev, [key, value]) => {
return { ...prev, [key]: value === undefined ? null : value };
return { ...prev, [key]: value === undefined || value === null ? NaN : value };
}, {});
}

Expand Down Expand Up @@ -428,3 +436,59 @@ function removeIndicesFromPath(path) {
const intermediatePath = path.replace(regexToGetIndices, '.');
return intermediatePath.replace(/\.\d+$/, '');
}

export function processJSONLogicNode({
node,
formFields,
formValues,
accRequired,
parentID,
logic,
}) {
const requiredFields = new Set(accRequired);

if (node.allOf) {
node.allOf
.map((allOfNode) =>
processJSONLogicNode({ node: allOfNode, formValues, formFields, logic, parentID })
)
.forEach(({ required: allOfItemRequired }) => {
allOfItemRequired.forEach(requiredFields.add, requiredFields);
});
}

if (node.if) {
const matchesPropertyCondition = checkIfConditionMatchesProperties(
node,
formValues,
formFields,
logic
);
const matchesValidationsAndComputedValues =
matchesPropertyCondition &&
checkIfMatchesValidationsAndComputedValues(node, formValues, logic, parentID);

const isConditionMatch = matchesPropertyCondition && matchesValidationsAndComputedValues;

let nextNode;
if (isConditionMatch && node.then) {
nextNode = node.then;
}
if (!isConditionMatch && node.else) {
nextNode = node.else;
}
if (nextNode) {
const { required: branchRequired } = processNode({
node: nextNode,
formValues,
formFields,
accRequired,
logic,
parentID,
});
branchRequired.forEach((field) => requiredFields.add(field));
}
}

return { required: requiredFields };
}
12 changes: 6 additions & 6 deletions src/tests/checkIfConditionMatches.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { checkIfConditionMatches } from '../checkIfConditionMatches';
import { checkIfConditionMatchesProperties } from '../checkIfConditionMatches';

it('Empty if is always going to be true', () => {
expect(checkIfConditionMatches({ if: { properties: {} } })).toBe(true);
expect(checkIfConditionMatchesProperties({ if: { properties: {} } })).toBe(true);
});

it('Basic if check passes with correct value', () => {
expect(
checkIfConditionMatches(
checkIfConditionMatchesProperties(
{ if: { properties: { a: { const: 'hello' } } } },
{
a: 'hello',
Expand All @@ -17,7 +17,7 @@ it('Basic if check passes with correct value', () => {

it('Basic if check fails with incorrect value', () => {
expect(
checkIfConditionMatches(
checkIfConditionMatchesProperties(
{ if: { properties: { a: { const: 'hello' } } } },
{
a: 'goodbye',
Expand All @@ -28,7 +28,7 @@ it('Basic if check fails with incorrect value', () => {

it('Nested properties check passes with correct value', () => {
expect(
checkIfConditionMatches(
checkIfConditionMatchesProperties(
{ if: { properties: { parent: { properties: { child: { const: 'hello from child' } } } } } },
{
parent: { child: 'hello from child' },
Expand All @@ -40,7 +40,7 @@ it('Nested properties check passes with correct value', () => {

it('Nested properties check passes with correct value', () => {
expect(
checkIfConditionMatches(
checkIfConditionMatchesProperties(
{ if: { properties: { parent: { properties: { child: { const: 'hello from child' } } } } } },
{
parent: { child: 'goodbye from child' },
Expand Down
Loading