Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

-->

# 6.0.0-beta.9

## @rjsf/util

- Allow form value overrides with defaults [#4625](https://github.com/rjsf-team/react-jsonschema-form/pull/4625
Copy link
Member

@heath-freenome heath-freenome May 23, 2025

Choose a reason for hiding this comment

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

This needs to move to a new # 6.0.0-beta.11 section since we released this one already


# 6.0.0-beta.8

## @rjsf/util
Expand Down
3 changes: 3 additions & 0 deletions packages/docs/docs/api-reference/form-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ NOTE: If there is a default for a field and the `formData` is unspecified, the d
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `useFormDataIfPresent` | Legacy behavior - Do not merge defaults if there is a value for a field in `formData` even if that value is explicitly set to `undefined` |
| `useDefaultIfFormDataUndefined` | If the value of a field within the `formData` is `undefined`, then use the default value instead |
| `useDefaultAlways` | Always use the default value instead of form data |

|

## experimental_customMergeAllOf

Expand Down
3 changes: 2 additions & 1 deletion packages/docs/docs/api-reference/utility-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ Merges the `defaults` object of type `T` into the `formData` of type `T`
When merging defaults and form data, we want to merge in this specific way:

- objects are deeply merged
- arrays are merged in such a way that:
- arrays are either replaced (when `defaultSupercedes` is true) or merged in such a way that:
- when the array is set in form data, only array entries set in form data are deeply merged; additional entries from the defaults are ignored unless `mergeExtraArrayDefaults` is true, in which case the extras are appended onto the end of the form data
- when the array is not set in form data, the default is copied over
- scalars are overwritten/set by form data
Expand All @@ -672,6 +672,7 @@ When merging defaults and form data, we want to merge in this specific way:
- [formData]: T | undefined - The form data into which the defaults will be merged
- [mergeExtraArrayDefaults=false]: boolean - If true, any additional default array entries are appended onto the formData
- [defaultSupercedesUndefined=false]: boolean - If true, an explicit undefined value will be overwritten by the default value
- [defaultSupercedes=false]: boolean - If true, a value will be overwritten by the default value

#### Returns

Expand Down
5 changes: 5 additions & 0 deletions packages/playground/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ const liveSettingsSelectSchema: RJSFSchema = {
title: 'Use default for undefined field value',
enum: ['useDefaultIfFormDataUndefined'],
},
{
type: 'string',
title: 'Always use default for field value',
enum: ['useDefaultAlways'],
},
],
},
},
Expand Down
46 changes: 28 additions & 18 deletions packages/utils/src/mergeDefaultsWithFormData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import get from 'lodash/get';

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

