Skip to content

Commit

Permalink
feat: Initial source code
Browse files Browse the repository at this point in the history
  • Loading branch information
sandrina-p committed May 16, 2023
1 parent f5460c6 commit 1efcc76
Show file tree
Hide file tree
Showing 16 changed files with 8,496 additions and 0 deletions.
124 changes: 124 additions & 0 deletions src/calculateConditionalProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import merge from 'lodash/merge';
import omit from 'lodash/omit';

import { extractParametersFromNode } from './helpers';
import { supportedTypes } from './internals/fields';
import { getFieldDescription, pickXKey } from './internals/helpers';
import { buildYupSchema } from './yupSchema';
/**
* @typedef {import('./createHeadlessForm').FieldParameters} FieldParameters
*/

/**
* Verifies if a field is required
* @param {Object} node - JSON schema parent node
* @param {String} inputName - input name
* @return {Boolean}
*/
function isFieldRequired(node, inputName) {
// For nested properties (case of fieldset) we need to check recursively
if (node?.required) {
return node.required.includes(inputName);
}

return false;
}

/**
* Loops recursively through fieldset fields and returns an copy version of them
* where the required property is updated.
*
* @param {Array} fields - list of fields of a fieldset
* @param {Object} property - property that relates with the list of fields
* @returns {Object}
*/
function rebuildInnerFieldsRequiredProperty(fields, property) {
if (property?.properties) {
return fields.map((field) => {
if (field.fields) {
return {
...field,
fields: rebuildInnerFieldsRequiredProperty(field.fields, property.properties[field.name]),
};
}
return {
...field,
required: isFieldRequired(property, field.name),
};
});
}

return fields.map((field) => ({
...field,
required: isFieldRequired(property, field.name),
}));
}

/**
* 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}
*/
export function calculateConditionalProperties(fieldParams, customProperties) {
/**
* 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) => {
// Check if the current field is conditionally declared in the schema

const conditionalProperty = conditionBranch?.properties?.[fieldParams.name];

if (conditionalProperty) {
const presentation = pickXKey(conditionalProperty, 'presentation') ?? {};

const fieldDescription = getFieldDescription(conditionalProperty, customProperties);

const newFieldParams = extractParametersFromNode({
...conditionalProperty,
...fieldDescription,
});

let fieldSetFields;

if (fieldParams.inputType === supportedTypes.FIELDSET) {
fieldSetFields = rebuildInnerFieldsRequiredProperty(
fieldParams.fields,
conditionalProperty
);
newFieldParams.fields = fieldSetFields;
}

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,
}),
};

return omit(merge(base, presentation, newFieldParams), ['inputType']);
}

// If field is not conditionally declared it should be visible if it's required
const isVisible = isRequired;

return {
isVisible,
required: isRequired,
schema: buildYupSchema({
...fieldParams,
...extractParametersFromNode(conditionBranch),
required: isRequired,
}),
};
};
}
91 changes: 91 additions & 0 deletions src/calculateCustomValidationProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import inRange from 'lodash/inRange';
import isFunction from 'lodash/isFunction';
import isNil from 'lodash/isNil';
import isObject from 'lodash/isObject';
import mapValues from 'lodash/mapValues';
import pick from 'lodash/pick';

import { pickXKey } from './internals/helpers';
import { buildYupSchema } from './yupSchema';

export const SUPPORTED_CUSTOM_VALIDATION_FIELD_PARAMS = ['minimum', 'maximum'];

const isCustomValidationAllowed = (fieldParams) => (customValidation, customValidationKey) => {
// don't apply custom validation in cases when the fn returns null.
if (isNil(customValidation)) {
return false;
}

const { minimum, maximum } = fieldParams;
const isAllowed = inRange(
customValidation,
minimum ?? -Infinity,
maximum ? maximum + 1 : Infinity
);

if (!isAllowed) {
const errorMessage = `Custom validation for ${fieldParams.name} is not allowed because ${customValidationKey}:${customValidation} is less strict than the original range: ${minimum} to ${maximum}`;

if (process.env.NODE_ENV === 'development') {
throw new Error(errorMessage);
} else {
// eslint-disable-next-line no-console
console.warn(errorMessage);
}
}

return isAllowed;
};

export function calculateCustomValidationProperties(fieldParams, customProperties) {
return (isRequired, conditionBranch, formValues) => {
const params = { ...fieldParams, ...conditionBranch?.properties?.[fieldParams.name] };
const presentation = pickXKey(params, 'presentation') ?? {};

const supportedParams = pick(customProperties, SUPPORTED_CUSTOM_VALIDATION_FIELD_PARAMS);

const checkIfAllowed = isCustomValidationAllowed(params);

const customErrorMessages = [];
const fieldParamsWithNewValidation = mapValues(
supportedParams,
(customValidationValue, customValidationKey) => {
const originalValidation = params[customValidationKey];

const customValidation = isFunction(customValidationValue)
? customValidationValue(formValues, params)
: customValidationValue;

if (isObject(customValidation)) {
if (checkIfAllowed(customValidation[customValidationKey], customValidationKey)) {
customErrorMessages.push(pickXKey(customValidation, 'errorMessage'));

return customValidation[customValidationKey];
}

return originalValidation;
}

return checkIfAllowed(customValidation, customValidationKey)
? customValidation
: originalValidation;
}
);

const errorMessage = Object.assign({ ...params.errorMessage }, ...customErrorMessages);

return {
...params,
...fieldParamsWithNewValidation,
type: presentation?.inputType || params.inputType,
errorMessage,
required: isRequired,
schema: buildYupSchema({
...params,
...fieldParamsWithNewValidation,
errorMessage,
required: isRequired,
}),
};
};
}
Loading

0 comments on commit 1efcc76

Please sign in to comment.