Skip to content

Commit c2a60d4

Browse files
Bug: Issue with formData not updating when dependencies change (#4388)
* Fixing issue with formData value not changing when dependencies are updated. * refactoring tests and added test to test non-valid formData * update changeLog * added test for getValidFormData method * fixed failing issue because of type change * changes based on feedback * changes based on feedback * fixed test coverage * Override the formData with the const if the constAsDefaults is set to always * Fixed issue with validator-ajv6 ignoring oneOf in dependencies because it thinks there isn't exactly one subschema that is valid. * improvement based on feedback * Fixed failing tests * removed unnecessary code * improvement based on feedback. --------- Co-authored-by: Heath C <[email protected]>
1 parent e292164 commit c2a60d4

File tree

6 files changed

+2142
-1519
lines changed

6 files changed

+2142
-1519
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ should change the heading of the (upcoming) version to include a major version b
2020

2121
## @rjsf/utils
2222

23+
- Fixed issue with formData not updating when dependencies change, fixing [#4325](https://github.com/rjsf-team/react-jsonschema-form/issues/4325)
2324
- Fixed issue with assigning default values to formData with deeply nested required properties, fixing [#4399](https://github.com/rjsf-team/react-jsonschema-form/issues/4399)
2425

2526
# 5.23.2

packages/utils/src/mergeDefaultsWithFormData.ts

+42-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import get from 'lodash/get';
22

33
import isObject from './isObject';
44
import { GenericObjectType } from '../src';
5+
import isNil from 'lodash/isNil';
56

67
/** Merges the `defaults` object of type `T` into the `formData` of type `T`
78
*
@@ -19,47 +20,78 @@ import { GenericObjectType } from '../src';
1920
* @param [formData] - The form data into which the defaults will be merged
2021
* @param [mergeExtraArrayDefaults=false] - If true, any additional default array entries are appended onto the formData
2122
* @param [defaultSupercedesUndefined=false] - If true, an explicit undefined value will be overwritten by the default value
23+
* @param [overrideFormDataWithDefaults=false] - If true, the default value will overwrite the form data value. If the value
24+
* doesn't exist in the default, we take it from formData and in the case where the value is set to undefined in formData.
25+
* This is useful when we have already merged formData with defaults and want to add an additional field from formData
26+
* that does not exist in defaults.
2227
* @returns - The resulting merged form data with defaults
2328
*/
2429
export default function mergeDefaultsWithFormData<T = any>(
2530
defaults?: T,
2631
formData?: T,
2732
mergeExtraArrayDefaults = false,
28-
defaultSupercedesUndefined = false
33+
defaultSupercedesUndefined = false,
34+
overrideFormDataWithDefaults = false
2935
): T | undefined {
3036
if (Array.isArray(formData)) {
3137
const defaultsArray = Array.isArray(defaults) ? defaults : [];
32-
const mapped = formData.map((value, idx) => {
33-
if (defaultsArray[idx]) {
38+
39+
// If overrideFormDataWithDefaults is true, we want to override the formData with the defaults
40+
const overrideArray = overrideFormDataWithDefaults ? defaultsArray : formData;
41+
const overrideOppositeArray = overrideFormDataWithDefaults ? formData : defaultsArray;
42+
43+
const mapped = overrideArray.map((value, idx) => {
44+
if (overrideOppositeArray[idx]) {
3445
return mergeDefaultsWithFormData<any>(
3546
defaultsArray[idx],
36-
value,
47+
formData[idx],
3748
mergeExtraArrayDefaults,
38-
defaultSupercedesUndefined
49+
defaultSupercedesUndefined,
50+
overrideFormDataWithDefaults
3951
);
4052
}
4153
return value;
4254
});
55+
4356
// Merge any extra defaults when mergeExtraArrayDefaults is true
44-
if (mergeExtraArrayDefaults && mapped.length < defaultsArray.length) {
45-
mapped.push(...defaultsArray.slice(mapped.length));
57+
// Or when overrideFormDataWithDefaults is true and the default array is shorter than the formData array
58+
if ((mergeExtraArrayDefaults || overrideFormDataWithDefaults) && mapped.length < overrideOppositeArray.length) {
59+
mapped.push(...overrideOppositeArray.slice(mapped.length));
4660
}
4761
return mapped as unknown as T;
4862
}
4963
if (isObject(formData)) {
5064
const acc: { [key in keyof T]: any } = Object.assign({}, defaults); // Prevent mutation of source object.
5165
return Object.keys(formData as GenericObjectType).reduce((acc, key) => {
66+
const keyValue = get(formData, key);
67+
const keyExistsInDefaults = isObject(defaults) && key in (defaults as GenericObjectType);
68+
const keyExistsInFormData = key in (formData as GenericObjectType);
5269
acc[key as keyof T] = mergeDefaultsWithFormData<T>(
5370
defaults ? get(defaults, key) : {},
54-
get(formData, key),
71+
keyValue,
5572
mergeExtraArrayDefaults,
56-
defaultSupercedesUndefined
73+
defaultSupercedesUndefined,
74+
// overrideFormDataWithDefaults can be true only when the key value exists in defaults
75+
// Or if the key value doesn't exist in formData
76+
overrideFormDataWithDefaults && (keyExistsInDefaults || !keyExistsInFormData)
5777
);
5878
return acc;
5979
}, acc);
6080
}
61-
if (defaultSupercedesUndefined && formData === undefined) {
81+
82+
/**
83+
* If the defaultSupercedesUndefined flag is true
84+
* And formData is set to undefined or null and defaults are defined
85+
* Or if formData is a number and is NaN return defaults
86+
* Or if overrideFormDataWithDefaults flag is true and formData is set to not undefined/null return defaults
87+
*/
88+
if (
89+
(defaultSupercedesUndefined &&
90+
((!isNil(defaults) && isNil(formData)) || (typeof formData === 'number' && isNaN(formData)))) ||
91+
(overrideFormDataWithDefaults && !isNil(formData))
92+
) {
6293
return defaults;
6394
}
95+
6496
return formData;
6597
}

packages/utils/src/schema/getDefaultFormState.ts

+102-16
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ import {
3030
ValidatorType,
3131
} from '../types';
3232
import isMultiSelect from './isMultiSelect';
33+
import isSelect from './isSelect';
3334
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
35+
import isConstant from '../isConstant';
3436
import { JSONSchema7Object } from 'json-schema';
37+
import isEqual from 'lodash/isEqual';
38+
import optionsList from '../optionsList';
3539

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

@@ -170,6 +174,10 @@ interface ComputeDefaultsProps<T = any, S extends StrictRJSFSchema = RJSFSchema>
170174
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;
171175
/** Optional flag, if true, indicates this schema was required in the parent schema. */
172176
required?: boolean;
177+
/** Optional flag, if true, It will merge defaults into formData.
178+
* 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.
179+
*/
180+
shouldMergeDefaultsIntoFormData?: boolean;
173181
}
174182

175183
/** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into
@@ -194,6 +202,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
194202
experimental_defaultFormStateBehavior = undefined,
195203
experimental_customMergeAllOf = undefined,
196204
required,
205+
shouldMergeDefaultsIntoFormData = false,
197206
} = computeDefaultsProps;
198207
const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
199208
const schema: S = isObject(rawSchema) ? rawSchema : ({} as S);
@@ -246,6 +255,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
246255
parentDefaults: Array.isArray(parentDefaults) ? parentDefaults[idx] : undefined,
247256
rawFormData: formData as T,
248257
required,
258+
shouldMergeDefaultsIntoFormData,
249259
})
250260
) as T[];
251261
} else if (ONE_OF_KEY in schema) {
@@ -267,7 +277,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
267277
getClosestMatchingOption<T, S, F>(
268278
validator,
269279
rootSchema,
270-
isEmpty(formData) ? undefined : formData,
280+
rawFormData,
271281
oneOf as S[],
272282
0,
273283
discriminator,
@@ -285,7 +295,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
285295
getClosestMatchingOption<T, S, F>(
286296
validator,
287297
rootSchema,
288-
isEmpty(formData) ? undefined : formData,
298+
rawFormData,
289299
anyOf as S[],
290300
0,
291301
discriminator,
@@ -305,6 +315,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
305315
parentDefaults: defaults as T | undefined,
306316
rawFormData: formData as T,
307317
required,
318+
shouldMergeDefaultsIntoFormData,
308319
});
309320
}
310321

@@ -315,7 +326,68 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
315326

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

318-
return defaultBasedOnSchemaType ?? defaults;
329+
let defaultsWithFormData = defaultBasedOnSchemaType ?? defaults;
330+
// if shouldMergeDefaultsIntoFormData is true, then merge the defaults into the formData.
331+
if (shouldMergeDefaultsIntoFormData) {
332+
const { arrayMinItems = {} } = experimental_defaultFormStateBehavior || {};
333+
const { mergeExtraDefaults } = arrayMinItems;
334+
335+
const matchingFormData = ensureFormDataMatchingSchema(
336+
validator,
337+
schema,
338+
rootSchema,
339+
rawFormData,
340+
experimental_defaultFormStateBehavior
341+
);
342+
if (!isObject(rawFormData)) {
343+
defaultsWithFormData = mergeDefaultsWithFormData<T>(
344+
defaultsWithFormData as T,
345+
matchingFormData as T,
346+
mergeExtraDefaults,
347+
true
348+
) as T;
349+
}
350+
}
351+
352+
return defaultsWithFormData;
353+
}
354+
355+
/**
356+
* 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.
357+
*
358+
* @param validator - an implementation of the `ValidatorType` interface that will be used when necessary
359+
* @param schema - The schema for which the formData state is desired
360+
* @param rootSchema - The root schema, used to primarily to look up `$ref`s
361+
* @param formData - The current formData
362+
* @param experimental_defaultFormStateBehavior - Optional configuration object, if provided, allows users to override default form state behavior
363+
* @returns - valid formData that matches schema
364+
*/
365+
export function ensureFormDataMatchingSchema<
366+
T = any,
367+
S extends StrictRJSFSchema = RJSFSchema,
368+
F extends FormContextType = any
369+
>(
370+
validator: ValidatorType<T, S, F>,
371+
schema: S,
372+
rootSchema: S,
373+
formData: T | undefined,
374+
experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior
375+
): T | T[] | undefined {
376+
const isSelectField = !isConstant(schema) && isSelect(validator, schema, rootSchema);
377+
let validFormData: T | T[] | undefined = formData;
378+
if (isSelectField) {
379+
const getOptionsList = optionsList(schema);
380+
const isValid = getOptionsList?.some((option) => isEqual(option.value, formData));
381+
validFormData = isValid ? formData : undefined;
382+
}
383+
384+
// Override the formData with the const if the constAsDefaults is set to always
385+
const constTakesPrecedence = schema[CONST_KEY] && experimental_defaultFormStateBehavior?.constAsDefaults === 'always';
386+
if (constTakesPrecedence) {
387+
validFormData = schema.const as T;
388+
}
389+
390+
return validFormData;
319391
}
320392

321393
/** Computes the default value for objects.
@@ -337,6 +409,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
337409
experimental_defaultFormStateBehavior = undefined,
338410
experimental_customMergeAllOf = undefined,
339411
required,
412+
shouldMergeDefaultsIntoFormData,
340413
}: ComputeDefaultsProps<T, S> = {},
341414
defaults?: T | T[] | undefined
342415
): T {
@@ -370,6 +443,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
370443
parentDefaults: get(defaults, [key]),
371444
rawFormData: get(formData, [key]),
372445
required: retrievedSchema.required?.includes(key),
446+
shouldMergeDefaultsIntoFormData,
373447
});
374448
maybeAddDefaultToObject<T>(
375449
acc,
@@ -414,6 +488,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
414488
parentDefaults: get(defaults, [key]),
415489
rawFormData: get(formData, [key]),
416490
required: retrievedSchema.required?.includes(key),
491+
shouldMergeDefaultsIntoFormData,
417492
});
418493
// Since these are additional properties we don't need to add the `experimental_defaultFormStateBehavior` prop
419494
maybeAddDefaultToObject<T>(
@@ -448,6 +523,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
448523
experimental_defaultFormStateBehavior = undefined,
449524
experimental_customMergeAllOf = undefined,
450525
required,
526+
shouldMergeDefaultsIntoFormData,
451527
}: ComputeDefaultsProps<T, S> = {},
452528
defaults?: T | T[] | undefined
453529
): T | T[] | undefined {
@@ -475,6 +551,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
475551
experimental_customMergeAllOf,
476552
parentDefaults: item,
477553
required,
554+
shouldMergeDefaultsIntoFormData,
478555
});
479556
}) as T[];
480557
}
@@ -494,6 +571,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
494571
rawFormData: item,
495572
parentDefaults: get(defaults, [idx]),
496573
required,
574+
shouldMergeDefaultsIntoFormData,
497575
});
498576
}) as T[];
499577

@@ -542,6 +620,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
542620
experimental_defaultFormStateBehavior,
543621
experimental_customMergeAllOf,
544622
required,
623+
shouldMergeDefaultsIntoFormData,
545624
})
546625
) as T[];
547626
// then fill up the rest with either the item default or empty, up to minItems
@@ -608,26 +687,33 @@ export default function getDefaultFormState<
608687
throw new Error('Invalid schema: ' + theSchema);
609688
}
610689
const schema = retrieveSchema<T, S, F>(validator, theSchema, rootSchema, formData, experimental_customMergeAllOf);
690+
691+
// Get the computed defaults with 'shouldMergeDefaultsIntoFormData' set to true to merge defaults into formData.
692+
// This is done when for example the value from formData does not exist in the schema 'enum' property, in such
693+
// cases we take the value from the defaults because the value from the formData is not valid.
611694
const defaults = computeDefaults<T, S, F>(validator, schema, {
612695
rootSchema,
613696
includeUndefinedValues,
614697
experimental_defaultFormStateBehavior,
615698
experimental_customMergeAllOf,
616699
rawFormData: formData,
700+
shouldMergeDefaultsIntoFormData: true,
617701
});
618702

619-
if (formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData))) {
620-
// No form data? Use schema defaults.
621-
return defaults;
622-
}
623-
const { mergeDefaultsIntoFormData, arrayMinItems = {} } = experimental_defaultFormStateBehavior || {};
624-
const { mergeExtraDefaults } = arrayMinItems;
625-
const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined';
626-
if (isObject(formData)) {
627-
return mergeDefaultsWithFormData<T>(defaults as T, formData, mergeExtraDefaults, defaultSupercedesUndefined);
628-
}
629-
if (Array.isArray(formData)) {
630-
return mergeDefaultsWithFormData<T[]>(defaults as T[], formData, mergeExtraDefaults, defaultSupercedesUndefined);
703+
// If the formData is an object or an array, add additional properties from formData and override formData with
704+
// defaults since the defaults are already merged with formData.
705+
if (isObject(formData) || Array.isArray(formData)) {
706+
const { mergeDefaultsIntoFormData } = experimental_defaultFormStateBehavior || {};
707+
const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined';
708+
const result = mergeDefaultsWithFormData<T | T[]>(
709+
defaults,
710+
formData,
711+
true, // set to true to add any additional default array entries.
712+
defaultSupercedesUndefined,
713+
true // set to true to override formData with defaults if they exist.
714+
);
715+
return result;
631716
}
632-
return formData;
717+
718+
return defaults;
633719
}

0 commit comments

Comments
 (0)