/** Merges the `defaults` object of type `T` into the `formData` of type `T`
Expand All @@ -20,25 +20,24 @@ import isNil from 'lodash/isNil';
* @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.
* @param [overrideFormDataWithDefaultsStrategy=OverrideFormDataStrategy.noop] - Strategy for merging defaults and form data
* @returns - The resulting merged form data with defaults
*/
export default function mergeDefaultsWithFormData<T = any>(
defaults?: T,
formData?: T,
mergeExtraArrayDefaults = false,
defaultSupercedesUndefined = false,
overrideFormDataWithDefaults = false,
overrideFormDataWithDefaultsStrategy: OverrideFormDataStrategy = OverrideFormDataStrategy.noop,
): T | undefined {
if (Array.isArray(formData)) {
const defaultsArray = Array.isArray(defaults) ? defaults : [];

// If overrideFormDataWithDefaults is true, we want to override the formData with the defaults
const overrideArray = overrideFormDataWithDefaults ? defaultsArray : formData;
const overrideOppositeArray = overrideFormDataWithDefaults ? formData : defaultsArray;
// If overrideFormDataWithDefaultsStrategy is not noop, we want to override the formData with the defaults
const overrideArray =
overrideFormDataWithDefaultsStrategy !== OverrideFormDataStrategy.noop ? defaultsArray : formData;
const overrideOppositeArray =
overrideFormDataWithDefaultsStrategy !== OverrideFormDataStrategy.noop ? formData : defaultsArray;

const mapped = overrideArray.map((value, idx) => {
// We want to explicitly make sure that the value is NOT undefined since null, 0 and empty space are valid values
Expand All @@ -48,33 +47,44 @@ export default function mergeDefaultsWithFormData<T = any>(
formData[idx],
mergeExtraArrayDefaults,
defaultSupercedesUndefined,
overrideFormDataWithDefaults,
overrideFormDataWithDefaultsStrategy,
);
}
return value;
});

// Merge any extra defaults when mergeExtraArrayDefaults is true
// Or when overrideFormDataWithDefaults is true and the default array is shorter than the formData array
if ((mergeExtraArrayDefaults || overrideFormDataWithDefaults) && mapped.length < overrideOppositeArray.length) {
// Or when overrideFormDataWithDefaults is 'merge' and the default array is shorter than the formData array
if (
(mergeExtraArrayDefaults || overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.merge) &&
mapped.length < overrideOppositeArray.length
) {
mapped.push(...overrideOppositeArray.slice(mapped.length));
}
return mapped as unknown as T;
}
if (isObject(formData)) {
const iterationSource =
overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.replace ? (defaults ?? {}) : 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) => {
return Object.keys(iterationSource 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);
// overrideFormDataWithDefaultsStrategy can be 'merge' only when the key value exists in defaults
// Or if the key value doesn't exist in formData
const keyOverrideDefaultStrategy =
overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.replace
? OverrideFormDataStrategy.replace
: keyExistsInDefaults || !keyExistsInFormData
? overrideFormDataWithDefaultsStrategy
: OverrideFormDataStrategy.noop;
acc[key as keyof T] = mergeDefaultsWithFormData<T>(
defaults ? get(defaults, key) : {},
keyValue,
mergeExtraArrayDefaults,
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),
keyOverrideDefaultStrategy,
);
return acc;
}, acc);
Expand All @@ -89,10 +99,10 @@ export default function mergeDefaultsWithFormData<T = any>(
if (
(defaultSupercedesUndefined &&
((!isNil(defaults) && isNil(formData)) || (typeof formData === 'number' && isNaN(formData)))) ||
(overrideFormDataWithDefaults && !isNil(formData))
(overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.merge && !isNil(formData))
) {
return defaults;
}

return formData;
return overrideFormDataWithDefaultsStrategy === OverrideFormDataStrategy.replace ? defaults : formData;
}
6 changes: 5 additions & 1 deletion packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
Experimental_DefaultFormStateBehavior,
FormContextType,
GenericObjectType,
OverrideFormDataStrategy,
RJSFSchema,
StrictRJSFSchema,
ValidatorType,
Expand Down Expand Up @@ -363,6 +364,9 @@ export function computeDefaults<T = any, S extends StrictRJSFSchema = RJSFSchema
matchingFormData as T,
mergeExtraDefaults,
true,
experimental_defaultFormStateBehavior?.mergeDefaultsIntoFormData === 'useDefaultAlways'
? OverrideFormDataStrategy.replace
: OverrideFormDataStrategy.noop,
) as T;
}
}
Expand Down Expand Up @@ -732,7 +736,7 @@ export default function getDefaultFormState<
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.
OverrideFormDataStrategy.merge, // set to 'merge' to override formData with defaults if they exist.
);
return result;
}
Expand Down
14 changes: 13 additions & 1 deletion packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ export type Experimental_DefaultFormStateBehavior = {
* even if that value is explicitly set to `undefined`
* - `useDefaultIfFormDataUndefined`: - If the value of a field within the `formData` is `undefined`, then use the
* default value instead
* - `useDefaultAlways`: - Always use the default value
*/
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined';
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined' | 'useDefaultAlways';
/** Optional enumerated flag controlling how const values are merged into the form data as defaults when dealing with
* undefined values, defaulting to `always`. The defaulting behavior for this flag will always be controlled by the
* `emptyObjectField` flag value. For instance, if `populateRequiredDefaults` is set and the const value is not
Expand Down Expand Up @@ -1265,3 +1266,14 @@ export interface SchemaUtilsType<T = any, S extends StrictRJSFSchema = RJSFSchem
*/
toPathSchema(schema: S, name?: string, formData?: T): PathSchema<T>;
}

/** Strategy for merging defaults with existing form data */
export enum OverrideFormDataStrategy {
/** No merge or override applied */
noop,
/** 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 */
merge,
/** Replace form data with defined default */
replace,
}
Loading
Loading