Skip to content

Commit 53b3c57

Browse files
fix: Added support for anyOf/oneOf in uiSchema (#4055)
Fixes #4039 by updating `MultiSchemaField` to properly support `anyOf`/`oneOf` arrays in the `uiSchema` - In `@rjsf/utils`: Improved documentation and typescript ignores in tests related to `base64` from previous PR - In `@rjsf/core`: Updated `MultiSchemaField` to support `anyOf`/`oneOf` arrays in the `uiSchema` - Updated the tests to verify the new feature - In `docs`: Added documentation to the `uiSchema.md` file describing how to use the new feature - Updated the `CHANGELOG.md` accordingly
1 parent f31bef1 commit 53b3c57

File tree

7 files changed

+785
-219
lines changed

7 files changed

+785
-219
lines changed

CHANGELOG.md

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

1919
# 5.16.2
2020

21+
## @rjsf/core
22+
23+
- Added support for `anyOf`/`oneOf` in `uiSchema`s in the `MultiSchemaField`, fixing [#4039](https://github.com/rjsf-team/react-jsonschema-form/issues/4039)
24+
2125
## @rjsf/utils
2226

2327
- [4024](https://github.com/rjsf-team/react-jsonschema-form/issues/4024) Added `base64` to support `encoding`
@@ -27,6 +31,7 @@ should change the heading of the (upcoming) version to include a major version b
2731

2832
- [4024](https://github.com/rjsf-team/react-jsonschema-form/issues/4024) Updated the base64 references from (`atob`
2933
and `btoa`) to invoke the functions from the new `base64` object in `@rjsf/utils`.
34+
- Updated the `uiSchema.md` documentation to describe how to use the new `anyOf`/`oneOf` support
3035

3136
# 5.16.1
3237

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

+34-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import get from 'lodash/get';
33
import isEmpty from 'lodash/isEmpty';
44
import omit from 'lodash/omit';
55
import {
6+
ANY_OF_KEY,
67
deepEquals,
78
ERRORS_KEY,
89
FieldProps,
@@ -11,9 +12,11 @@ import {
1112
getUiOptions,
1213
getWidget,
1314
mergeSchemas,
15+
ONE_OF_KEY,
1416
RJSFSchema,
1517
StrictRJSFSchema,
1618
TranslatableString,
19+
UiSchema,
1720
} from '@rjsf/utils';
1821

1922
/** Type used for the state of the `AnyOfField` component */
@@ -167,7 +170,7 @@ class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
167170
const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
168171

169172
const option = selectedOption >= 0 ? retrievedOptions[selectedOption] || null : null;
170-
let optionSchema: S;
173+
let optionSchema: S | undefined | null;
171174

172175
if (option) {
173176
// merge top level required field
@@ -176,14 +179,39 @@ class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
176179
optionSchema = required ? (mergeSchemas({ required }, option) as S) : option;
177180
}
178181

182+
// First we will check to see if there is an anyOf/oneOf override for the UI schema
183+
let optionsUiSchema: UiSchema<T, S, F>[] = [];
184+
if (ONE_OF_KEY in schema && uiSchema && ONE_OF_KEY in uiSchema) {
185+
if (Array.isArray(uiSchema[ONE_OF_KEY])) {
186+
optionsUiSchema = uiSchema[ONE_OF_KEY];
187+
} else {
188+
console.warn(`uiSchema.oneOf is not an array for "${title || name}"`);
189+
}
190+
} else if (ANY_OF_KEY in schema && uiSchema && ANY_OF_KEY in uiSchema) {
191+
if (Array.isArray(uiSchema[ANY_OF_KEY])) {
192+
optionsUiSchema = uiSchema[ANY_OF_KEY];
193+
} else {
194+
console.warn(`uiSchema.anyOf is not an array for "${title || name}"`);
195+
}
196+
}
197+
// Then we pick the one that matches the selected option index, if one exists otherwise default to the main uiSchema
198+
let optionUiSchema = uiSchema;
199+
if (selectedOption >= 0 && optionsUiSchema.length > selectedOption) {
200+
optionUiSchema = optionsUiSchema[selectedOption];
201+
}
202+
179203
const translateEnum: TranslatableString = title
180204
? TranslatableString.TitleOptionPrefix
181205
: TranslatableString.OptionPrefix;
182206
const translateParams = title ? [title] : [];
183-
const enumOptions = retrievedOptions.map((opt: { title?: string }, index: number) => ({
184-
label: opt.title || translateString(translateEnum, translateParams.concat(String(index + 1))),
185-
value: index,
186-
}));
207+
const enumOptions = retrievedOptions.map((opt: { title?: string }, index: number) => {
208+
// Also see if there is an override title in the uiSchema for each option, otherwise use the title from the option
209+
const { title: uiTitle = opt.title } = getUiOptions<T, S, F>(optionsUiSchema[index]);
210+
return {
211+
label: uiTitle || translateString(translateEnum, translateParams.concat(String(index + 1))),
212+
value: index,
213+
};
214+
});
187215

188216
return (
189217
<div className='panel panel-default panel-body'>
@@ -210,7 +238,7 @@ class AnyOfField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends For
210238
hideLabel={!displayLabel}
211239
/>
212240
</div>
213-
{option !== null && <_SchemaField {...this.props} schema={optionSchema!} />}
241+
{optionSchema && <_SchemaField {...this.props} schema={optionSchema} uiSchema={optionUiSchema} />}
214242
</div>
215243
);
216244
}

packages/core/test/anyOf.test.jsx

+144-33
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ describe('anyOf', () => {
5757
schema,
5858
});
5959

60-
console.log(node.innerHTML);
61-
6260
expect(node.querySelectorAll('select')).to.have.length.of(1);
6361
expect(node.querySelector('select').id).eql('root__anyof_select');
6462
expect(node.querySelectorAll('span.required')).to.have.length.of(1);
@@ -92,8 +90,6 @@ describe('anyOf', () => {
9290
schema,
9391
});
9492

95-
console.log(node.innerHTML);
96-
9793
expect(node.querySelectorAll('select')).to.have.length.of(1);
9894
expect(node.querySelector('select').id).eql('root__anyof_select');
9995
expect(node.querySelectorAll('span.required')).to.have.length.of(2);
@@ -1139,6 +1135,61 @@ describe('anyOf', () => {
11391135
Simulate.change(strInputs[1], { target: { value: 'bar' } });
11401136
expect(strInputs[1].value).eql('bar');
11411137
});
1138+
it('should correctly render mixed types for anyOf inside array items', () => {
1139+
const schema = {
1140+
type: 'object',
1141+
properties: {
1142+
items: {
1143+
type: 'array',
1144+
items: {
1145+
anyOf: [
1146+
{
1147+
type: 'string',
1148+
},
1149+
{
1150+
type: 'object',
1151+
properties: {
1152+
foo: {
1153+
type: 'integer',
1154+
},
1155+
bar: {
1156+
type: 'string',
1157+
},
1158+
},
1159+
},
1160+
],
1161+
},
1162+
},
1163+
},
1164+
};
1165+
1166+
const { node } = createFormComponent({
1167+
schema,
1168+
});
1169+
1170+
expect(node.querySelector('.array-item-add button')).not.eql(null);
1171+
1172+
Simulate.click(node.querySelector('.array-item-add button'));
1173+
1174+
const $select = node.querySelector('select');
1175+
expect($select).not.eql(null);
1176+
Simulate.change($select, {
1177+
target: { value: $select.options[1].value },
1178+
});
1179+
1180+
expect(node.querySelectorAll('input#root_items_0_foo')).to.have.length.of(1);
1181+
expect(node.querySelectorAll('input#root_items_0_bar')).to.have.length.of(1);
1182+
});
1183+
});
1184+
1185+
describe('definitions', () => {
1186+
beforeEach(() => {
1187+
sandbox = createSandbox();
1188+
sandbox.stub(console, 'warn');
1189+
});
1190+
afterEach(() => {
1191+
sandbox.restore();
1192+
});
11421193

11431194
it('should correctly set the label of the options', () => {
11441195
const schema = {
@@ -1262,50 +1313,110 @@ describe('anyOf', () => {
12621313
expect($select.options[2].text).eql('Baz');
12631314
});
12641315

1265-
it('should correctly render mixed types for anyOf inside array items', () => {
1316+
it('should correctly set the label of the options, with uiSchema-based titles, for each anyOf option', () => {
12661317
const schema = {
12671318
type: 'object',
1268-
properties: {
1269-
items: {
1270-
type: 'array',
1271-
items: {
1272-
anyOf: [
1273-
{
1274-
type: 'string',
1275-
},
1276-
{
1277-
type: 'object',
1278-
properties: {
1279-
foo: {
1280-
type: 'integer',
1281-
},
1282-
bar: {
1283-
type: 'string',
1284-
},
1285-
},
1286-
},
1287-
],
1319+
anyOf: [
1320+
{
1321+
title: 'Foo',
1322+
properties: {
1323+
foo: { type: 'string' },
1324+
},
1325+
},
1326+
{
1327+
properties: {
1328+
bar: { type: 'string' },
1329+
},
1330+
},
1331+
{
1332+
$ref: '#/definitions/baz',
1333+
},
1334+
],
1335+
definitions: {
1336+
baz: {
1337+
title: 'Baz',
1338+
properties: {
1339+
baz: { type: 'string' },
12881340
},
12891341
},
12901342
},
12911343
};
12921344

12931345
const { node } = createFormComponent({
12941346
schema,
1347+
uiSchema: {
1348+
anyOf: [
1349+
{
1350+
'ui:title': 'Custom foo',
1351+
},
1352+
{
1353+
'ui:title': 'Custom bar',
1354+
},
1355+
{
1356+
'ui:title': 'Custom baz',
1357+
},
1358+
],
1359+
},
12951360
});
1361+
const $select = node.querySelector('select');
12961362

1297-
expect(node.querySelector('.array-item-add button')).not.eql(null);
1363+
expect($select.options[0].text).eql('Custom foo');
1364+
expect($select.options[1].text).eql('Custom bar');
1365+
expect($select.options[2].text).eql('Custom baz');
12981366

1299-
Simulate.click(node.querySelector('.array-item-add button'));
1367+
// Also verify the uiSchema was passed down to the underlying widget by confirming the lable (in the legend)
1368+
// matches the selected option's title
1369+
expect($select.value).eql('0');
1370+
const inputLabel = node.querySelector('legend#root__title');
1371+
expect(inputLabel.innerHTML).eql($select.options[$select.value].text);
1372+
});
13001373

1301-
const $select = node.querySelector('select');
1302-
expect($select).not.eql(null);
1303-
Simulate.change($select, {
1304-
target: { value: $select.options[1].value },
1374+
it('should warn when the anyOf in the uiSchema is not an array, and pass the base uiSchema down', () => {
1375+
const schema = {
1376+
type: 'object',
1377+
anyOf: [
1378+
{
1379+
title: 'Foo',
1380+
properties: {
1381+
foo: { type: 'string' },
1382+
},
1383+
},
1384+
{
1385+
properties: {
1386+
bar: { type: 'string' },
1387+
},
1388+
},
1389+
{
1390+
$ref: '#/definitions/baz',
1391+
},
1392+
],
1393+
definitions: {
1394+
baz: {
1395+
title: 'Baz',
1396+
properties: {
1397+
baz: { type: 'string' },
1398+
},
1399+
},
1400+
},
1401+
};
1402+
1403+
const { node } = createFormComponent({
1404+
schema,
1405+
uiSchema: {
1406+
'ui:title': 'My Title',
1407+
anyOf: { 'ui:title': 'UiSchema title' },
1408+
},
13051409
});
13061410

1307-
expect(node.querySelectorAll('input#root_items_0_foo')).to.have.length.of(1);
1308-
expect(node.querySelectorAll('input#root_items_0_bar')).to.have.length.of(1);
1411+
expect(console.warn.calledWithMatch(/uiSchema.anyOf is not an array for "My Title"/)).to.be.true;
1412+
1413+
const $select = node.querySelector('select');
1414+
1415+
// Also verify the base uiSchema was passed down to the underlying widget by confirming the label (in the legend)
1416+
// matches the selected option's title
1417+
expect($select.value).eql('0');
1418+
const inputLabel = node.querySelector('legend#root__title');
1419+
expect(inputLabel.innerHTML).eql('My Title');
13091420
});
13101421

13111422
it('should correctly infer the selected option based on value', () => {

0 commit comments

Comments
 (0)