Skip to content

Commit 27e6956

Browse files
Fix: made small changes to utility functions to fix #3997, #4314 and #4322 (#4329)
* Fix: made small changes to utility functions to fix #3997 and #4322 Fixes #3997 and #4322 - In `@rjsf/utils`, made the following changes: - Updated `mergeDefaultsWithFormData()` to not overwrite a default when the formData has an undefined value - Updated `getClosestMatchingOption()` to improve the scoring function so that an object container that matches a key gets an extra point - In `@rjsf/core`, updated `MultiSchemaField` to call `onChange` after setting the new option in state rather than before - Updated the `CHANGELOG.md` accordingly * - 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 * - Improved documentation a teeny bit * - More doc improvements * - Responded to reviewer feedback * - Fixed broken links * - Added section header for wrapping BaseInputTemplate * - Updated CHANGELOG.md
1 parent f6c5bf7 commit 27e6956

14 files changed

+158
-21
lines changed

CHANGELOG.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,26 @@ should change the heading of the (upcoming) version to include a major version b
1818

1919
# 5.22.0
2020

21+
## @rjsf/core
22+
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)
24+
2125
## @rjsf/utils
2226

27+
- Added `experimental_customMergeAllOf` option to `retrieveSchema()` and `getDefaultFormState()` to allow custom merging of `allOf` schemas
2328
- Made fields with const property pre-filled and readonly, fixing [#2600](https://github.com/rjsf-team/react-jsonschema-form/issues/2600)
24-
- Added `experimental_customMergeAllOf` option to `retrieveSchema` to allow custom merging of `allOf` schemas
29+
- Added `mergeDefaultsIntoFormData` option to `Experimental_DefaultFormStateBehavior` type to control how to handle merging of defaults
30+
- 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)
31+
- 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)
32+
- 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)
33+
34+
## Dev / docs / playground
35+
36+
- Updated the `form-props.md` to add documentation for the new `experimental_customMergeAllOf` props and the `experimental_defaultFormStateBehavior.mergeDefaultsIntoFormData` option
37+
- Updated the `utility-functions.md` to add documentation for the new optional `defaultSupercedesUndefined` parameter and the two missing optional fields on `getDefaultFormState()`
38+
- Updated the `custom-templates.md` to add a section header for wrapping `BaseInputTemplate`
39+
- Updated the playground to add controls for the new `mergeDefaultsIntoFormData` option
40+
- In the process, moved the `Show Error List` component over one column, making it inline radio buttons rather than a select
2541

2642
# 5.21.2
2743

packages/core/package.json

+1-1
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/core/src/components/fields/MultiSchemaField.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,10 @@ class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
128128
// so that only the root objects themselves are created without adding undefined children properties
129129
newFormData = schemaUtils.getDefaultFormState(newOption, newFormData, 'excludeObjectChildren') as T;
130130
}
131-
onChange(newFormData, undefined, this.getFieldId());
132131

133-
this.setState({ selectedOption: intOption });
132+
this.setState({ selectedOption: intOption }, () => {
133+
onChange(newFormData, undefined, this.getFieldId());
134+
});
134135
};
135136

136137
getFieldId() {

packages/docs/docs/advanced-customization/custom-templates.md

+2
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ render(
362362
);
363363
```
364364

365+
### Wrapping BaseInputTemplate to customize it
366+
365367
Sometimes you just need to pass some additional properties to the existing `BaseInputTemplate`.
366368
The way to do this varies based upon whether you are using `core` or some other theme (such as `mui`):
367369

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

+11
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

+3
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` documentation for the [experimental_defaultFormStateBehavior](./form-props.md#experimental_defaultFormStateBehavior) prop
902+
- [experimental_customMergeAllOf]: Experimental_CustomMergeAllOf&lt;S&gt; - See `Form` documentation for the [experimental_customMergeAllOf](./form-props.md#experimental_customMergeAllOf) prop
900903

901904
#### Returns
902905

packages/playground/src/components/Header.tsx

+33-6
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

+16-4
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
}
61+
if (defaultSupercedesUndefined && formData === undefined) {
62+
return defaults;
63+
}
5264
return formData;
5365
}

packages/utils/src/schema/getClosestMatchingOption.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function calculateIndexScore<T = any, S extends StrictRJSFSchema = RJSFSc
5151
validator: ValidatorType<T, S, F>,
5252
rootSchema: S,
5353
schema?: S,
54-
formData: any = {}
54+
formData?: any
5555
): number {
5656
let totalScore = 0;
5757
if (schema) {
@@ -83,7 +83,11 @@ export function calculateIndexScore<T = any, S extends StrictRJSFSchema = RJSFSc
8383
);
8484
}
8585
if (value.type === 'object') {
86-
return score + calculateIndexScore<T, S, F>(validator, rootSchema, value as S, formValue || {});
86+
if (isObject(formValue)) {
87+
// If the structure is matching then give it a little boost in score
88+
score += 1;
89+
}
90+
return score + calculateIndexScore<T, S, F>(validator, rootSchema, value as S, formValue);
8791
}
8892
if (value.type === guessType(formValue)) {
8993
// If the types match, then we bump the score by one

packages/utils/src/schema/getDefaultFormState.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -586,12 +586,14 @@ export default function getDefaultFormState<
586586
// No form data? Use schema defaults.
587587
return defaults;
588588
}
589-
const { mergeExtraDefaults } = experimental_defaultFormStateBehavior?.arrayMinItems || {};
589+
const { mergeDefaultsIntoFormData, arrayMinItems = {} } = experimental_defaultFormStateBehavior || {};
590+
const { mergeExtraDefaults } = arrayMinItems;
591+
const defaultSupercedesUndefined = mergeDefaultsIntoFormData === 'useDefaultIfFormDataUndefined';
590592
if (isObject(formData)) {
591-
return mergeDefaultsWithFormData<T>(defaults as T, formData, mergeExtraDefaults);
593+
return mergeDefaultsWithFormData<T>(defaults as T, formData, mergeExtraDefaults, defaultSupercedesUndefined);
592594
}
593595
if (Array.isArray(formData)) {
594-
return mergeDefaultsWithFormData<T[]>(defaults as T[], formData, mergeExtraDefaults);
596+
return mergeDefaultsWithFormData<T[]>(defaults as T[], formData, mergeExtraDefaults, defaultSupercedesUndefined);
595597
}
596598
return formData;
597599
}

