Skip to content

Commit 2e04b8f

Browse files
committed
Fixing issue with formData value not changing when dependencies are updated.
1 parent 62f3397 commit 2e04b8f

File tree

3 files changed

+247
-26
lines changed

3 files changed

+247
-26
lines changed

packages/utils/src/mergeDefaultsWithFormData.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,47 +19,69 @@ import { GenericObjectType } from '../src';
1919
* @param [formData] - The form data into which the defaults will be merged
2020
* @param [mergeExtraArrayDefaults=false] - If true, any additional default array entries are appended onto the formData
2121
* @param [defaultSupercedesUndefined=false] - If true, an explicit undefined value will be overwritten by the default value
22+
* @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.
2223
* @returns - The resulting merged form data with defaults
2324
*/
2425
export default function mergeDefaultsWithFormData<T = any>(
2526
defaults?: T,
2627
formData?: T,
2728
mergeExtraArrayDefaults = false,
28-
defaultSupercedesUndefined = false
29+
defaultSupercedesUndefined = false,
30+
overrideFormDataWithDefaults = false
2931
): T | undefined {
3032
if (Array.isArray(formData)) {
3133
const defaultsArray = Array.isArray(defaults) ? defaults : [];
32-
const mapped = formData.map((value, idx) => {
33-
if (defaultsArray[idx]) {
34+
35+
// If overrideFormDataWithDefaults is true, we want to override the formData with the defaults
36+
const overrideArray = overrideFormDataWithDefaults ? defaultsArray : formData;
37+
const overrideOppositeArray = overrideFormDataWithDefaults ? formData : defaultsArray;
38+
39+
const mapped = overrideArray.map((value, idx) => {
40+
if (overrideOppositeArray[idx]) {
3441
return mergeDefaultsWithFormData<any>(
3542
defaultsArray[idx],
36-
value,
43+
formData[idx],
3744
mergeExtraArrayDefaults,
38-
defaultSupercedesUndefined
45+
defaultSupercedesUndefined,
46+
overrideFormDataWithDefaults
3947
);
4048
}
4149
return value;
4250
});
51+
4352
// Merge any extra defaults when mergeExtraArrayDefaults is true
44-
if (mergeExtraArrayDefaults && mapped.length < defaultsArray.length) {
45-
mapped.push(...defaultsArray.slice(mapped.length));
53+
// Or when overrideFormDataWithDefaults is true and the default array is shorter than the formData array
54+
if ((mergeExtraArrayDefaults || overrideFormDataWithDefaults) && mapped.length < overrideOppositeArray.length) {
55+
mapped.push(...overrideOppositeArray.slice(mapped.length));
4656
}
4757
return mapped as unknown as T;
4858
}
4959
if (isObject(formData)) {
5060
const acc: { [key in keyof T]: any } = Object.assign({}, defaults); // Prevent mutation of source object.
5161
return Object.keys(formData as GenericObjectType).reduce((acc, key) => {
62+
const keyValue = get(formData, key);
63+
const keyExistsInDefaults = isObject(defaults) && key in (defaults as GenericObjectType);
64+
const keyExistsInFormData = key in (formData as GenericObjectType);
5265
acc[key as keyof T] = mergeDefaultsWithFormData<T>(
5366
defaults ? get(defaults, key) : {},
54-
get(formData, key),
67+
keyValue,
5568
mergeExtraArrayDefaults,
56-
defaultSupercedesUndefined
69+
defaultSupercedesUndefined,
70+
// overrideFormDataWithDefaults can be true only when the key value exists in defaults
71+
// Or if the key value doesn't exist in formData
72+
overrideFormDataWithDefaults && (keyExistsInDefaults || !keyExistsInFormData)
5773
);
5874
return acc;
5975
}, acc);
6076
}
61-
if (defaultSupercedesUndefined && formData === undefined) {
77+
if (
78+
defaultSupercedesUndefined &&
79+
(formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData)))
80+
) {
6281
return defaults;
82+
} else if (overrideFormDataWithDefaults && (formData === undefined || formData === null)) {
83+
// If the overrideFormDataWithDefaults flag is true and formData is set to undefined or null return formData
84+
return formData;
6385
}
64-
return formData;
86+
return overrideFormDataWithDefaults ? defaults : formData;
6587
}

packages/utils/src/schema/getDefaultFormState.ts

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ import {
3030
ValidatorType,
3131
} from '../types';
3232
import isMultiSelect from './isMultiSelect';
33+
import isSelect from './isSelect';
3334
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
3435
import isConstant from '../isConstant';
3536
import { JSONSchema7Object } from 'json-schema';
37+
import { isEqual } from 'lodash';
38+
import optionsList from '../optionsList';
3639

3740
/** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function.
3841
*/
@@ -168,6 +171,10 @@ interface ComputeDefaultsProps<T = any, S extends StrictRJSFSchema = RJSFSchema>
168171
experimental_customMergeAllOf?: Experimental_CustomMergeAllOf<S>;
169172
/** Optional flag, if true, indicates this schema was required in the parent schema. */
170173
required?: boolean;
174+
/** Optional flag, if true, It will merge defaults into formData.
175+
* 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.
176+
*/
177+
shouldMergeDefaultsIntoFormData?: boolean;
171178
}
172179

173180
/** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into
@@ -192,6 +199,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
192199
experimental_defaultFormStateBehavior = undefined,
193200
experimental_customMergeAllOf = undefined,
194201
required,
202+
shouldMergeDefaultsIntoFormData = false,
195203
} = computeDefaultsProps;
196204
const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T;
197205
const schema: S = isObject(rawSchema) ? rawSchema : ({} as S);
@@ -242,6 +250,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
242250
parentDefaults: Array.isArray(parentDefaults) ? parentDefaults[idx] : undefined,
243251
rawFormData: formData as T,
244252
required,
253+
shouldMergeDefaultsIntoFormData,
245254
})
246255
) as T[];
247256
} else if (ONE_OF_KEY in schema) {
@@ -289,6 +298,7 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
289298
parentDefaults: defaults as T | undefined,
290299
rawFormData: formData as T,
291300
required,
301+
shouldMergeDefaultsIntoFormData,
292302
});
293303
}
294304

@@ -299,7 +309,49 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
299309

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

302-
return defaultBasedOnSchemaType ?? defaults;
312+
let defaultsWithFormData = defaultBasedOnSchemaType ?? defaults;
313+
// if shouldMfMergeDefaultsIntoFormData is true, then merge the defaults into the formData.
314+
if (shouldMergeDefaultsIntoFormData) {
315+
const { arrayMinItems = {} } = experimental_defaultFormStateBehavior || {};
316+
const { mergeExtraDefaults } = arrayMinItems;
317+
318+
const validFormData = getValidFormData(validator, schema, rootSchema, rawFormData);
319+
if (!isObject(rawFormData)) {
320+
defaultsWithFormData = mergeDefaultsWithFormData<T>(
321+
defaultsWithFormData as T,
322+
validFormData as T,
323+
mergeExtraDefaults,
324+
true
325+
) as T;
326+
}
327+
}
328+
329+
return defaultsWithFormData;
330+
}
331+
332+
/**
333+
* Gets valid formData. If it's not valid in the case of a selectField, we change it to a valid value.
334+
* @param validator - an implementation of the `ValidatorType` interface that will be used when necessary
335+
* @param schema - The schema for which the formData state is desired
336+
* @param rootSchema The root schema, used to primarily to look up `$ref`s
337+
* @param formData The current formData
338+
* @returns valid formData
339+
*/
340+
function getValidFormData<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
341+
validator: ValidatorType<T, S, F>,
342+
schema: S,
343+
rootSchema: S,
344+
formData: T | undefined
345+
): T | T[] | undefined {
346+
const isSelectField = !isConstant(schema) && isSelect(validator, schema, rootSchema);
347+
let validFormData: T | T[] | undefined = formData;
348+
349+
if (isSelectField) {
350+
const getOptionsList = optionsList(schema);
351+
const isValid = getOptionsList?.some((option) => isEqual(option.value, formData)) ?? false;
352+
validFormData = isValid ? formData : undefined;
353+
}
354+
return validFormData;
303355
}
304356

305357
/** Computes the default value for objects.
@@ -321,6 +373,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
321373
experimental_defaultFormStateBehavior = undefined,
322374
experimental_customMergeAllOf = undefined,
323375
required,
376+
shouldMergeDefaultsIntoFormData,
324377
}: ComputeDefaultsProps<T, S> = {},
325378
defaults?: T | T[] | undefined
326379
): T {
@@ -351,6 +404,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
351404
parentDefaults: get(defaults, [key]),
352405
rawFormData: get(formData, [key]),
353406
required: retrievedSchema.required?.includes(key),
407+
shouldMergeDefaultsIntoFormData,
354408
});
355409
maybeAddDefaultToObject<T>(
356410
acc,
@@ -394,6 +448,7 @@ export function getObjectDefaults<T = any, S extends StrictRJSFSchema = RJSFSche
394448
parentDefaults: get(defaults, [key]),
395449
rawFormData: get(formData, [key]),
396450
required: retrievedSchema.required?.includes(key),
451+
shouldMergeDefaultsIntoFormData,
397452
});
398453
// Since these are additional properties we don't need to add the `experimental_defaultFormStateBehavior` prop
399454
maybeAddDefaultToObject<T>(
@@ -427,6 +482,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
427482
_recurseList = [],
428483
experimental_defaultFormStateBehavior = undefined,
429484
required,
485+
shouldMergeDefaultsIntoFormData,
430486
}: ComputeDefaultsProps<T, S> = {},
431487
defaults?: T | T[] | undefined
432488
): T | T[] | undefined {
@@ -453,6 +509,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
453509
experimental_defaultFormStateBehavior,
454510
parentDefaults: item,
455511
required,
512+
shouldMergeDefaultsIntoFormData,
456513
});
457514
}) as T[];
458515
}
@@ -471,6 +528,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
471528
rawFormData: item,
472529
parentDefaults: get(defaults, [idx]),
473530
required,
531+
shouldMergeDefaultsIntoFormData,
474532
});
475533
}) as T[];
476534

@@ -516,6 +574,7 @@ export function getArrayDefaults<T = any, S extends StrictRJSFSchema = RJSFSchem
516574
_recurseList,
517575
experimental_defaultFormStateBehavior,
518576
required,
577+
shouldMergeDefaultsIntoFormData,
519578
})
520579
) as T[];
521580
// then fill up the rest with either the item default or empty, up to minItems
@@ -582,26 +641,31 @@ export default function getDefaultFormState<
582641
throw new Error('Invalid schema: ' + theSchema);
583642
}
584643
const schema = retrieveSchema<T, S, F>(validator, theSchema, rootSchema, formData, experimental_customMergeAllOf);
644+
645+
// Get the computed defaults with 'shouldMergeDefaultsIntoFormData' set to true to merge defaults into formData.
646+
// 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.
585647
const defaults = computeDefaults<T, S, F>(validator, schema, {
586648
rootSchema,
587649
includeUndefinedValues,
588650
experimental_defaultFormStateBehavior,
589651
experimental_customMergeAllOf,
590652
rawFormData: formData,
653+
shouldMergeDefaultsIntoFormData: true,
591654
});
592655

593-
if (formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData))) {
594-
// No form data? Use schema defaults.
595-
return defaults;
596-
}
597-
const { mergeDefaultsIntoFormData, arrayMinItems = {} } = experimental_defaultFormStateBehavior || {};
598-
const { mergeExtraDefaults } = arrayMinItems;
599-
const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined';
600-
if (isObject(formData)) {
601-
return mergeDefaultsWithFormData<T>(defaults as T, formData, mergeExtraDefaults, defaultSupercedesUndefined);
602-
}
603-
if (Array.isArray(formData)) {
604-
return mergeDefaultsWithFormData<T[]>(defaults as T[], formData, mergeExtraDefaults, defaultSupercedesUndefined);
656+
// 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.
657+
if (isObject(formData) || Array.isArray(formData)) {
658+
const { mergeDefaultsIntoFormData } = experimental_defaultFormStateBehavior || {};
659+
const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined';
660+
const restult = mergeDefaultsWithFormData<T>(
661+
defaults as T,
662+
formData,
663+
true, // set to true to add any additional default array entries.
664+
defaultSupercedesUndefined,
665+
true // set to true to override formDat with defaults if they exist.
666+
);
667+
return restult;
605668
}
606-
return formData;
669+
670+
return defaults;
607671
}

0 commit comments

Comments
 (0)