Skip to content

Commit 2cb943b

Browse files
fix: fixed several issue in oneOf/anyOf functions
Fixes rjsf-team#2944, rjsf-team#3236, rjsf-team#2978 and possibly others - In `@rjsf/utils`, added new `getClosestMatchingOption()`, `getFirstMatchingOption()` and `sanitizeDataForNewSchema()` schema-based utility functions - Deprecated `getMatchingOption()` and updated all calls to it in other utility functions to use `getFirstMatchingOption()` - Added 100% unit tests for all new functions, renaming the old `getMatchingOptionsTest.ts` file to `getFirstMatchingOptionsTest.ts` - Updated `createSchemaUtils()` and it's associated type to add the three new functions - In `@rjsf/validator-ajv6` and `@rjsf/validator-ajv8`, updated the `schema.tests.ts` to add the new tests for the new schema-based utility functions - In `@rjsf/core`, updated the `MultiSchemaField` to use the new `getClosestMatchingOption()` and `sanitizeDataForNewSchema()` utility functions - Also updated the render to properly pass props to the widget and the schema field - In `@rjsf/playground`, updated `onFormDataEdited()` to only change the formData in the state if the `JSON.stringify()` of the old and new values are different - Also updated the `npm start` command to add the `--force` option to avoid issues where changes made to other packages weren't getting picked up due to `vite` caching - Updated the `utility-functions.md` file to document the new schema-based functions and to fix up incorrect strike-through caused by the unescaped `<S>` generic - Updated the `5.x upgrade guide.md` file to document the new utility functions and the deprecation of `getMatchingOption()`
1 parent a3cf692 commit 2cb943b

25 files changed

+1743
-109
lines changed

CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ it according to semantic versioning. For example, if your PR adds a breaking cha
1515
should change the heading of the (upcoming) version to include a major version bump.
1616
1717
-->
18+
# 5.0.0-beta-18
19+
20+
# @rjsf/core
21+
- Updated `MultiSchemaField` to utilize the new `getClosestMatchingOption()` and `sanitizeDataForNewSchema()` functions, fixing [#2944](https://github.com/rjsf-team/react-jsonschema-form/issues/2944), [#3236](https://github.com/rjsf-team/react-jsonschema-form/issues/3236), [#2978](https://github.com/rjsf-team/react-jsonschema-form/issues/2978), and probably others
22+
23+
# @rjsf/utils
24+
- Added new `getClosestMatchingOption()`, `getFirstMatchingOption()` and `sanitizeDataForNewSchema()` schema-based utility functions
25+
- Deprecated `getMatchingOption()` and updated all calls to it in other utility functions to use `getFirstMatchingOption()`
26+
27+
## Dev / docs / playground
28+
- Updated the playground to `onFormDataEdited()` to only change the formData in the state if the `JSON.stringify()` of the old and new values are different, partially fixing [#3236](https://github.com/rjsf-team/react-jsonschema-form/issues/3236)
29+
- Updated the playground `npm start` command to always use the `--force` option to avoid issues where changes made to other packages weren't getting picked up due to `vite` caching
30+
- Updated the documentation for `utility-functions` and the `5.x upgrade guide` to add the new utility functions and to document the deprecation of `getMatchingOption()`
31+
1832
# 5.0.0-beta-17
1933

2034
## @rjsf/antd

docs/5.x upgrade guide.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Unfortunately, there is required work pending to properly support React 18, so u
2727
There are four new packages added in RJSF version 5:
2828

2929
- `@rjsf/utils`: All of the [utility functions](https://react-jsonschema-form.readthedocs.io/en/stable/api-reference/utiltity-functions) previously imported from `@rjsf/core/utils` as well as the Typescript types for RJSF version 5.
30-
- The following new utility functions were added: `createSchemaUtils()`, `getInputProps()`, `mergeValidationData()` and `processSelectValue()`
30+
- The following new utility functions were added: `ariaDescribedByIds()`, `createSchemaUtils()`, `descriptionId()`, `enumOptionsDeselectValue()`, `enumOptionsSelectValue()`, `errorId()`, `examplesId()`, `getClosestMatchingOption()`, `getFirstMatchingOption()`, `getInputProps()`, `helpId()`, `mergeValidationData()`, `optionId()`, `processSelectValue()`, `sanitizeDataForNewSchema()` and `titleId()`
3131
- `@rjsf/validator-ajv6`: The [ajv](https://github.com/ajv-validator/ajv)-v6-based validator refactored out of `@rjsf/[email protected]`, that implements the `ValidatorType` interface defined in `@rjsf/utils`.
3232
- `@rjsf/validator-ajv8`: The [ajv](https://github.com/ajv-validator/ajv)-v8-based validator that is an upgrade of the `@rjsf/validator-ajv6`, that implements the `ValidatorType` interface defined in `@rjsf/utils`. See the ajv 6 to 8 [migration guide](https://ajv.js.org/v6-to-v8-migration.html) for more information.
3333
- `@rjsf/mui`: Previously `@rjsf/material-ui/v5`, now provided as its own theme.
@@ -231,6 +231,7 @@ render((
231231
In version 5, all the utility functions that were previously accessed via `import { utils } from '@rjsf/core';` are now available via `import utils from '@rjsf/utils';`.
232232
Because of the decoupling of validation from `@rjsf/core` there is a breaking change for all the [validator-based utility functions](https://react-jsonschema-form.readthedocs.io/en/stable/api-reference/utiltity-functions#validator-based-utility-functions), since they now require an additional `ValidatorType` parameter.
233233
More over, one previously exported function `resolveSchema()` is no longer exposed in the `@rjsf/utils`, so use `retrieveSchema()` instead.
234+
Finally, one previously exported function `getMatchingOption()` deprecated in favor of `getFirstMatchingOption()`.
234235

235236
If you have built custom fields or widgets that utilized any of these breaking-change functions, don't worry, there is a quick and easy solution for you.
236237
The `registry` has a breaking-change which removes the previously deprecated `definitions` property while adding the new `schemaUtils` property.
@@ -259,8 +260,10 @@ import { RJSFSchema, WidgetProps, getUiOptions } from '@rjsf/utils';
259260
function YourWidget(props: WidgetProps) {
260261
const { registry, uiSchema } = props;
261262
const { schemaUtils } = registry;
263+
// const matchingOption = getMatchingOption({}, options, rootSchema); <- version 4
262264
// const isMultiSelect = isMultiSelect(schema, rootSchema); <- version 4
263265
// const newSchema = resolveSchema(schema, formData, rootSchema); <- version 4
266+
const matchingOption = schemaUtils.getFirstMatchingOption({}, options);
264267
const isMultiSelect = schemaUtils.isMultiSelect(schema);
265268
const newSchema: RJSFSchema = schemaUtils.retrieveSchema(schema, formData);
266269
const options = getUiOptions(uiSchema);
@@ -399,6 +402,9 @@ From v5, the child fields will correctly use the parent id when generating its o
399402

400403
#### Deprecations added in v5
401404

405+
##### getMatchingOption()
406+
The utility function `getMatchingOption()` was deprecated in favor of the more aptly named `getFirstMatchingOption()` which has the exact same implementation.
407+
402408
##### Non-standard `enumNames` property
403409

404410
`enumNames` is a non-standard JSON Schema field that was deprecated in version 5.

docs/api-reference/utility-functions.md

+57-8
Original file line numberDiff line numberDiff line change
@@ -99,22 +99,22 @@ Return a consistent `id` for the field description element.
9999
Removes the `value` from the currently `selected` list of values.
100100

101101
#### Parameters
102-
- value: EnumOptionsType<S>["value"] - The value that should be selected
103-
- selected: EnumOptionsType<S>["value"][] - The current list of selected values
102+
- value: EnumOptionsType\<S>["value"] - The value that should be selected
103+
- selected: EnumOptionsType\<S>["value"][] - The current list of selected values
104104

105105
#### Returns
106-
- EnumOptionsType<S>["value"][]: The updated `selected` list with the `value` removed from it
106+
- EnumOptionsType\<S>["value"][]: The updated `selected` list with the `value` removed from it
107107

108108
### enumOptionsSelectValue\<S extends StrictRJSFSchema = RJSFSchema>()
109109
Add the `value` to the list of `selected` values in the proper order as defined by `allEnumOptions`.
110110

111111
#### Parameters
112-
- value: EnumOptionsType<S>["value"] - The value that should be selected
113-
- selected: EnumOptionsType<S>["value"][] - The current list of selected values
114-
- allEnumOptions: EnumOptionsType<S>[] - The list of all the known enumOptions
112+
- value: EnumOptionsType\<S>["value"] - The value that should be selected
113+
- selected: EnumOptionsType\<S>["value"][] - The current list of selected values
114+
- allEnumOptions: EnumOptionsType\<S>[] - The list of all the known enumOptions
115115

116116
#### Returns
117-
- EnumOptionsType<S>["value"][]: The updated list of selected enum values with `value` added to it in the proper location
117+
- EnumOptionsType\<S>["value"][]: The updated list of selected enum values with `value` added to it in the proper location
118118

119119
### errorId<T = any>()
120120
Return a consistent `id` for the field error element.
@@ -344,7 +344,7 @@ Return a consistent `id` for the `option`s of a `Radio` or `Checkboxes` widget
344344

345345
#### Parameters
346346
- id: string - The id of the parent component for the option
347-
- option: EnumOptionsType<S> - The option for which the id is desired
347+
- option: EnumOptionsType\<S> - The option for which the id is desired
348348

349349
#### Returns
350350
- string: An id for the option based on the parent `id`
@@ -517,6 +517,39 @@ Determines whether the combination of `schema` and `uiSchema` properties indicat
517517
#### Returns
518518
- boolean: True if the label should be displayed or false if it should not
519519

520+
### getClosestMatchingOption<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
521+
Determines which of the given `options` provided most closely matches the `formData`.
522+
The `@rjsf` utility function
523+
* `getFirstMatchingOption()` will match two schemas that differ only by the readOnly, default value of a field based on
524+
* the `formData` and returns 0 when there is no match. Rather than passing in all of the `options` at once to this
525+
* utility, instead an array of valid option indexes is created by iterating over the list of options, call
526+
* `getFirstMatchingOptions` with a list of one junk option and one good option, seeing if the good option is considered
527+
* matched.
528+
*
529+
* Once the list of valid indexes is created, if there is only one valid index, just return it. Otherwise, if there are
530+
* no valid indexes, then fill the valid indexes array with the indexes of all the options. Next, the index of the
531+
* option with the highest score is determined by iterating over the list of valid options, calling the
532+
* `calculateIndexScore()` on each, comparing it against the current best score, and returning the index of the one that
533+
* eventually has the best score.
534+
*
535+
* @param formData - The form data associated with the schema
536+
* @param options - The list of options that can be selected from
537+
* @param [selectedOption=-1] - The index of the currently selected option, defaulted to -1 if not specified
538+
* @returns - The index of the option that is the closest match to the `formData` or the `selectedOption` if no match
539+
*/
540+
### getFirstMatchingOption<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
541+
Given the `formData` and list of `options`, attempts to find the index of the first option that matches the data.
542+
Always returns the first option if there is nothing that matches.
543+
544+
#### Parameters
545+
- validator: ValidatorType<T, S, F> - An implementation of the `ValidatorType` interface that will be used when necessary
546+
- formData: T | undefined - The current formData, if any, used to figure out a match
547+
- options: S[] - The list of options to find a matching options from
548+
- rootSchema: S - The root schema, used to primarily to look up `$ref`s
549+
550+
#### Returns
551+
- number: The index of the first matched option or 0 if none is available
552+
520553
### getMatchingOption<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
521554
Given the `formData` and list of `options`, attempts to find the index of the option that best matches the data.
522555

@@ -589,6 +622,22 @@ potentially recursive resolution.
589622
#### Returns
590623
- RJSFSchema: The schema having its conditions, additional properties, references and dependencies resolved
591624

625+
### sanitizeDataForNewSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
626+
Sanitize the `data` associated with the `oldSchema` so it is considered appropriate for the `newSchema`.
627+
If the new schema does not contain any properties, then `undefined` is returned to clear all the form data.
628+
Due to the nature of schemas, this sanitization happens recursively for nested objects of data.
629+
Also, any properties in the old schema that are non-existent in the new schema are set to `undefined`.
630+
631+
#### Parameters
632+
- validator: ValidatorType<T, S, F> - An implementation of the `ValidatorType` interface that will be used when necessary
633+
- rootSchema: S - The root JSON schema of the entire form
634+
- [newSchema]: S - The new schema for which the data is being sanitized
635+
- [oldSchema]: S - The old schema from which the data originated
636+
- [data={}]: any - The form data associated with the schema, defaulting to an empty object when undefined
637+
638+
#### Returns
639+
- T: The new form data, with all the fields uniquely associated with the old schema set to `undefined`. Will return `undefined` if the new schema is not an object containing properties.
640+
592641
### toIdSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>()
593642
Generates an `IdSchema` object for the `schema`, recursively
594643

packages/core/src/components/fields/MultiSchemaField.tsx

+55-74
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import React, { Component } from "react";
2+
import get from "lodash/get";
3+
import isEmpty from "lodash/isEmpty";
4+
import omit from "lodash/omit";
25
import {
36
getUiOptions,
47
getWidget,
5-
guessType,
68
deepEquals,
79
FieldProps,
810
FormContextType,
911
RJSFSchema,
1012
StrictRJSFSchema,
13+
ERRORS_KEY,
1114
} from "@rjsf/utils";
12-
import has from "lodash/has";
13-
import unset from "lodash/unset";
1415

1516
/** Type used for the state of the `AnyOfField` component */
1617
type AnyOfFieldState = {
@@ -83,8 +84,12 @@ class AnyOfField<
8384
getMatchingOption(selectedOption: number, formData: T, options: S[]) {
8485
const { schemaUtils } = this.props.registry;
8586

86-
const option = schemaUtils.getMatchingOption(formData, options);
87-
if (option !== 0) {
87+
const option = schemaUtils.getClosestMatchingOption(
88+
formData,
89+
options,
90+
selectedOption
91+
);
92+
if (option > 0) {
8893
return option;
8994
}
9095
// If the form data matches none of the options, use the currently selected
@@ -98,53 +103,40 @@ class AnyOfField<
98103
*
99104
* @param option -
100105
*/
101-
onOptionChange = (option: any) => {
102-
const selectedOption = parseInt(option, 10);
106+
onOptionChange = (option?: string) => {
107+
const { selectedOption } = this.state;
103108
const { formData, onChange, options, registry } = this.props;
104109
const { schemaUtils } = registry;
105-
const newOption = schemaUtils.retrieveSchema(
106-
options[selectedOption],
110+
const intOption = option !== undefined ? parseInt(option, 10) : -1;
111+
if (intOption === selectedOption) {
112+
return;
113+
}
114+
const newOption =
115+
intOption >= 0
116+
? schemaUtils.retrieveSchema(options[intOption], formData)
117+
: undefined;
118+
const oldOption =
119+
selectedOption >= 0
120+
? schemaUtils.retrieveSchema(options[selectedOption], formData)
121+
: undefined;
122+
123+
let newFormData = schemaUtils.sanitizeDataForNewSchema(
124+
newOption,
125+
oldOption,
107126
formData
108127
);
109-
110-
// If the new option is of type object and the current data is an object,
111-
// discard properties added using the old option.
112-
let newFormData: T | undefined = undefined;
113-
if (
114-
guessType(formData) === "object" &&
115-
(newOption.type === "object" || newOption.properties)
116-
) {
117-
newFormData = Object.assign({}, formData);
118-
119-
const optionsToDiscard = options.slice();
120-
optionsToDiscard.splice(selectedOption, 1);
121-
122-
// Discard any data added using other options
123-
for (const option of optionsToDiscard) {
124-
if (option.properties) {
125-
for (const key in option.properties) {
126-
if (has(newFormData, key)) {
127-
unset(newFormData, key);
128-
}
129-
}
130-
}
131-
}
132-
}
133-
// Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren"
134-
// so that only the root objects themselves are created without adding undefined children properties
135-
onChange(
136-
schemaUtils.getDefaultFormState(
137-
options[selectedOption],
128+
if (newFormData && newOption) {
129+
// Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren"
130+
// so that only the root objects themselves are created without adding undefined children properties
131+
newFormData = schemaUtils.getDefaultFormState(
132+
newOption,
138133
newFormData,
139134
"excludeObjectChildren"
140-
) as T,
141-
undefined,
142-
this.getFieldId()
143-
);
135+
) as T;
136+
}
137+
onChange(newFormData, undefined, this.getFieldId());
144138

145-
this.setState({
146-
selectedOption: parseInt(option, 10),
147-
});
139+
this.setState({ selectedOption: intOption });
148140
};
149141

150142
getFieldId() {
@@ -158,19 +150,11 @@ class AnyOfField<
158150
*/
159151
render() {
160152
const {
161-
name,
162153
baseType,
163154
disabled = false,
164-
readonly = false,
165-
hideError = false,
166155
errorSchema = {},
167-
formData,
168156
formContext,
169-
idPrefix,
170-
idSeparator,
171-
idSchema,
172157
onBlur,
173-
onChange,
174158
onFocus,
175159
options,
176160
registry,
@@ -180,8 +164,16 @@ class AnyOfField<
180164
const { widgets, fields } = registry;
181165
const { SchemaField: _SchemaField } = fields;
182166
const { selectedOption } = this.state;
183-
const { widget = "select", ...uiOptions } = getUiOptions<T, S, F>(uiSchema);
167+
const {
168+
widget = "select",
169+
placeholder,
170+
autofocus,
171+
autocomplete,
172+
...uiOptions
173+
} = getUiOptions<T, S, F>(uiSchema);
184174
const Widget = getWidget<T, S, F>({ type: "number" }, widget, widgets);
175+
const rawErrors = get(errorSchema, ERRORS_KEY, []);
176+
const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]);
185177

186178
const option = options[selectedOption] || null;
187179
let optionSchema;
@@ -208,33 +200,22 @@ class AnyOfField<
208200
onChange={this.onOptionChange}
209201
onBlur={onBlur}
210202
onFocus={onFocus}
203+
disabled={disabled || isEmpty(enumOptions)}
204+
multiple={false}
205+
rawErrors={rawErrors}
206+
errorSchema={fieldErrorSchema}
211207
value={selectedOption}
212-
options={{ enumOptions }}
208+
options={{ enumOptions, ...uiOptions }}
213209
registry={registry}
214210
formContext={formContext}
215-
{...uiOptions}
211+
placeholder={placeholder}
212+
autocomplete={autocomplete}
213+
autofocus={autofocus}
216214
label=""
217215
/>
218216
</div>
219217
{option !== null && (
220-
<_SchemaField
221-
name={name}
222-
schema={optionSchema}
223-
uiSchema={uiSchema}
224-
errorSchema={errorSchema}
225-
idSchema={idSchema}
226-
idPrefix={idPrefix}
227-
idSeparator={idSeparator}
228-
formData={formData}
229-
formContext={formContext}
230-
onChange={onChange}
231-
onBlur={onBlur}
232-
onFocus={onFocus}
233-
registry={registry}
234-
disabled={disabled}
235-
readonly={readonly}
236-
hideError={hideError}
237-
/>
218+
<_SchemaField {...this.props} schema={optionSchema} />
238219
)}
239220
</div>
240221
);

packages/core/test/anyOf_test.js

+27
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,33 @@ describe("anyOf", () => {
5656
expect(node.querySelector("select").id).eql("root__anyof_select");
5757
});
5858

59+
it("should render a root select element with default value", () => {
60+
const formData = { foo: "b" };
61+
const schema = {
62+
type: "object",
63+
anyOf: [
64+
{
65+
title: "foo1",
66+
properties: {
67+
foo: { type: "string", enum: ["a", "b"], default: "a" },
68+
},
69+
},
70+
{
71+
title: "foo2",
72+
properties: {
73+
foo: { type: "string", enum: ["a", "b"], default: "b" },
74+
},
75+
},
76+
],
77+
};
78+
79+
const { node } = createFormComponent({
80+
schema,
81+
formData,
82+
});
83+
expect(node.querySelector("select").value).eql("1");
84+
});
85+
5986
it("should assign a default value and set defaults on option change", () => {
6087
const { node, onChange } = createFormComponent({
6188
schema: {

0 commit comments

Comments
 (0)