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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ should change the heading of the (upcoming) version to include a major version b
- Updated `Experimental_DefaultFormStateBehavior` to add a new `constAsDefaults` option
- Updated `getDefaultFormState()` to use the new `constAsDefaults` option to control how const is used for defaulting, fixing [#4344](https://github.com/rjsf-team/react-jsonschema-form/issues/4344), [#4361](https://github.com/rjsf-team/react-jsonschema-form/issues/4361) and [#4377](https://github.com/rjsf-team/react-jsonschema-form/issues/4377)
- Use `experimental_customMergeAllOf` option in functions that have previously missed it.
- - Fixed issue with formData not updating when dependencies change, fixing [#4325](https://github.com/rjsf-team/react-jsonschema-form/issues/4325)

## @rjsf/validator-ajv8

Expand Down
44 changes: 33 additions & 11 deletions packages/utils/src/mergeDefaultsWithFormData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,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 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.
abdalla-rko marked this conversation as resolved.
Show resolved Hide resolved
* @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 &&
(formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData)))
abdalla-rko marked this conversation as resolved.
Show resolved Hide resolved
) {
return defaults;
} else if (overrideFormDataWithDefaults && (formData === undefined || formData === null)) {
abdalla-rko marked this conversation as resolved.
Show resolved Hide resolved
// If the overrideFormDataWithDefaults flag is true and formData is set to undefined or null return formData
return formData;
}
return formData;
return overrideFormDataWithDefaults ? defaults : formData;
}
93 changes: 79 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,49 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema

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

return defaultBasedOnSchemaType ?? defaults;
let defaultsWithFormData = defaultBasedOnSchemaType ?? defaults;
// if shouldMfMergeDefaultsIntoFormData is true, then merge the defaults into the formData.
abdalla-rko marked this conversation as resolved.
Show resolved Hide resolved
if (shouldMergeDefaultsIntoFormData) {
const { arrayMinItems = {} } = experimental_defaultFormStateBehavior || {};
const { mergeExtraDefaults } = arrayMinItems;

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

return defaultsWithFormData;
}

/**
* Gets valid formData. If it's not valid in the case of a selectField, we change it to a valid value.
* @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
abdalla-rko marked this conversation as resolved.
Show resolved Hide resolved
*/
export function getValidFormData<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really check validity w.r.t. to the schema so I think this name might be confusing. Maybe getFormDataMatchingSchema?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @nickgros, I think it's a much better name but I've changed it to ensureFormDataMatchingSchema because I think it's slightly better.

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 +389,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 +423,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 +468,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 +503,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 +531,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
experimental_customMergeAllOf,
parentDefaults: item,
required,
shouldMergeDefaultsIntoFormData,
});
}) as T[];
}
Expand All @@ -493,6 +551,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 +600,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 +667,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 restult = mergeDefaultsWithFormData<T>(
defaults as T,
formData,
true, // set to true to add any additional default array entries.
defaultSupercedesUndefined,
true // set to true to override formDat with defaults if they exist.
abdalla-rko marked this conversation as resolved.
Show resolved Hide resolved
);
return restult;
abdalla-rko marked this conversation as resolved.
Show resolved Hide resolved
}
return formData;

return defaults;
}
Loading
Loading