Skip to content

Commit

Permalink
chore: support barebones computedAttrs
Browse files Browse the repository at this point in the history
  • Loading branch information
brennj committed Aug 31, 2023
1 parent 87d2d9b commit d2cc213
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 7 deletions.
20 changes: 17 additions & 3 deletions src/createHeadlessForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
getInputType,
} from './internals/fields';
import { pickXKey } from './internals/helpers';
import { createValidationChecker } from './jsonLogic';
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 @@ -188,6 +188,10 @@ function applyFieldsDependencies(fieldsParameters, node) {
applyFieldsDependencies(fieldsParameters, condition);
});
}

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

/**
Expand Down Expand Up @@ -238,6 +242,10 @@ function buildField(fieldParams, config, scopedJsonSchema, logic) {
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 @@ -253,6 +261,7 @@ function buildField(fieldParams, config, scopedJsonSchema, logic) {
...(hasCustomValidations && {
calculateCustomValidationProperties: calculateCustomValidationPropertiesClosure,
}),
...(getComputedAttributes && { getComputedAttributes }),
// field customization properties
...(customProperties && { fieldCustomization: customProperties }),
// base schema
Expand Down Expand Up @@ -302,7 +311,7 @@ function getFieldsFromJSONSchema(scopedJsonSchema, config, logic) {
addFieldText: fieldParams.addFieldText,
};

buildField(fieldParams, config, scopedJsonSchema).forEach((groupField) => {
buildField(fieldParams, config, scopedJsonSchema, validations).forEach((groupField) => {

Check failure on line 314 in src/createHeadlessForm.js

View workflow job for this annotation

GitHub Actions / lint

'validations' is not defined
fields.push(groupField);
});
} else {
Expand Down Expand Up @@ -331,7 +340,12 @@ export function createHeadlessForm(jsonSchema, customConfig = {}) {

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

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

Check failure on line 347 in src/createHeadlessForm.js

View workflow job for this annotation

GitHub Actions / lint

'validations' is not defined
);

return {
fields,
Expand Down
32 changes: 28 additions & 4 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export function getPrefillValues(fields, initialValues = {}) {
* @param {Object} node - JSON-schema node
* @returns
*/
function updateField(field, requiredFields, node, formValues) {
function updateField(field, requiredFields, node, formValues, validations, config) {
// If there was an error building the field, it might not exist in the form even though
// it can be mentioned in the schema so we return early in that case
if (!field) {
Expand Down Expand Up @@ -226,6 +226,18 @@ function updateField(field, requiredFields, node, formValues) {
}
});

if (field.getComputedAttributes) {
const computedFieldValues = field.getComputedAttributes({
field,
isRequired: fieldIsRequired,
node,
formValues,
config,
validations,
});
updateValues(computedFieldValues);
}

// If field has a calculateConditionalProperties closure, run it and update the field properties
if (field.calculateConditionalProperties) {
const newFieldValues = field.calculateConditionalProperties(fieldIsRequired, node);
Expand Down Expand Up @@ -270,13 +282,15 @@ export function processNode({
// Go through the node properties definition and update each field accordingly
Object.keys(node.properties ?? []).forEach((fieldName) => {
const field = getField(fieldName, formFields);
updateField(field, requiredFields, node, formValues);
updateField(field, requiredFields, node, formValues, validations, { parentID });

Check failure on line 285 in src/helpers.js

View workflow job for this annotation

GitHub Actions / lint

'validations' is not defined
});

// Update required fields based on the `required` property and mutate node if needed
node.required?.forEach((fieldName) => {
requiredFields.add(fieldName);
updateField(getField(fieldName, formFields), requiredFields, node, formValues);
updateField(getField(fieldName, formFields), requiredFields, node, formValues, validations, {

Check failure on line 291 in src/helpers.js

View workflow job for this annotation

GitHub Actions / lint

'validations' is not defined
parentID,
});
});

if (node.if) {
Expand Down Expand Up @@ -316,7 +330,7 @@ export function processNode({
node.anyOf.forEach(({ required = [] }) => {
required.forEach((fieldName) => {
const field = getField(fieldName, formFields);
updateField(field, requiredFields, node, formValues);
updateField(field, requiredFields, node, formValues, validations, { parentID });

Check failure on line 333 in src/helpers.js

View workflow job for this annotation

GitHub Actions / lint

'validations' is not defined
});
});
}
Expand Down Expand Up @@ -452,6 +466,15 @@ export function extractParametersFromNode(schemaNode) {
const presentation = pickXKey(schemaNode, 'presentation') ?? {};
const errorMessage = pickXKey(schemaNode, 'errorMessage') ?? {};
const requiredValidations = schemaNode['x-jsf-logic-validations'];
const computedAttributes = schemaNode['x-jsf-logic-computedAttrs'];

// This is when a forced value is computed.
const decoratedComputedAttributes = {
...(computedAttributes ?? {}),
...(computedAttributes?.const && computedAttributes?.default
? { value: computedAttributes.const }
: {}),
};

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

Expand Down Expand Up @@ -499,6 +522,7 @@ export function extractParametersFromNode(schemaNode) {
// Handle [name].presentation
...presentation,
requiredValidations,
computedAttributes: decoratedComputedAttributes,
description: containsHTML(description)
? wrapWithSpan(description, {
class: 'jsf-description',
Expand Down
30 changes: 30 additions & 0 deletions src/jsonLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,33 @@ export function yupSchemaWithCustomJSONLogic({ field, logic, config, id }) {
}
);
}

export function calculateComputedAttributes(fieldParams, { parentID = 'root' } = {}) {
return ({ validations, formValues }) => {
const { name, computedAttributes } = fieldParams;
const attributes = Object.fromEntries(
Object.entries(computedAttributes)
.map(handleComputedAttribute(validations, formValues, parentID, name))
.filter(([, value]) => value !== null)
);

return attributes;
};
}

function handleComputedAttribute(validations, formValues, parentID, name) {
return ([key, value]) => {
if (key === 'const')
return [
key,
validations.getScope(parentID).evaluateComputedValueRuleForField(value, formValues, name),
];

if (typeof value === 'string') {
return [
key,
validations.getScope(parentID).evaluateComputedValueRuleForField(value, formValues, name),
];
}
};
}
13 changes: 13 additions & 0 deletions src/tests/jsonLogic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createSchemaWithRulesOnFieldA,
createSchemaWithThreePropertiesWithRuleOnFieldA,
multiRuleSchema,
schemaWithComputedAttributes,
schemaWithNativeAndJSONLogicChecks,
schemaWithNonRequiredField,
schemaWithTwoRules,
Expand Down Expand Up @@ -212,4 +213,16 @@ describe('jsonLogic: cross-values validations', () => {
expect(handleValidation({ field_a: 4, field_b: 2 }).formErrors).toEqual(undefined);
});
});

describe('Derive values', () => {
it('field_b is field_a * 2', () => {
const { fields } = createHeadlessForm(schemaWithComputedAttributes, {
strictInputType: false,
initialValues: { field_a: 2 },
});
const fieldB = fields.find((i) => i.name === 'field_b');
expect(fieldB.default).toEqual(4);
expect(fieldB.value).toEqual(4);
});
});
});

0 comments on commit d2cc213

Please sign in to comment.