Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ it according to semantic versioning. For example, if your PR adds a breaking cha
should change the heading of the (upcoming) version to include a major version bump.

-->
# 6.0.0-beta.12

## @rjsf/utils

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

# 6.0.0-beta.11

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
61 changes: 37 additions & 24 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,22 +47,27 @@ 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);
Expand All @@ -73,22 +77,31 @@ export default function mergeDefaultsWithFormData<T = any>(
const keyDefaultIsObject = keyExistsInDefaults && isObject(get(defaults, key));
const keyHasFormDataObject = keyExistsInFormData && isObject(keyValue);

if (keyDefaultIsObject && keyHasFormDataObject && !defaultValueIsNestedObject) {
acc[key as keyof T] = {
...get(defaults, key),
...keyValue,
};
if (
keyDefaultIsObject &&
keyHasFormDataObject &&
!defaultValueIsNestedObject &&
overrideFormDataWithDefaultsStrategy !== OverrideFormDataStrategy.replace
) {
acc[key as keyof T] = { ...keyDefault, ...keyValue };
return acc;
}

// 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>(
get(defaults, key) ?? {},
keyDefault,
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 @@ -103,10 +116,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 @@ -369,6 +370,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 @@ -745,7 +749,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 @@ -1266,3 +1267,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