@rjsf/utils
Version:
Utility functions for @rjsf/core
103 lines (88 loc) • 4.11 kB
text/typescript
import isObject from './isObject';
import { isValueEmpty, retrieveSchema } from './schema';
import type { FormContextType, GenericObjectType, RJSFSchema, StrictRJSFSchema, ValidatorType } from './types';
/** Recursively removes optional objects from the `formData` that are empty (i.e., all their fields
* are undefined, null, empty strings, or themselves empty optional objects). This solves the problem
* where interacting with fields inside an optional object "activates" it permanently, making the
* form unsubmittable when the optional object has required inner fields.
*
* An object property is considered "optional" when it is NOT listed in its parent schema's `required`
* array.
*
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The JSON schema describing the `formData`
* @param [rootSchema] - The root schema, used primarily to look up `$ref`s
* @param [formData] - The current form data to prune
* @returns - A new copy of `formData` with empty optional objects removed, or `undefined` if the
* entire formData was pruned
* @deprecated - This function will be removed in a future release. The equivalent pruning behavior
* is now built into `omitExtraData` — use that instead.
*/
// oxlint-disable-next-line @typescript-eslint/no-deprecated
export default function removeOptionalEmptyObjects<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(validator: ValidatorType<T, S, F>, schema: S, rootSchema?: S, formData?: T): T | undefined {
if (!isObject(schema)) {
return formData;
}
const resolvedSchema = retrieveSchema<T, S, F>(validator, schema, rootSchema, formData);
if (Array.isArray(formData)) {
const itemsSchema = resolvedSchema.items as S | S[];
if (!itemsSchema) {
return formData;
}
let hasChanges = false;
const mapped = formData.map((item, index) => {
let itemSchema = itemsSchema as S;
if (Array.isArray(itemsSchema)) {
itemSchema = itemsSchema[index] || (resolvedSchema.additionalItems as S) || ({} as S);
}
// oxlint-disable-next-line @typescript-eslint/no-deprecated
const cleaned = removeOptionalEmptyObjects<T, S, F>(validator, itemSchema, rootSchema, item);
if (cleaned !== item) {
hasChanges = true;
}
return cleaned === undefined ? ({} as T) : cleaned;
});
// Although T is an array type here, we still need to cast it back to T since TS
// doesn't narrow the generic T automatically
return hasChanges ? (mapped as T) : formData;
}
const { properties, required: requiredFields = [] } = resolvedSchema;
if (!isObject(formData) || !properties) {
return formData;
}
const result: GenericObjectType = {};
const data = formData as GenericObjectType;
let hasAnyValue = false;
for (const key of Object.keys(data)) {
const value = data[key];
const propertySchema = (properties[key] || {}) as S;
const isRequired = requiredFields.includes(key);
const isObj = isObject(value);
const isArr = Array.isArray(value);
if ((isObj || isArr) && properties[key]) {
// Recursively process nested objects and arrays
// oxlint-disable-next-line @typescript-eslint/no-deprecated
const cleaned = removeOptionalEmptyObjects<T, S, F>(validator, propertySchema, rootSchema, value as T);
if (isRequired || !isValueEmpty(cleaned)) {
result[key] = cleaned;
hasAnyValue = true;
}
} else if (isRequired || !isValueEmpty(value) || !properties[key]) {
// Keep: required, non-empty, or not schema-defined; skip optional empty scalars silently
result[key] = value;
if (!isValueEmpty(value)) {
hasAnyValue = true;
}
}
}
// If the entire object ended up empty after pruning, return undefined so that the
// caller (which may itself be a recursive call) can decide whether to keep or drop it
if (!hasAnyValue && Object.keys(result).length === 0) {
return undefined;
}
return result as T;
}