Skip to content

Commit d06c44c

Browse files
- In order to avoid regressions, added a new mergeDefaultsWithFormData prop to the Experimental_DefaultFormStateBehavior
- Updated `mergeDefaultsWithFormData()` to add new optional `defaultSupercedesUndefined` that when true uses the defaults rather than `undefined` formData - Updated `getDefaultFormState()` to pass true to `mergeDefaultsWithFormData` for `defaultSupercedesUndefined` when `mergeDefaultsIntoFormData` has the value `useDefaultIfFormDataUndefined` - Updated the documentation for the new capabilities - Updated the playground to add controls for the new `mergeDefaultsIntoFormData` option - moved the `Show Error List` component over one column, making it inline radio buttons rather than a select
1 parent caad3c5 commit d06c44c

File tree

10 files changed

+134
-23
lines changed

10 files changed

+134
-23
lines changed

CHANGELOG.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,26 @@ should change the heading of the (upcoming) version to include a major version b
1616
1717
-->
1818

19-
# 5.21.3
19+
# 5.22.0
2020

2121
## @rjsf/core
2222

23-
- Updated `MultiSchemaField` to call the `onChange` handler after setting the new option, fixing [#3997](https://github.com/rjsf-team/react-jsonschema-form/issues/3977)
23+
- Updated `MultiSchemaField` to call the `onChange` handler after setting the new option, fixing [#3997](https://github.com/rjsf-team/react-jsonschema-form/issues/3977) and [#4314](https://github.com/rjsf-team/react-jsonschema-form/issues/4314)
2424

2525
## @rjsf/utils
2626

27-
- Added `experimental_customMergeAllOf` option to `retrieveSchema` to allow custom merging of `allOf` schemas
28-
- Updated `mergeDefaultsWithFormData()` to not merge `undefined` when there is a proper default for it, fixing [#4322](https://github.com/rjsf-team/react-jsonschema-form/issues/4322)
29-
- Updated `getClosestMatchingOption()` to improve the scoring of sub-property objects that are provided over ones that aren't, fixing [#3997](https://github.com/rjsf-team/react-jsonschema-form/issues/3977)
27+
- Added `experimental_customMergeAllOf` option to `retrieveSchema()` and `getDefaultFormState()` to allow custom merging of `allOf` schemas
28+
- Added `mergeDefaultsIntoFormData` option to `Experimental_DefaultFormStateBehavior` type to control how to handle merging of defaults
29+
- Updated `mergeDefaultsWithFormData()` to add new optional `defaultSupercedesUndefined` that when true uses the defaults rather than `undefined` formData, fixing [#4322](https://github.com/rjsf-team/react-jsonschema-form/issues/4322)
30+
- Updated `getDefaultFormState()` to pass true to `mergeDefaultsWithFormData` for `defaultSupercedesUndefined` when `mergeDefaultsIntoFormData` has the value `useDefaultIfFormDataUndefined`, fixing [#4322](https://github.com/rjsf-team/react-jsonschema-form/issues/4322)
31+
- Updated `getClosestMatchingOption()` to improve the scoring of sub-property objects that are provided over ones that aren't, fixing [#3997](https://github.com/rjsf-team/react-jsonschema-form/issues/3977) and [#4314](https://github.com/rjsf-team/react-jsonschema-form/issues/4314)
32+
33+
## Dev / docs / playground
34+
35+
- Updated the `form-props.md` to add documentation for the new `experimental_customMergeAllOf` props and the `experimental_defaultFormStateBehavior.mergeDefaultsIntoFormData` option
36+
- Updated the `utility-functions.md` to add documentation for the new optional `defaultSupercedesUndefined` parameter and the two missing optional fields on `getDefaultFormState()`
37+
- Updated the playground to add controls for the new `mergeDefaultsIntoFormData` option
38+
- In the process, moved the `Show Error List` component over one column, making it inline radio buttons rather than a select
3039

3140
# 5.21.2
3241

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"precommit": "lint-staged",
1515
"publish-to-npm": "npm run build && npm publish",
1616
"test": "jest",
17-
"test:debug": "node --inspect-brk node_modules/.bin/jest",
17+
"test:debug": "node --inspect-brk ../../node_modules/.bin/jest",
1818
"test:update": "jest --u",
1919
"test:watch": "jest --watch",
2020
"test-coverage": "jest --coverage"

packages/docs/docs/api-reference/form-props.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,17 @@ render(
251251
);
252252
```
253253

254+
### mergeDefaultsIntoFormData
255+
256+
Optional enumerated flag controlling how the defaults are merged into the form data when dealing with undefined values, defaulting to `useFormDataIfPresent`.
257+
258+
NOTE: If there is a default for a field and the `formData` is unspecified, the default ALWAYS merges.
259+
260+
| Flag Value | Description |
261+
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
262+
| `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` |
263+
| `useDefaultIfFormDataUndefined` | If the value of a field within the `formData` is `undefined`, then use the default value instead |
264+
254265
## experimental_customMergeAllOf
255266

256267
The `experimental_customMergeAllOf` function allows you to provide a custom implementation for merging `allOf` schemas. This can be particularly useful in scenarios where the default [json-schema-merge-allof](https://github.com/mokkabonna/json-schema-merge-allof) library becomes a performance bottleneck, especially with large and complex schemas or doesn't satisfy your needs.

packages/docs/docs/api-reference/utility-functions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ When merging defaults and form data, we want to merge in this specific way:
575575
- [defaults]: T | undefined - The defaults to merge
576576
- [formData]: T | undefined - The form data into which the defaults will be merged
577577
- [mergeExtraArrayDefaults=false]: boolean - If true, any additional default array entries are appended onto the formData
578+
- [defaultSupercedesUndefined=false]: boolean - If true, an explicit undefined value will be overwritten by the default value
578579

579580
#### Returns
580581

@@ -897,6 +898,8 @@ Returns the superset of `formData` that includes the given set updated to includ
897898
- [formData]: T | undefined - The current formData, if any, onto which to provide any missing defaults
898899
- [rootSchema]: S | undefined - The root schema, used to primarily to look up `$ref`s
899900
- [includeUndefinedValues=false]: boolean | "excludeObjectChildren" - Optional flag, if true, cause undefined values to be added as defaults. If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as false when computing defaults for any nested object properties.
901+
- [experimental_defaultFormStateBehavior]: Experimental_DefaultFormStateBehavior - See Form props [experimental_defaultFormStateBehavior documentation](./api-reference/form-props.md#experimental_defaultFormStateBehavior)
902+
- [experimental_customMergeAllOf]: Experimental_CustomMergeAllOf<S> - See Form props [experimental_customMergeAllOf documentation](./api-reference/form-props.md#experimental_customMergeAllOf)
900903

901904
#### Returns
902905

packages/playground/src/components/Header.tsx

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,18 @@ const liveSettingsBooleanSchema: RJSFSchema = {
6363
noValidate: { type: 'boolean', title: 'Disable validation' },
6464
noHtml5Validate: { type: 'boolean', title: 'Disable HTML 5 validation' },
6565
focusOnFirstError: { type: 'boolean', title: 'Focus on 1st Error' },
66-
},
67-
};
68-
69-
const liveSettingsSelectSchema: RJSFSchema = {
70-
type: 'object',
71-
properties: {
7266
showErrorList: {
7367
type: 'string',
7468
default: 'top',
7569
title: 'Show Error List',
7670
enum: [false, 'top', 'bottom'],
7771
},
72+
},
73+
};
74+
75+
const liveSettingsSelectSchema: RJSFSchema = {
76+
type: 'object',
77+
properties: {
7878
experimental_defaultFormStateBehavior: {
7979
title: 'Default Form State Behavior (Experimental)',
8080
type: 'object',
@@ -157,11 +157,37 @@ const liveSettingsSelectSchema: RJSFSchema = {
157157
},
158158
],
159159
},
160+
mergeDefaultsIntoFormData: {
161+
type: 'string',
162+
title: 'Merge defaults into formData',
163+
default: 'useFormDataIfPresent',
164+
oneOf: [
165+
{
166+
type: 'string',
167+
title: 'Use undefined field value if present',
168+
enum: ['useFormDataIfPresent'],
169+
},
170+
{
171+
type: 'string',
172+
title: 'Use default for undefined field value',
173+
enum: ['useDefaultIfFormDataUndefined'],
174+
},
175+
],
176+
},
160177
},
161178
},
162179
},
163180
};
164181

182+
const liveSettingsBooleanUiSchema: UiSchema = {
183+
showErrorList: {
184+
'ui:widget': 'radio',
185+
'ui:options': {
186+
inline: true,
187+
},
188+
},
189+
};
190+
165191
const liveSettingsSelectUiSchema: UiSchema = {
166192
experimental_defaultFormStateBehavior: {
167193
'ui:options': {
@@ -282,6 +308,7 @@ export default function Header({
282308
formData={liveSettings}
283309
validator={localValidator}
284310
onChange={handleSetLiveSettings}
311+
uiSchema={liveSettingsBooleanUiSchema}
285312
>
286313
<div />
287314
</Form>

packages/utils/src/mergeDefaultsWithFormData.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,31 @@ import { GenericObjectType } from '../src';
1212
* are deeply merged; additional entries from the defaults are ignored unless `mergeExtraArrayDefaults` is true, in
1313
* which case the extras are appended onto the end of the form data
1414
* - when the array is not set in form data, the default is copied over
15-
* - scalars are overwritten/set by form data
15+
* - scalars are overwritten/set by form data unless undefined and there is a default AND `defaultSupercedesUndefined`
16+
* is true
1617
*
1718
* @param [defaults] - The defaults to merge
1819
* @param [formData] - The form data into which the defaults will be merged
1920
* @param [mergeExtraArrayDefaults=false] - If true, any additional default array entries are appended onto the formData
21+
* @param [defaultSupercedesUndefined=false] - If true, an explicit undefined value will be overwritten by the default value
2022
* @returns - The resulting merged form data with defaults
2123
*/
2224
export default function mergeDefaultsWithFormData<T = any>(
2325
defaults?: T,
2426
formData?: T,
25-
mergeExtraArrayDefaults = false
27+
mergeExtraArrayDefaults = false,
28+
defaultSupercedesUndefined = false
2629
): T | undefined {
2730
if (Array.isArray(formData)) {
2831
const defaultsArray = Array.isArray(defaults) ? defaults : [];
2932
const mapped = formData.map((value, idx) => {
3033
if (defaultsArray[idx]) {
31-
return mergeDefaultsWithFormData<any>(defaultsArray[idx], value, mergeExtraArrayDefaults);
34+
return mergeDefaultsWithFormData<any>(
35+
defaultsArray[idx],
36+
value,
37+
mergeExtraArrayDefaults,
38+
defaultSupercedesUndefined
39+
);
3240
}
3341
return value;
3442
});
@@ -44,10 +52,14 @@ export default function mergeDefaultsWithFormData<T = any>(
4452
acc[key as keyof T] = mergeDefaultsWithFormData<T>(
4553
defaults ? get(defaults, key) : {},
4654
get(formData, key),
47-
mergeExtraArrayDefaults
55+
mergeExtraArrayDefaults,
56+
defaultSupercedesUndefined
4857
);
4958
return acc;
5059
}, acc);
5160
}
52-
return formData === undefined ? defaults : formData;
61+
if (defaultSupercedesUndefined && formData === undefined) {
62+
return defaults;
63+
}
64+
return formData;
5365
}

packages/utils/src/schema/getDefaultFormState.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -566,12 +566,14 @@ export default function getDefaultFormState<
566566
// No form data? Use schema defaults.
567567
return defaults;
568568
}
569-
const { mergeExtraDefaults } = experimental_defaultFormStateBehavior?.arrayMinItems || {};
569+
const { mergeDefaultsIntoFormData, arrayMinItems = {} } = experimental_defaultFormStateBehavior || {};
570+
const { mergeExtraDefaults } = arrayMinItems;
571+
const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined';
570572
if (isObject(formData)) {
571-
return mergeDefaultsWithFormData<T>(defaults as T, formData, mergeExtraDefaults);
573+
return mergeDefaultsWithFormData<T>(defaults as T, formData, mergeExtraDefaults, defaultSupercedesUndefined);
572574
}
573575
if (Array.isArray(formData)) {
574-
return mergeDefaultsWithFormData<T[]>(defaults as T[], formData, mergeExtraDefaults);
576+
return mergeDefaultsWithFormData<T[]>(defaults as T[], formData, mergeExtraDefaults, defaultSupercedesUndefined);
575577
}
576578
return formData;
577579
}

packages/utils/src/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export type Experimental_ArrayMinItems = {
6666

6767
/** Experimental features to specify different default form state behaviors. Currently, this affects the
6868
* handling of optional array fields where `minItems` is set and handling of setting defaults based on the
69-
* value of `emptyObjectFields`.
69+
* value of `emptyObjectFields`. It also affects how `allOf` fields are handled and how to handle merging defaults into
70+
* the formData in relation to explicit `undefined` values via `mergeDefaultsIntoFormData`.
7071
*/
7172
export type Experimental_DefaultFormStateBehavior = {
7273
/** Optional object, that controls how the default form state for arrays with `minItems` is handled. When not provided
@@ -86,6 +87,15 @@ export type Experimental_DefaultFormStateBehavior = {
8687
* Optional flag to compute the default form state using allOf and if/then/else schemas. Defaults to `skipDefaults'.
8788
*/
8889
allOf?: 'populateDefaults' | 'skipDefaults';
90+
/** Optional enumerated flag controlling how the defaults are merged into the form data when dealing with undefined
91+
* values, defaulting to `useFormDataIfPresent`.
92+
* NOTE: If there is a default for a field and the `formData` is unspecified, the default ALWAYS merges.
93+
* - `useFormDataIfPresent`: Legacy behavior - Do not merge defaults if there is a value for a field in `formData`,
94+
* even if that value is explicitly set to `undefined`
95+
* - `useDefaultIfFormDataUndefined`: - If the value of a field within the `formData` is `undefined`, then use the
96+
* default value instead
97+
*/
98+
mergeDefaultsIntoFormData?: 'useFormDataIfPresent' | 'useDefaultIfFormDataUndefined';
8999
};
90100

91101
/** Optional function that allows for custom merging of `allOf` schemas

packages/utils/test/mergeDefaultsWithFormData.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ describe('mergeDefaultsWithFormData()', () => {
1717
expect(mergeDefaultsWithFormData(undefined, [2])).toEqual([2]);
1818
});
1919

20-
it('should return default when formData is undefined', () => {
21-
expect(mergeDefaultsWithFormData({}, undefined)).toEqual({});
20+
it('should return formData when formData is undefined', () => {
21+
expect(mergeDefaultsWithFormData({}, undefined)).toEqual(undefined);
22+
});
23+
24+
it('should return default when formData is undefined and defaultSupercedesUndefined true', () => {
25+
expect(mergeDefaultsWithFormData({}, undefined, undefined, true)).toEqual({});
2226
});
2327

2428
it('should return undefined when formData is undefined', () => {

packages/utils/test/schema/getDefaultFormStateTest.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3597,6 +3597,39 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
35973597
expect(getDefaultFormState(testValidator, schema, formData)).toEqual(result);
35983598
});
35993599
});
3600+
describe('object with defaults and undefined in formData, testing mergeDefaultsIntoFormData', () => {
3601+
let schema: RJSFSchema;
3602+
let defaultedFormData: any;
3603+
beforeAll(() => {
3604+
schema = {
3605+
type: 'object',
3606+
properties: {
3607+
field: {
3608+
type: 'string',
3609+
default: 'foo',
3610+
},
3611+
},
3612+
required: ['field'],
3613+
};
3614+
defaultedFormData = { field: 'foo' };
3615+
});
3616+
it('returns field value of default when formData is empty', () => {
3617+
const formData = {};
3618+
expect(getDefaultFormState(testValidator, schema, formData)).toEqual(defaultedFormData);
3619+
});
3620+
it('returns field value of undefined when formData has undefined for field', () => {
3621+
const formData = { field: undefined };
3622+
expect(getDefaultFormState(testValidator, schema, formData)).toEqual(formData);
3623+
});
3624+
it('returns field value of default when formData has undefined for field and `useDefaultIfFormDataUndefined`', () => {
3625+
const formData = { field: undefined };
3626+
expect(
3627+
getDefaultFormState(testValidator, schema, formData, undefined, undefined, {
3628+
mergeDefaultsIntoFormData: 'useDefaultIfFormDataUndefined',
3629+
})
3630+
).toEqual(defaultedFormData);
3631+
});
3632+
});
36003633
it('should return undefined defaults for a required array property with minItems', () => {
36013634
const schema: RJSFSchema = {
36023635
type: 'object',

0 commit comments

Comments
 (0)