packages/utils/src/types.ts

+11-1
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

+16
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ describe('mergeDefaultsWithFormData()', () => {
1717
expect(mergeDefaultsWithFormData(undefined, [2])).toEqual([2]);
1818
});
1919

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({});
26+
});
27+
28+
it('should return default when formData is null and defaultSupercedesUndefined true', () => {
29+
expect(mergeDefaultsWithFormData({}, null, undefined, true)).toBeNull();
30+
});
31+
32+
it('should return undefined when formData is undefined', () => {
33+
expect(mergeDefaultsWithFormData(undefined, undefined)).toBeUndefined();
34+
});
35+
2036
it('should merge two one-level deep objects', () => {
2137
expect(mergeDefaultsWithFormData({ a: 1 }, { b: 2 })).toEqual({
2238
a: 1,

packages/utils/test/schema/getClosestMatchingOptionTest.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export default function getClosestMatchingOptionTest(testValidator: TestValidato
4949
expect(calculateIndexScore(testValidator, oneOfSchema, firstOption, ONE_OF_SCHEMA_DATA)).toEqual(1);
5050
});
5151
it('returns 8 for second option in oneOf schema', () => {
52-
expect(calculateIndexScore(testValidator, oneOfSchema, secondOption, ONE_OF_SCHEMA_DATA)).toEqual(8);
52+
expect(calculateIndexScore(testValidator, oneOfSchema, secondOption, ONE_OF_SCHEMA_DATA)).toEqual(9);
5353
});
5454
it('returns 1 for a schema that has a type matching the formData type', () => {
5555
expect(calculateIndexScore(testValidator, oneOfSchema, { type: 'boolean' }, true)).toEqual(1);

packages/utils/test/schema/getDefaultFormStateTest.ts

+33
Original file line numberDiff line numberDiff line change
@@ -3743,6 +3743,39 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType
37433743
expect(getDefaultFormState(testValidator, schema, formData)).toEqual(result);
37443744
});
37453745
});
3746+
describe('object with defaults and undefined in formData, testing mergeDefaultsIntoFormData', () => {
3747+
let schema: RJSFSchema;
3748+
let defaultedFormData: any;
3749+
beforeAll(() => {
3750+
schema = {
3751+
type: 'object',
3752+
properties: {
3753+
field: {
3754+
type: 'string',
3755+
default: 'foo',
3756+
},
3757+
},
3758+
required: ['field'],
3759+
};
3760+
defaultedFormData = { field: 'foo' };
3761+
});
3762+
it('returns field value of default when formData is empty', () => {
3763+
const formData = {};
3764+
expect(getDefaultFormState(testValidator, schema, formData)).toEqual(defaultedFormData);
3765+
});
3766+
it('returns field value of undefined when formData has undefined for field', () => {
3767+
const formData = { field: undefined };
3768+
expect(getDefaultFormState(testValidator, schema, formData)).toEqual(formData);
3769+
});
3770+
it('returns field value of default when formData has undefined for field and `useDefaultIfFormDataUndefined`', () => {
3771+
const formData = { field: undefined };
3772+
expect(
3773+
getDefaultFormState(testValidator, schema, formData, undefined, undefined, {
3774+
mergeDefaultsIntoFormData: 'useDefaultIfFormDataUndefined',
3775+
})
3776+
).toEqual(defaultedFormData);
3777+
});
3778+
});
37463779
it('should return undefined defaults for a required array property with minItems', () => {
37473780
const schema: RJSFSchema = {
37483781
type: 'object',

0 commit comments

Comments
 (0)