Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Issue with formData not updating when dependencies change #4388

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ should change the heading of the (upcoming) version to include a major version b

-->

# 5.24.0

## @rjsf/utils

- Fixed issue with formData not updating when dependencies change, fixing [#4325](https://github.com/rjsf-team/react-jsonschema-form/issues/4325)

# 5.23.1

## @rjsf/chakra-ui
Expand Down
45 changes: 34 additions & 11 deletions packages/utils/src/mergeDefaultsWithFormData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import get from 'lodash/get';

import isObject from './isObject';
import { GenericObjectType } from '../src';
import { isNil } from 'lodash';

/** Merges the `defaults` object of type `T` into the `formData` of type `T`
*
Expand All @@ -19,47 +20,69 @@ import { GenericObjectType } from '../src';
* @param [formData] - The form data into which the defaults will be merged
* @param [mergeExtraArrayDefaults=false] - If true, any additional default array entries are appended onto the formData
* @param [defaultSupercedesUndefined=false] - If true, an explicit undefined value will be overwritten by the default value
* @param [overrideFormDataWithDefaults=false] - If true, the default value will overwrite the form data value. If the value
* doesn't exist in the default, we take it from formData and in the case where the value is set to undefined in formData.
* This is useful when we have already merged formData with defaults and want to add an additional field from formData
* that does not exist in defaults.
* @returns - The resulting merged form data with defaults
*/
export default function mergeDefaultsWithFormData<T = any>(
defaults?: T,
formData?: T,
mergeExtraArrayDefaults = false,
defaultSupercedesUndefined = false
defaultSupercedesUndefined = false,
overrideFormDataWithDefaults = false
): T | undefined {
if (Array.isArray(formData)) {
const defaultsArray = Array.isArray(defaults) ? defaults : [];
const mapped = formData.map((value, idx) => {
if (defaultsArray[idx]) {

// If overrideFormDataWithDefaults is true, we want to override the formData with the defaults
const overrideArray = overrideFormDataWithDefaults ? defaultsArray : formData;
const overrideOppositeArray = overrideFormDataWithDefaults ? formData : defaultsArray;

const mapped = overrideArray.map((value, idx) => {
if (overrideOppositeArray[idx]) {
return mergeDefaultsWithFormData<any>(
defaultsArray[idx],
value,
formData[idx],
mergeExtraArrayDefaults,
defaultSupercedesUndefined
defaultSupercedesUndefined,
overrideFormDataWithDefaults
);
}
return value;
});

// Merge any extra defaults when mergeExtraArrayDefaults is true
if (mergeExtraArrayDefaults && mapped.length < defaultsArray.length) {
mapped.push(...defaultsArray.slice(mapped.length));
// Or when overrideFormDataWithDefaults is true and the default array is shorter than the formData array
if ((mergeExtraArrayDefaults || overrideFormDataWithDefaults) && mapped.length < overrideOppositeArray.length) {
mapped.push(...overrideOppositeArray.slice(mapped.length));
}
return mapped as unknown as T;
}
if (isObject(formData)) {
const acc: { [key in keyof T]: any } = Object.assign({}, defaults); // Prevent mutation of source object.
return Object.keys(formData as GenericObjectType).reduce((acc, key) => {
const keyValue = get(formData, key);
const keyExistsInDefaults = isObject(defaults) && key in (defaults as GenericObjectType);
const keyExistsInFormData = key in (formData as GenericObjectType);
acc[key as keyof T] = mergeDefaultsWithFormData<T>(
defaults ? get(defaults, key) : {},
get(formData, key),
keyValue,
mergeExtraArrayDefaults,
defaultSupercedesUndefined
defaultSupercedesUndefined,
// overrideFormDataWithDefaults can be true only when the key value exists in defaults
// Or if the key value doesn't exist in formData
overrideFormDataWithDefaults && (keyExistsInDefaults || !keyExistsInFormData)
);
return acc;
}, acc);
}
if (defaultSupercedesUndefined && formData === undefined) {
if (defaultSupercedesUndefined && (isNil(formData) || (typeof formData === 'number' && isNaN(formData)))) {
return defaults;
} else if (overrideFormDataWithDefaults && isNil(formData)) {
// If the overrideFormDataWithDefaults flag is true and formData is set to undefined or null return formData
return formData;
}
return formData;
return overrideFormDataWithDefaults ? defaults : formData;
}
92 changes: 78 additions & 14 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ import {
ValidatorType,
} from '../types';
import isMultiSelect from './isMultiSelect';
import isSelect from './isSelect';
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
import isConstant from '../isConstant';
import { JSONSchema7Object } from 'json-schema';
import { isEqual } from 'lodash';
import optionsList from '../optionsList';

const PRIMITIVE_TYPES = ['string', 'number', 'integer', 'boolean', 'null'];

Expand Down Expand Up @@ -169,6 +173,10 @@ interface ComputeDefaultsProps<T = any, S extends StrictRJSFSchema = RJSFSchema>
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;
/** Optional flag, if true, indicates this schema was required in the parent schema. */
required?: boolean;
/** Optional flag, if true, It will merge defaults into formData.
* The formData should take precedence unless it's not valid. This is useful when for example the value from formData does not exist in the schema 'enum' property, in such cases we take the value from the defaults because the value from the formData is not valid.
*/
shouldMergeDefaultsIntoFormData?: boolean;
}

/** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into
Expand All @@ -193,6 +201,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
experimental_defaultFormStateBehavior = undefined,
experimental_customMergeAllOf = undefined,
required,
shouldMergeDefaultsIntoFormData = false,
} = computeDefaultsProps;
const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
const schema: S = isObject(rawSchema) ? rawSchema : ({} as S);
Expand Down Expand Up @@ -245,6 +254,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
parentDefaults: Array.isArray(parentDefaults) ? parentDefaults[idx] : undefined,
rawFormData: formData as T,
required,
shouldMergeDefaultsIntoFormData,
})
) as T[];
} else if (ONE_OF_KEY in schema) {
Expand Down Expand Up @@ -304,6 +314,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
parentDefaults: defaults as T | undefined,
rawFormData: formData as T,
required,
shouldMergeDefaultsIntoFormData,
});
}

Expand All @@ -314,7 +325,48 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema

const defaultBasedOnSchemaType = getDefaultBasedOnSchemaType(validator, schema, computeDefaultsProps, defaults);

return defaultBasedOnSchemaType ?? defaults;
let defaultsWithFormData = defaultBasedOnSchemaType ?? defaults;
// if shouldMergeDefaultsIntoFormData is true, then merge the defaults into the formData.
if (shouldMergeDefaultsIntoFormData) {
const { arrayMinItems = {} } = experimental_defaultFormStateBehavior || {};
const { mergeExtraDefaults } = arrayMinItems;

const matchingFormData = ensureFormDataMatchingSchema(validator, schema, rootSchema, rawFormData);
if (!isObject(rawFormData)) {
defaultsWithFormData = mergeDefaultsWithFormData<T>(
defaultsWithFormData as T,
matchingFormData as T,
mergeExtraDefaults,
true
) as T;
}
}

return defaultsWithFormData;
}

/**
* Ensure that the formData matches the given schema. If it's not matching in the case of a selectField, we change it to match the schema.
* @param validator - an implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The schema for which the formData state is desired
* @param rootSchema - The root schema, used to primarily to look up `$ref`s
* @param formData - The current formData
* @returns - valid formData that matches schema
*/
export function ensureFormDataMatchingSchema<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(validator: ValidatorType<T, S, F>, schema: S, rootSchema: S, formData: T | undefined): T | T[] | undefined {
const isSelectField = !isConstant(schema) && isSelect(validator, schema, rootSchema);
let validFormData: T | T[] | undefined = formData;

if (isSelectField) {
const getOptionsList = optionsList(schema);
const isValid = getOptionsList?.some((option) => isEqual(option.value, formData));
validFormData = isValid ? formData : undefined;
}
return validFormData;
}

/** Computes the default value for objects.
Expand All @@ -336,6 +388,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
experimental_defaultFormStateBehavior = undefined,
experimental_customMergeAllOf = undefined,
required,
shouldMergeDefaultsIntoFormData,
}: ComputeDefaultsProps<T, S> = {},
defaults?: T | T[] | undefined
): T {
Expand Down Expand Up @@ -369,6 +422,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
parentDefaults: get(defaults, [key]),
rawFormData: get(formData, [key]),
required: retrievedSchema.required?.includes(key),
shouldMergeDefaultsIntoFormData,
});
maybeAddDefaultToObject<T>(
acc,
Expand Down Expand Up @@ -413,6 +467,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
parentDefaults: get(defaults, [key]),
rawFormData: get(formData, [key]),
required: retrievedSchema.required?.includes(key),
shouldMergeDefaultsIntoFormData,
});
// Since these are additional properties we don't need to add the `experimental_defaultFormStateBehavior` prop
maybeAddDefaultToObject<T>(
Expand Down Expand Up @@ -447,6 +502,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
experimental_defaultFormStateBehavior = undefined,
experimental_customMergeAllOf = undefined,
required,
shouldMergeDefaultsIntoFormData,
}: ComputeDefaultsProps<T, S> = {},
defaults?: T | T[] | undefined
): T | T[] | undefined {
Expand Down Expand Up @@ -474,6 +530,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
experimental_customMergeAllOf,
parentDefaults: item,
required,
shouldMergeDefaultsIntoFormData,
});
}) as T[];
}
Expand All @@ -493,6 +550,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
rawFormData: item,
parentDefaults: get(defaults, [idx]),
required,
shouldMergeDefaultsIntoFormData,
});
}) as T[];

Expand Down Expand Up @@ -541,6 +599,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
experimental_defaultFormStateBehavior,
experimental_customMergeAllOf,
required,
shouldMergeDefaultsIntoFormData,
})
) as T[];
// then fill up the rest with either the item default or empty, up to minItems
Expand Down Expand Up @@ -607,26 +666,31 @@ export default function getDefaultFormState<
throw new Error('Invalid schema: ' + theSchema);
}
const schema = retrieveSchema<T, S, F>(validator, theSchema, rootSchema, formData, experimental_customMergeAllOf);

// Get the computed defaults with 'shouldMergeDefaultsIntoFormData' set to true to merge defaults into formData.
// This is done when for example the value from formData does not exist in the schema 'enum' property, in such cases we take the value from the defaults because the value from the formData is not valid.
const defaults = computeDefaults<T, S, F>(validator, schema, {
rootSchema,
includeUndefinedValues,
experimental_defaultFormStateBehavior,
experimental_customMergeAllOf,
rawFormData: formData,
shouldMergeDefaultsIntoFormData: true,
});

if (formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData))) {
// No form data? Use schema defaults.
return defaults;
}
const { mergeDefaultsIntoFormData, arrayMinItems = {} } = experimental_defaultFormStateBehavior || {};
const { mergeExtraDefaults } = arrayMinItems;
const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined';
if (isObject(formData)) {
return mergeDefaultsWithFormData<T>(defaults as T, formData, mergeExtraDefaults, defaultSupercedesUndefined);
}
if (Array.isArray(formData)) {
return mergeDefaultsWithFormData<T[]>(defaults as T[], formData, mergeExtraDefaults, defaultSupercedesUndefined);
// If the formData is an object or an array, add additional properties from formData and override formData with defaults since the defaults are already merged with formData.
if (isObject(formData) || Array.isArray(formData)) {
const { mergeDefaultsIntoFormData } = experimental_defaultFormStateBehavior || {};
const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined';
const result = mergeDefaultsWithFormData<T>(
defaults as T,
formData,
true, // set to true to add any additional default array entries.
defaultSupercedesUndefined,
true // set to true to override formData with defaults if they exist.
);
return result;
}
return formData;

return defaults;
}
Loading
Loading