@remoteoss/json-schema-form
Version:
Headless UI form powered by JSON Schemas
1,519 lines (1,501 loc) • 71.9 kB
JavaScript
/*!
Copyright (c) 2025 Remote Technology, Inc.
NPM Package: @remoteoss/json-schema-form@0.12.0-beta.0
Generated: Wed, 25 Jun 2025 12:45:39 GMT
MIT License
Copyright (c) 2023 Remote Technology, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// src/createHeadlessForm.js
import get3 from "lodash/get";
import isNil3 from "lodash/isNil";
import omit3 from "lodash/omit";
import omitBy2 from "lodash/omitBy";
import pick2 from "lodash/pick";
import size from "lodash/size";
// src/calculateConditionalProperties.js
import merge2 from "lodash/merge";
import omit2 from "lodash/omit";
// src/helpers.js
import get2 from "lodash/get";
import isNil from "lodash/isNil";
import omit from "lodash/omit";
import omitBy from "lodash/omitBy";
import pickBy from "lodash/pickBy";
import set from "lodash/set";
import { lazy } from "yup";
// src/utils.js
function convertDiskSizeFromTo(from, to) {
const units = ["bytes", "kb", "mb"];
return function convert(value) {
return value * Math.pow(1024, units.indexOf(from.toLowerCase())) / Math.pow(1024, units.indexOf(to.toLowerCase()));
};
}
function hasProperty(object2, propertyName) {
return Object.prototype.hasOwnProperty.call(object2, propertyName);
}
// src/internals/checkIfConditionMatches.js
function checkIfConditionMatchesProperties(node, formValues, formFields, logic) {
if (typeof node.if === "boolean") {
return node.if;
}
if (node.if.anyOf) {
return node.if.anyOf.some(
(property) => checkIfConditionMatchesProperties({ if: property }, formValues, formFields, logic)
);
}
return Object.keys(node.if.properties ?? {}).every((name) => {
const currentProperty = node.if.properties[name];
const value = formValues[name];
const hasEmptyValue = typeof value === "undefined" || // NOTE: This is a "Remote API" dependency, as empty fields are sent as "null".
value === null;
const hasIfExplicit = node.if.required?.includes(name);
if (hasEmptyValue && !hasIfExplicit) {
return true;
}
if (hasProperty(currentProperty, "const")) {
return compareFormValueWithSchemaValue(value, currentProperty.const);
}
if (currentProperty.contains?.pattern) {
const formValue = value || [];
if (Array.isArray(formValue)) {
const pattern = new RegExp(currentProperty.contains.pattern);
return (value || []).some((item) => pattern.test(item));
}
}
if (currentProperty.enum) {
return currentProperty.enum.includes(value);
}
if (currentProperty.properties) {
return checkIfConditionMatchesProperties(
{ if: currentProperty },
formValues[name],
getField(name, formFields).fields,
logic
);
}
const field = getField(name, formFields);
return validateFieldSchema(
{
...field,
...currentProperty,
required: true
},
value
);
});
}
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;
}
// src/internals/helpers.js
import merge from "lodash/fp/merge";
import get from "lodash/get";
import isEmpty from "lodash/isEmpty";
import isFunction from "lodash/isFunction";
function pickXKey(node, key) {
const deprecatedKeys = ["presentation", "errorMessage"];
return get(node, `x-jsf-${key}`, deprecatedKeys.includes(key) ? node?.[key] : void 0);
}
function getFieldDescription(node, customProperties = {}) {
const nodeDescription = node?.description ? {
description: node.description
} : {};
const customDescription = customProperties?.description ? {
description: isFunction(customProperties.description) ? customProperties.description(node?.description, {
...node,
...customProperties
}) : customProperties.description
} : {};
const nodePresentation = pickXKey(node, "presentation");
const presentation = !isEmpty(nodePresentation) && {
presentation: { ...nodePresentation, ...customDescription }
};
return merge(nodeDescription, { ...customDescription, ...presentation });
}
// src/internals/fields.js
var jsonTypes = {
STRING: "string",
NUMBER: "number",
INTEGER: "integer",
OBJECT: "object",
ARRAY: "array",
BOOLEAN: "boolean",
NULL: "null"
};
var supportedTypes = {
TEXT: "text",
NUMBER: "number",
SELECT: "select",
FILE: "file",
RADIO: "radio",
GROUP_ARRAY: "group-array",
EMAIL: "email",
DATE: "date",
CHECKBOX: "checkbox",
FIELDSET: "fieldset"
};
var jsonTypeToInputType = {
[jsonTypes.STRING]: ({ oneOf, format }) => {
if (format === "email")
return supportedTypes.EMAIL;
if (format === "date")
return supportedTypes.DATE;
if (format === "data-url")
return supportedTypes.FILE;
if (oneOf)
return supportedTypes.RADIO;
return supportedTypes.TEXT;
},
[jsonTypes.NUMBER]: () => supportedTypes.NUMBER,
[jsonTypes.INTEGER]: () => supportedTypes.NUMBER,
[jsonTypes.OBJECT]: () => supportedTypes.FIELDSET,
[jsonTypes.ARRAY]: ({ items }) => {
if (items.properties)
return supportedTypes.GROUP_ARRAY;
return supportedTypes.SELECT;
},
[jsonTypes.BOOLEAN]: () => supportedTypes.CHECKBOX
};
function getInputType(fieldProperties, strictInputType, name) {
const presentation = pickXKey(fieldProperties, "presentation") ?? {};
const presentationInputType = presentation?.inputType;
if (presentationInputType) {
return presentationInputType;
}
if (strictInputType) {
throw Error(`Strict error: Missing inputType to field "${name || fieldProperties.title}".
You can fix the json schema or skip this error by calling createHeadlessForm(schema, { strictInputType: false })`);
}
if (!fieldProperties.type) {
if (fieldProperties.items?.properties) {
return supportedTypes.GROUP_ARRAY;
}
if (fieldProperties.properties) {
return supportedTypes.SELECT;
}
return jsonTypeToInputType[jsonTypes.STRING](fieldProperties);
}
return jsonTypeToInputType[fieldProperties.type]?.(fieldProperties);
}
function _composeFieldFile({ name, label, description, accept, required = true, ...attrs }) {
return {
type: supportedTypes.FILE,
name,
label,
description,
required,
accept,
...attrs
};
}
function _composeFieldText({ name, label, description, required = true, ...attrs }) {
return {
type: supportedTypes.TEXT,
name,
label,
description,
required,
...attrs
};
}
function _composeFieldEmail({ name, label, required = true, ...attrs }) {
return {
type: supportedTypes.EMAIL,
name,
label,
required,
...attrs
};
}
function _composeFieldNumber({
name,
label,
percentage = false,
required = true,
minimum,
maximum,
...attrs
}) {
let minValue = minimum;
let maxValue = maximum;
if (percentage) {
minValue = minValue ?? 0;
maxValue = maxValue ?? 100;
}
return {
type: supportedTypes.NUMBER,
name,
label,
percentage,
required,
minimum: minValue,
maximum: maxValue,
...attrs
};
}
function _composeFieldDate({ name, label, required = true, ...attrs }) {
return {
type: supportedTypes.DATE,
name,
label,
required,
...attrs
};
}
function _composeFieldRadio({ name, label, options, required = true, ...attrs }) {
return {
type: supportedTypes.RADIO,
name,
label,
options,
required,
...attrs
};
}
function _composeFieldSelect({ name, label, options, required = true, ...attrs }) {
return {
type: supportedTypes.SELECT,
name,
label,
options,
required,
...attrs
};
}
function _composeNthFieldGroup({ name, label, required, nthFieldGroup, ...attrs }) {
return [
{
...nthFieldGroup,
type: supportedTypes.GROUP_ARRAY,
name,
label,
required,
...attrs
}
];
}
function _composeFieldCheckbox({
required = true,
name,
label,
description,
default: defaultValue,
checkboxValue,
...attrs
}) {
return {
type: supportedTypes.CHECKBOX,
required,
name,
label,
description,
checkboxValue,
...defaultValue && { default: defaultValue },
...attrs
};
}
function _composeFieldset({ name, label, fields, variant, ...attrs }) {
return {
type: supportedTypes.FIELDSET,
name,
label,
fields,
variant,
...attrs
};
}
var _composeFieldArbitraryClosure = (inputType) => (attrs) => ({
type: inputType,
...attrs
});
var inputTypeMap = {
text: _composeFieldText,
select: _composeFieldSelect,
radio: _composeFieldRadio,
date: _composeFieldDate,
number: _composeFieldNumber,
"group-array": _composeNthFieldGroup,
fieldset: _composeFieldset,
file: _composeFieldFile,
email: _composeFieldEmail,
checkbox: _composeFieldCheckbox
};
function _composeFieldCustomClosure(defaultComposeFn) {
return ({ fieldCustomization, ...attrs }) => {
const { description, ...restFieldCustomization } = fieldCustomization;
const fieldDescription = getFieldDescription(attrs, fieldCustomization);
const { nthFieldGroup, ...restAttrs } = attrs;
const commonAttrs = {
...restAttrs,
...restFieldCustomization,
...fieldDescription
};
if (attrs.inputType === supportedTypes.GROUP_ARRAY) {
return [
{
...nthFieldGroup,
...commonAttrs
}
];
}
return {
...defaultComposeFn(attrs),
...commonAttrs
};
};
}
// src/jsonLogic.js
import jsonLogic from "json-logic-js";
// src/yupSchema.js
import flow from "lodash/flow";
import noop from "lodash/noop";
import { randexp } from "randexp";
import { string, number, boolean, object, array, mixed } from "yup";
var DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
var baseString = string().trim();
var todayDateHint = (/* @__PURE__ */ new Date()).toISOString().substring(0, 10);
var convertBytesToKB = convertDiskSizeFromTo("Bytes", "KB");
var convertKbBytesToMB = convertDiskSizeFromTo("KB", "MB");
var validateOnlyStrings = string().trim().nullable().test(
"is-string",
"${path} must be a `string` type, but the final value was: `${value}`.",
(value, context) => {
if (context.originalValue !== null && context.originalValue !== void 0) {
return typeof context.originalValue === "string";
}
return true;
}
);
var compareDates = (d1, d2) => {
let date1 = new Date(d1).getTime();
let date2 = new Date(d2).getTime();
if (date1 < date2) {
return "LESSER";
} else if (date1 > date2) {
return "GREATER";
} else {
return "EQUAL";
}
};
var validateMinDate = (value, minDate) => {
const compare = compareDates(value, minDate);
return compare === "GREATER" || compare === "EQUAL" ? true : false;
};
var validateMaxDate = (value, minDate) => {
const compare = compareDates(value, minDate);
return compare === "LESSER" || compare === "EQUAL" ? true : false;
};
var validateRadioOrSelectOptions = (value, options) => {
if (value === void 0)
return true;
const exactMatch = options.some((option) => option.value === value);
if (exactMatch)
return true;
const patternMatch = options.some((option) => option.pattern?.test(value));
return !!patternMatch;
};
var yupSchemas = {
text: validateOnlyStrings,
radioOrSelectString: (options) => {
return string().nullable().transform((value) => {
if (value === "") {
return void 0;
}
if (options?.some((option) => option.value === null)) {
return value;
}
return value === null ? void 0 : value;
}).test(
"matchesOptionOrPattern",
({ value }) => `The option ${JSON.stringify(value)} is not valid.`,
(castValue, { originalValue }) => {
if (castValue !== void 0 && typeof originalValue !== "string") {
return false;
}
return validateRadioOrSelectOptions(castValue, options);
}
);
},
date: ({ minDate, maxDate }) => {
let dateString = string().nullable().transform((value) => {
if (value === "") {
return void 0;
}
return value === null ? void 0 : value;
}).trim().matches(
/(?:\d){4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9]|3[0-1])/,
`Must be a valid date in ${DEFAULT_DATE_FORMAT.toLocaleLowerCase()} format. e.g. ${todayDateHint}`
);
if (minDate) {
dateString = dateString.test(
"minDate",
`The date must be ${minDate} or after.`,
(value) => validateMinDate(value, minDate)
);
}
if (maxDate) {
dateString = dateString.test(
"maxDate",
`The date must be ${maxDate} or before.`,
(value) => validateMaxDate(value, maxDate)
);
}
return dateString;
},
radioOrSelectNumber: (options) => mixed().typeError("The value must be a number").transform((value) => {
if (options?.some((option) => option.value === null)) {
return value;
}
return value === null ? void 0 : value;
}).test(
"matchesOptionOrPattern",
({ value }) => {
return `The option ${JSON.stringify(value)} is not valid.`;
},
(value) => {
if (value !== void 0 && typeof value !== "number")
return false;
return validateRadioOrSelectOptions(value, options);
}
).nullable(),
radioOrSelectBoolean: (options) => {
return boolean().nullable().transform((value) => {
if (options?.some((option) => option.value === null)) {
return value;
}
return value === null ? void 0 : value;
}).test(
"matchesOptionOrPattern",
({ originalValue }) => {
return `The option ${JSON.stringify(originalValue)} is not valid.`;
},
(castValue, { originalValue }) => {
if (typeof originalValue !== "boolean" && castValue !== void 0)
return false;
return validateRadioOrSelectOptions(castValue, options);
}
);
},
number: number().typeError("The value must be a number").nullable(),
file: array().nullable(),
email: string().trim().email("Please enter a valid email address").nullable(),
fieldset: object().nullable(),
checkbox: string().trim().nullable(),
checkboxBool: boolean().typeError('The value must be a boolean, but received "${value}"').nullable(),
multiple: {
select: array().nullable(),
"group-array": array().nullable()
},
null: mixed().typeError("The value must be null").test(
"matchesNullValue",
({ value }) => `The value ${JSON.stringify(value)} is not valid.`,
(value) => value === void 0 || value === null
)
};
var yupSchemasToJsonTypes = {
string: yupSchemas.text,
number: yupSchemas.number,
integer: yupSchemas.number,
object: yupSchemas.fieldset,
array: yupSchemas.multiple.select,
boolean: yupSchemas.checkboxBool,
null: yupSchemas.null
};
function getRequiredErrorMessage(inputType, { inlineError, configError }) {
if (inlineError)
return inlineError;
if (configError)
return configError;
if (inputType === supportedTypes.CHECKBOX)
return "Please acknowledge this field";
return "Required field";
}
var getJsonTypeInArray = (jsonType) => {
return Array.isArray(jsonType) ? jsonType.find((val) => val !== "null") : jsonType;
};
var getOptions = (field) => {
const allValues = field.options?.map((option) => ({
value: option.value,
pattern: option.pattern ? new RegExp(option.pattern) : null
}));
const isOptionalWithNull = Array.isArray(field.jsonType) && // @TODO should also check the "oneOf" directly looking for "null"
// option but we don't have direct access at this point.
// Otherwise the JSON Schema validator will fail as explained in PR#18
field.jsonType.includes("null");
return isOptionalWithNull ? [...allValues, { option: null }] : allValues;
};
var getYupSchema = ({ inputType, ...field }) => {
const jsonType = getJsonTypeInArray(field.jsonType);
const hasOptions = field.options?.length > 0;
const generateOptionSchema = (type) => {
const optionValues = getOptions(field);
switch (type) {
case "number":
return yupSchemas.radioOrSelectNumber(optionValues);
case "boolean":
return yupSchemas.radioOrSelectBoolean(optionValues);
default:
return yupSchemas.radioOrSelectString(optionValues);
}
};
if (hasOptions) {
if (Array.isArray(field.jsonType)) {
return field.jsonType.includes("number") ? generateOptionSchema("number") : generateOptionSchema("string");
}
return generateOptionSchema(field.jsonType);
}
if (field.format === "date") {
return yupSchemas.date({ minDate: field.minDate, maxDate: field.maxDate });
}
return yupSchemas[inputType] || yupSchemasToJsonTypes[jsonType];
};
function buildYupSchema(field, config, logic) {
const { inputType, jsonType: jsonTypeValue, errorMessage = {}, ...propertyFields } = field;
const isCheckboxBoolean = typeof propertyFields.checkboxValue === "boolean";
let baseSchema;
const errorMessageFromConfig = config?.inputTypes?.[inputType]?.errorMessage || {};
const jsonType = getJsonTypeInArray(field.jsonType);
if (propertyFields.multiple) {
baseSchema = yupSchemas.multiple[inputType] || yupSchemasToJsonTypes.array;
} else if (isCheckboxBoolean) {
baseSchema = yupSchemas.checkboxBool;
} else {
baseSchema = getYupSchema(field);
}
if (!baseSchema) {
return noop;
}
const randomPlaceholder = propertyFields.pattern && randexp(propertyFields.pattern);
const requiredMessage = getRequiredErrorMessage(inputType, {
inlineError: errorMessage.required,
configError: errorMessageFromConfig.required
});
function withRequired(yupSchema) {
if (isCheckboxBoolean) {
return yupSchema.oneOf([true], requiredMessage).required(requiredMessage);
}
return yupSchema.required(requiredMessage);
}
function withInteger(yupSchema) {
return yupSchema.integer(
(message) => errorMessage.integer ?? errorMessageFromConfig.integer ?? `Must not contain decimal points. E.g. ${Math.floor(message.value)} instead of ${message.value}`
);
}
function withMin(yupSchema) {
return yupSchema.min(
propertyFields.minimum,
(message) => errorMessage.minimum ?? errorMessageFromConfig.minimum ?? `Must be greater or equal to ${message.min}`
);
}
function withMinLength(yupSchema) {
return yupSchema.min(
propertyFields.minLength,
(message) => errorMessage.minLength ?? errorMessageFromConfig.minLength ?? `Please insert at least ${message.min} characters`
);
}
function withMax(yupSchema) {
return yupSchema.max(
propertyFields.maximum,
(message) => errorMessage.maximum ?? errorMessageFromConfig.maximum ?? `Must be smaller or equal to ${message.max}`
);
}
function withMaxLength(yupSchema) {
return yupSchema.max(
propertyFields.maxLength,
(message) => errorMessage.maxLength ?? errorMessageFromConfig.maxLength ?? `Please insert up to ${message.max} characters`
);
}
function withMatches(yupSchema) {
return yupSchema.matches(
propertyFields.pattern,
() => errorMessage.pattern ?? errorMessageFromConfig.pattern ?? `Must have a valid format. E.g. ${randomPlaceholder}`
);
}
function isValidFileInput(files) {
return files === void 0 || files === null || files.every(
(file) => file instanceof File || Object.prototype.hasOwnProperty.call(file, "name")
);
}
function withFile(yupSchema) {
return yupSchema.test("isValidFile", "Not a valid file.", isValidFileInput);
}
function withMaxFileSize(yupSchema) {
return yupSchema.test(
"isValidFileSize",
errorMessage.maxFileSize ?? errorMessageFromConfig.maxFileSize ?? `File size too large. The limit is ${convertKbBytesToMB(propertyFields.maxFileSize)} MB.`,
(files) => isValidFileInput(files) && !files?.some((file) => convertBytesToKB(file.size) > propertyFields.maxFileSize)
);
}
function withFileFormat(yupSchema) {
return yupSchema.test(
"isSupportedFormat",
errorMessage.accept ?? errorMessageFromConfig.accept ?? `Unsupported file format. The acceptable formats are ${propertyFields.accept}.`,
(files) => isValidFileInput(files) && files?.length > 0 ? files.some((file) => {
const fileType = file.name.split(".").pop();
return propertyFields.accept.includes(fileType.toLowerCase());
}) : true
);
}
function withConst(yupSchema) {
return yupSchema.test(
"isConst",
errorMessage.const ?? errorMessageFromConfig.const ?? `The only accepted value is ${propertyFields.const}.`,
(value) => propertyFields.required === false && value === void 0 || value === null || value === propertyFields.const
);
}
function withBaseSchema() {
const customErrorMsg = errorMessage.type || errorMessageFromConfig.type;
if (customErrorMsg) {
return baseSchema.typeError(customErrorMsg);
}
return baseSchema;
}
function buildFieldSetSchema(innerFields) {
const fieldSetShape = {};
innerFields.forEach((fieldSetfield) => {
if (fieldSetfield.fields) {
fieldSetShape[fieldSetfield.name] = object().shape(
buildFieldSetSchema(fieldSetfield.fields)
);
} else {
fieldSetShape[fieldSetfield.name] = buildYupSchema(
{
...fieldSetfield,
inputType: fieldSetfield.type
},
config
)();
}
});
return fieldSetShape;
}
function buildGroupArraySchema() {
return object().shape(
propertyFields.nthFieldGroup.fields().reduce(
(schema, groupArrayField) => ({
...schema,
[groupArrayField.name]: buildYupSchema(groupArrayField, config)()
}),
{}
)
);
}
const validators = [withBaseSchema];
if (inputType === supportedTypes.GROUP_ARRAY) {
validators[0] = () => withBaseSchema().of(buildGroupArraySchema());
} else if (inputType === supportedTypes.FIELDSET) {
validators[0] = () => withBaseSchema().shape(buildFieldSetSchema(propertyFields.fields));
}
if (propertyFields.required) {
validators.push(withRequired);
}
if (inputType === supportedTypes.FILE) {
validators.push(withFile);
}
if (jsonType === "integer") {
validators.push(withInteger);
}
if (typeof propertyFields.minimum !== "undefined") {
validators.push(withMin);
}
if (typeof propertyFields.minLength !== "undefined") {
validators.push(withMinLength);
}
if (propertyFields.maximum !== void 0) {
validators.push(withMax);
}
if (propertyFields.maxLength) {
validators.push(withMaxLength);
}
if (propertyFields.pattern) {
validators.push(withMatches);
}
if (propertyFields.maxFileSize) {
validators.push(withMaxFileSize);
}
if (propertyFields.accept) {
validators.push(withFileFormat);
}
if (typeof propertyFields.const !== "undefined") {
validators.push(withConst);
}
if (propertyFields.jsonLogicValidations) {
propertyFields.jsonLogicValidations.forEach(
(id) => validators.push(yupSchemaWithCustomJSONLogic({ field, id, logic, config }))
);
}
return flow(validators);
}
function getNoSortEdges(fields = []) {
return fields.reduce((list, field) => {
if (field.noSortEdges) {
list.push(field.name);
}
return list;
}, []);
}
function getSchema(fields = [], config) {
const newSchema = {};
fields.forEach((field) => {
if (field.schema) {
if (field.name) {
if (field.inputType === supportedTypes.FIELDSET) {
const fieldsetSchema = buildYupSchema(field, config)();
newSchema[field.name] = fieldsetSchema;
} else {
newSchema[field.name] = field.schema;
}
} else {
Object.assign(newSchema, getSchema(field.fields, config));
}
}
});
return newSchema;
}
function buildCompleteYupSchema(fields, config) {
return object().shape(getSchema(fields, config), getNoSortEdges(fields));
}
// src/jsonLogic.js
function createValidationChecker(schema) {
const scopes = /* @__PURE__ */ new Map();
function createScopes(jsonSchema, key = "root") {
const sampleEmptyObject = buildSampleEmptyObject(schema);
scopes.set(key, createValidationsScope(jsonSchema));
Object.entries(jsonSchema?.properties ?? {}).filter(([, property]) => property.type === "object" || property.type === "array").forEach(([key2, property]) => {
if (property.type === "array") {
createScopes(property.items, `${key2}[]`);
} else {
createScopes(property, key2);
}
});
validateInlineRules(jsonSchema, sampleEmptyObject);
}
createScopes(schema);
return {
scopes,
getScope(name = "root") {
return scopes.get(name);
}
};
}
function createValidationsScope(schema) {
const validationMap = /* @__PURE__ */ new Map();
const computedValuesMap = /* @__PURE__ */ new Map();
const logic = schema?.["x-jsf-logic"] ?? {
validations: {},
computedValues: {}
};
const validations = Object.entries(logic.validations ?? {});
const computedValues = Object.entries(logic.computedValues ?? {});
const sampleEmptyObject = buildSampleEmptyObject(schema);
validations.forEach(([id, validation]) => {
if (!validation.rule) {
throw Error(`[json-schema-form] json-logic error: Validation "${id}" has missing rule.`);
}
checkRuleIntegrity(validation.rule, id, sampleEmptyObject);
validationMap.set(id, validation);
});
computedValues.forEach(([id, computedValue]) => {
if (!computedValue.rule) {
throw Error(`[json-schema-form] json-logic error: Computed value "${id}" has missing rule.`);
}
checkRuleIntegrity(computedValue.rule, id, sampleEmptyObject);
computedValuesMap.set(id, computedValue);
});
function validate(rule, values) {
return jsonLogic.apply(
rule,
replaceUndefinedValuesWithNulls({ ...sampleEmptyObject, ...values })
);
}
return {
validationMap,
computedValuesMap,
validate,
applyValidationRuleInCondition(id, values) {
const validation = validationMap.get(id);
return validate(validation.rule, values);
},
applyComputedValueInField(id, values, fieldName) {
const validation = computedValuesMap.get(id);
if (validation === void 0) {
throw Error(
`[json-schema-form] json-logic error: Computed value "${id}" doesn't exist in field "${fieldName}".`
);
}
return validate(validation.rule, values);
},
applyComputedValueRuleInCondition(id, values) {
const validation = computedValuesMap.get(id);
return validate(validation.rule, values);
}
};
}
function replaceUndefinedValuesWithNulls(values = {}) {
return Object.entries(values).reduce((prev, [key, value]) => {
return { ...prev, [key]: value === void 0 || value === null ? NaN : value };
}, {});
}
function yupSchemaWithCustomJSONLogic({ field, logic, config, id }) {
const { parentID = "root" } = config;
const validation = logic.getScope(parentID).validationMap.get(id);
if (validation === void 0) {
throw Error(
`[json-schema-form] json-logic error: "${field.name}" required validation "${id}" doesn't exist.`
);
}
return (yupSchema) => yupSchema.test(
`${field.name}-validation-${id}`,
validation?.errorMessage ?? "This field is invalid.",
(value, { parent }) => {
if (value === void 0 && !field.required)
return true;
return jsonLogic.apply(validation.rule, parent);
}
);
}
var HANDLEBARS_REGEX = /\{\{([^{}]+)\}\}/g;
function replaceHandlebarsTemplates({
value: toReplace,
logic,
formValues,
parentID,
name: fieldName
}) {
if (typeof toReplace === "string") {
return toReplace.replace(HANDLEBARS_REGEX, (match, key) => {
return logic.getScope(parentID).applyComputedValueInField(key.trim(), formValues, fieldName);
});
} else if (typeof toReplace === "object") {
const { value, ...rules } = toReplace;
if (Object.keys(rules).length > 1 && !value) {
throw Error("Cannot define multiple rules without a template string with key `value`.");
}
const computedTemplateValue = Object.entries(rules).reduce((prev, [key, rule]) => {
const computedValue = logic.getScope(parentID).validate(rule, formValues);
return prev.replaceAll(`{{${key}}}`, computedValue);
}, value);
return computedTemplateValue.replace(/\{\{([^{}]+)\}\}/g, (match, key) => {
return logic.getScope(parentID).applyComputedValueInField(key.trim(), formValues, fieldName);
});
}
return toReplace;
}
function calculateComputedAttributes(fieldParams, { parentID = "root" } = {}) {
return ({ logic, isRequired, config, formValues }) => {
const { name, computedAttributes } = fieldParams;
let attributes = Object.fromEntries(
Object.entries(computedAttributes).map(handleComputedAttribute(logic, formValues, parentID, name)).filter(([, value]) => value !== null)
);
const { "x-jsf-presentation": presentation, ...rest } = attributes;
if (presentation) {
attributes = {
...rest,
...presentation
};
}
return {
...attributes,
schema: buildYupSchema(
{ ...fieldParams, ...attributes, required: isRequired },
config,
logic
)
};
};
}
function handleComputedAttribute(logic, formValues, parentID, name) {
return ([key, value]) => {
switch (key) {
case "description":
return [key, replaceHandlebarsTemplates({ value, logic, formValues, parentID, name })];
case "title":
return ["label", replaceHandlebarsTemplates({ value, logic, formValues, parentID, name })];
case "x-jsf-errorMessage":
return [
"errorMessage",
handleNestedObjectForComputedValues(value, formValues, parentID, logic, name)
];
case "x-jsf-presentation": {
const values = {};
Object.entries(value).forEach(([presentationKey, presentationValue]) => {
if (typeof presentationValue === "object") {
values[presentationKey] = handleNestedObjectForComputedValues(
presentationValue,
formValues,
parentID,
logic,
name
);
} else {
values[presentationKey] = logic.getScope(parentID).applyComputedValueInField(presentationValue, formValues, name);
}
});
return [key, values];
}
case "const":
default: {
if (typeof value === "object" && value.rule) {
return [key, logic.getScope(parentID).validate(value.rule, formValues)];
}
return [key, logic.getScope(parentID).applyComputedValueInField(value, formValues, name)];
}
}
};
}
function handleNestedObjectForComputedValues(values, formValues, parentID, logic, name) {
return Object.fromEntries(
Object.entries(values).map(([key, value]) => {
return [key, replaceHandlebarsTemplates({ value, logic, formValues, parentID, name })];
})
);
}
function buildSampleEmptyObject(schema = {}) {
const sample = {};
if (typeof schema !== "object" || !schema.properties) {
return schema;
}
for (const key in schema.properties) {
if (schema.properties[key].type === "object") {
sample[key] = buildSampleEmptyObject(schema.properties[key]);
} else if (schema.properties[key].type === "array") {
const itemSchema = schema.properties[key].items;
sample[key] = buildSampleEmptyObject(itemSchema);
} else {
sample[key] = true;
}
}
return sample;
}
function validateInlineRules(jsonSchema, sampleEmptyObject) {
const properties = (jsonSchema?.properties || jsonSchema?.items?.properties) ?? {};
Object.entries(properties).filter(([, property]) => property["x-jsf-logic-computedAttrs"] !== void 0).forEach(([fieldName, property]) => {
Object.entries(property["x-jsf-logic-computedAttrs"]).filter(([, value]) => typeof value === "object").forEach(([key, item]) => {
Object.values(item).forEach((rule) => {
checkRuleIntegrity(
rule,
fieldName,
sampleEmptyObject,
(item2) => `[json-schema-form] json-logic error: fieldName "${item2.var}" doesn't exist in field "${fieldName}.x-jsf-logic-computedAttrs.${key}".`
);
});
});
});
}
function checkRuleIntegrity(rule, id, data, errorMessage = (item) => `[json-schema-form] json-logic error: rule "${id}" has no variable "${item.var}".`) {
Object.entries(rule ?? {}).map(([operator, subRule]) => {
if (!Array.isArray(subRule) && subRule !== null && subRule !== void 0)
return;
throwIfUnknownOperator(operator, subRule, id);
subRule.map((item) => {
const isVar = item !== null && typeof item === "object" && Object.hasOwn(item, "var");
if (isVar) {
const exists = jsonLogic.apply({ var: removeIndicesFromPath(item.var) }, data);
if (exists === null) {
throw Error(errorMessage(item));
}
} else {
checkRuleIntegrity(item, id, data);
}
});
});
}
function throwIfUnknownOperator(operator, subRule, id) {
try {
jsonLogic.apply({ [operator]: subRule });
} catch (e) {
if (e.message === `Unrecognized operation ${operator}`) {
throw Error(
`[json-schema-form] json-logic error: in "${id}" rule there is an unknown operator "${operator}".`
);
}
}
}
var regexToGetIndices = /\.\d+\./g;
function removeIndicesFromPath(path) {
const intermediatePath = path.replace(regexToGetIndices, ".");
return intermediatePath.replace(/\.\d+$/, "");
}
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 };
}
// src/helpers.js
var dynamicInternalJsfAttrs = [
"isVisible",
// Driven from conditionals state
"fields",
// driven from group-array
"getComputedAttributes",
// From json-logic
"computedAttributes",
// From json-logic
"calculateConditionalProperties",
// driven from conditionals
"calculateCustomValidationProperties",
// To be deprecated in favor of json-logic
"scopedJsonSchema",
// The respective JSON Schema
// HOTFIX/TODO Internal customizations, check test conditions.test.js for more info.
"Component",
"calculateDynamicProperties",
"visibilityCondition"
];
var dynamicInternalJsfAttrsObj = Object.fromEntries(
dynamicInternalJsfAttrs.map((k) => [k, true])
);
function removeConditionalStaleAttributes(field, conditionalAttrs, rootAttrs) {
Object.keys(field).forEach((key) => {
if (conditionalAttrs[key] === void 0 && rootAttrs[key] === void 0 && // Don't remove attrs that were declared in the root field.
dynamicInternalJsfAttrsObj[key] === void 0) {
field[key] = void 0;
}
});
}
function hasType(type, typeName) {
return Array.isArray(type) ? type.includes(typeName) : type === typeName;
}
function getField(fieldName, fields) {
return fields.find(({ name }) => name === fieldName);
}
function validateFieldSchema(field, value, logic) {
const validator = buildYupSchema(field, {}, logic);
return validator().isValidSync(value);
}
function compareFormValueWithSchemaValue(formValue, schemaValue) {
const currentPropertyValue = typeof schemaValue === "number" ? schemaValue : schemaValue || void 0;
return String(formValue) === String(currentPropertyValue);
}
function isFieldFilled(fieldValue) {
return Array.isArray(fieldValue) ? fieldValue.length > 0 : !!fieldValue;
}
function findFirstAnyOfMatch(nodes, formValues) {
return nodes.find(
({ required }) => required?.some((fieldName) => isFieldFilled(formValues[fieldName]))
) || nodes[0];
}
function getPrefillSubFieldValues(field, defaultValues, parentFieldKeyPath) {
let initialValue = defaultValues ?? {};
let fieldKeyPath = field.name;
if (parentFieldKeyPath) {
fieldKeyPath = fieldKeyPath ? `${parentFieldKeyPath}.${fieldKeyPath}` : parentFieldKeyPath;
}
const subFields = field.fields;
if (Array.isArray(subFields)) {
const subFieldValues = {};
subFields.forEach((subField) => {
Object.assign(
subFieldValues,
getPrefillSubFieldValues(subField, initialValue[field.name], fieldKeyPath)
);
});
if (field.inputType === supportedTypes.FIELDSET && field.valueGroupingDisabled) {
Object.assign(initialValue, subFieldValues);
} else {
initialValue[field.name] = subFieldValues;
}
} else {
if (typeof initialValue !== "object") {
console.warn(
`Field "${parentFieldKeyPath}"'s value is "${initialValue}", but should be type object.`
);
initialValue = getPrefillValues([field], {
// TODO nested fieldsets are not handled
});
} else {
initialValue = getPrefillValues([field], initialValue);
}
}
return initialValue;
}
function getPrefillValues(fields, initialValues = {}) {
fields.forEach((field) => {
const fieldName = field.name;
switch (field.type) {
case supportedTypes.GROUP_ARRAY: {
initialValues[fieldName] = initialValues[fieldName]?.map(
(subFieldValues) => getPrefillValues(field.fields(), subFieldValues)
);
break;
}
case supportedTypes.FIELDSET: {
const subFieldValues = getPrefillSubFieldValues(field, initialValues);
Object.assign(initialValues, subFieldValues);
break;
}
default: {
if (!initialValues[fieldName]) {
initialValues[fieldName] = field.default;
}
break;
}
}
});
return initialValues;
}
function updateField(field, requiredFields, node, formValues, logic, config) {
if (!field) {
return;
}
const fieldIsRequired = requiredFields.has(field.name);
if (node.properties && hasProperty(node.properties, field.name)) {
field.isVisible = !!node.properties[field.name];
}
if (fieldIsRequired) {
field.isVisible = true;
}
const updateAttributes = (fieldAttrs) => {
Object.entries(fieldAttrs).forEach(([key, value]) => {
field[key] = value;
if (key === "schema" && typeof value === "function") {
field[key] = value();
}
if (key === "value") {
const readOnlyPropertyWasUpdated = typeof fieldAttrs.readOnly !== "undefined";
const isReadonlyByDefault = field.readOnly;
const isReadonly = readOnlyPropertyWasUpdated ? fieldAttrs.readOnly : isReadonlyByDefault;
if (!isReadonly && (value === null || field.inputType === "checkbox")) {
field.value = void 0;
}
}
});
};
if (field.getComputedAttributes) {
const newAttributes = field.getComputedAttributes({
field,
isRequired: fieldIsRequired,
node,
formValues,
config,
logic
});
updateAttributes(newAttributes);
}
if (field.calculateConditionalProperties) {
const { rootFieldAttrs, newAttributes } = field.calculateConditionalProperties({
isRequired: fieldIsRequired,
conditionBranch: node,
formValues
});
updateAttributes(newAttributes);
removeConditionalStaleAttributes(field, newAttributes, rootFieldAttrs);
}
if (field.calculateCustomValidationProperties) {
const newAttributes = field.calculateCustomValidationProperties(
fieldIsRequired,
node,
formValues
);
updateAttributes(newAttributes);
}
}
function processNode({
node,
formValues,
formFields,
accRequired = /* @__PURE__ */ new Set(),
parentID = "root",
logic
}) {
const requiredFields = new Set(accRequired);
Object.keys(node.properties ?? []).forEach((fieldName) => {
const field = getField(fieldName, formFields);
updateField(field, requiredFields, node, formValues, logic, { parentID });
});
node.required?.forEach((fieldName) => {
requiredFields.add(fieldName);
updateField(getField(fieldName, formFields), requiredFields, node, formValues, logic, {
parentID
});
});
if (node.if !== void 0) {
const matchesCondition = checkIfConditionMatchesProperties(node, formValues, formFields, logic);
if (matchesCondition && node.then) {
const { required: branchRequired } = processNode({
node: node.then,
formValues,
formFields,
accRequired: requiredFields,
parentID,
logic
});
branchRequired.forEach((field) => requiredFields.add(field));
} else if (node.else) {
const { required: branchRequired } = processNode({
node: node.else,
formValues,
formFields,
accRequired: requiredFields,
parentID,
logic
});
branchRequired.forEach((field) => requiredFields.add(field));
}
}
if (node.anyOf) {
const firstMatchOfAnyOf = findFirstAnyOfMatch(node.anyOf, formValues);
firstMatchOfAnyOf.required?.forEach((fieldName) => {
requiredFields.add(fieldName);
});
node.anyOf.forEach(({ required = [] }) => {
required.forEach((fieldName) => {
const field = getField(fieldName, formFields);
updateField(field, requiredFields, node, formValues, logic, { parentID });
});
});
}
if (node.allOf) {
node.allOf.map(
(allOfNode) => processNode({
node: allOfNode,
formValues,
formFields,
accRequired: requiredFields,
parentID,
logic
})
).forEach(({ required: allOfItemRequired }) => {
allOfItemRequired.forEach(requiredFields.add, requiredFields);
});
}
if (node.properties) {
Object.entries(node.properties).forEach(([name, nestedNode]) => {
const inputType = getInputType(nestedNode);
if (inputType === supportedTypes.FIELDSET) {
processNode({
node: nestedNode,
formValues: formValues[name] || {},
formFields: getField(name, formFields).fields,
parentID: name,
logic
});
}
});
}
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
};
}
function clearValuesIfNotVisible(fields, formValues) {
fields.forEach(({ isVisible = true, name, inputType, fields: nestedFields }) => {
if (!isVisible) {
formValues[name] = null;
}
if (inputType === supportedTypes.FIELDSET && nestedFields && formValues[name]) {
clearValuesIfNotVisible(nestedFields, formValues[name]);
}
});
}
function updateFieldsProperties(fields, formValues, jsonSchema, logic) {
if (!jsonSchema?.properties) {
return;
}
processNode({ node: jsonSchema, formValues, formFields: fields, logic });
clearValuesIfNotVisible(fields, formValues);
}
var notNullOption = (opt) => opt.const !== null;
function flatPresentation(item) {
return Object.entries(item).reduce((newItem, [key, value]) => {
if (key === "x-jsf-presentation") {
return {
...newItem,
...value
};
}
return {
...newItem,
[key]: value
};
}, {});
}
function getFieldOptions(node, presentation) {
function convertToOptions(nodeOptions) {
return nodeOptions.filter(notNullOption).map(({ title, const: cons, ...item }) => ({
label: title,
value: cons,
...flatPresentation(item)
}));
}
if (presentation.options) {
return presentation.options;
}
if (node.oneOf || node.anyOf || presentation.inputType === "radio") {
return convertToOptions(node.oneOf || node.anyOf || []);
}
if (node.items?.anyOf) {
return convertToOptions(node.items.anyOf);
}
return null;
}
function extractParametersFromNode(schemaNode) {
if (!schemaNode) {
return {};
}
const presentation = pickXKey(schemaNode, "presentation") ?? {};
const errorMessage = pickXKey(schemaNode, "errorMessage") ?? {};
const jsonLogicValidations = schemaNode["x-jsf-logic-validations"];
const computedAttributes = schemaNode["x-jsf-logic-computedAttrs"];
const decoratedComputedAttributes = getDecoratedComputedAttributes(computedAttributes);
const node = omit(schemaNode, ["x-jsf-presentation", "presentation", "x-jsf-errorMessage"]);
const description = presentation?.description || node.description;
const statementDescription = presentation.statement?.description;
const value = typeof node.const !== "undefined" && typeof node.default !== "undefined" && node.const === node.default ? { forcedValue: node.const } : {};
return omitBy(
{
const: node.const,
...value,
label: node.title,
readOnly: node.readOnly,
...node.deprecated && {
deprecated: {
description: presentation.deprecated?.description
// @TODO/@IDEA These might be useful down the road :thinking:
// version: presentation.deprecated.version, // e.g. "1.1"
// replacement: presentation.deprecated.replacement, // e.g. ['contract_duration_type']
}
},
pattern: node.pattern,
options: getFieldOptions(node, presentation),
items: node.items,
maxLength: node.maxLength,
minLength: node.minLength,
minimum: node.minimum,
maximum: node.maximum,
maxFileSize: node.maxFileSize,
// @deprecated in favor of presentation.maxFileSize
default: node.default,
format: node.format,
// Checkboxes conditions
// — For checkboxes that only accept one value (string)
...presentation?.inputType === "checkbox" && { checkboxValue: node.const },
// - For checkboxes with boolean value
...presentation?.inputType === "checkbox" && node.type === "boolean" && {
// true is what describes this checkbox as a boolean, regardless if its required or not
checkboxValue: true
},
...hasType(node.type, "array") && {
multiple: true
},
// Handle [name].presentation
...presentation,
jsonLogicValidations,
computedAttributes: decoratedComputedAttributes,
description,
extra: presentation.extra,
statement: presentation.statement && {
...presentation.statement,
description: statementDescription
},
// Support scoped conditions (fieldsets)
if: node.if,
then: node.then,
else: node.else,
anyOf: node.anyOf,
allOf: node.allOf,
errorMessage,
// Pass down all x-* keys except x-jsf-*, as those are handled above.
...pickBy(node, (_, key) => key.startsWith("x-") && !key.startsWith("x-jsf-"))
},
isNil
);
}
function yupToFormErrors(yupError) {
if (!yupError) {
return yupError;
}
const errors = {};
if (yupError.inner) {
if (yupError.inner.length === 0) {
return set(errors, yupError.path, yupError.message);
}
yupError.inner.forEach((err) => {
if (!get2(errors, err.path)) {
set(errors, err.path, err.message);
}
});
}
return errors;
}
var handleValuesChange = (fields