diff --git a/src/components/fields/MultiSchemaField.js b/src/components/fields/MultiSchemaField.js index e11b9434a9..593590e6b8 100644 --- a/src/components/fields/MultiSchemaField.js +++ b/src/components/fields/MultiSchemaField.js @@ -30,7 +30,49 @@ class AnyOfField extends Component { getMatchingOption(formData, options) { for (let i = 0; i < options.length; i++) { - if (isValid(options[i], formData)) { + const option = options[i]; + + // If the schema describes an object then we need to add slightly more + // strict matching to the schema, because unless the schema uses the + // "requires" keyword, an object will match the schema as long as it + // doesn't have matching keys with a conflicting type. To do this we use an + // "anyOf" with an array of requires. This augmentation expresses that the + // schema should match if any of the keys in the schema are present on the + // object and pass validation. + if (option.properties) { + // Create an "anyOf" schema that requires at least one of the keys in the + // "properties" object + const requiresAnyOf = { + anyOf: Object.keys(option.properties).map(key => ({ + required: [key], + })), + }; + + let augmentedSchema; + + // If the "anyOf" keyword already exists, wrap the augmentation in an "allOf" + if (option.anyOf) { + // Create a shallow clone of the option + const { ...shallowClone } = option; + + if (!shallowClone.allOf) { + shallowClone.allOf = []; + } else { + // If "allOf" already exists, shallow clone the array + shallowClone.allOf = shallowClone.allOf.slice(); + } + + shallowClone.allOf.push(requiresAnyOf); + + augmentedSchema = shallowClone; + } else { + augmentedSchema = Object.assign({}, option, requiresAnyOf); + } + + if (isValid(augmentedSchema, formData)) { + return i; + } + } else if (isValid(options[i], formData)) { return i; } } diff --git a/test/anyOf_test.js b/test/anyOf_test.js index cc845af6f7..51566c3bf1 100644 --- a/test/anyOf_test.js +++ b/test/anyOf_test.js @@ -300,6 +300,235 @@ describe("anyOf", () => { expect(node.querySelector("select").value).eql("1"); }); + it("should not change the selected option when entering values", () => { + const schema = { + type: "object", + anyOf: [ + { + title: "First method of identification", + properties: { + firstName: { + type: "string", + }, + lastName: { + type: "string", + }, + }, + }, + { + title: "Second method of identification", + properties: { + idCode: { + type: "string", + }, + }, + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + const $select = node.querySelector("select"); + + expect($select.value).eql("0"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + Simulate.change(node.querySelector("input#root_idCode"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect($select.value).eql("1"); + }); + + it("should not change the selected option when entering values and the subschema uses `anyOf`", () => { + const schema = { + type: "object", + anyOf: [ + { + title: "First method of identification", + properties: { + firstName: { + type: "string", + }, + lastName: { + type: "string", + }, + }, + }, + { + title: "Second method of identification", + properties: { + idCode: { + type: "string", + }, + }, + anyOf: [ + { + properties: { + foo: { + type: "string", + }, + }, + }, + { + properties: { + bar: { + type: "string", + }, + }, + }, + ], + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + const $select = node.querySelector("select"); + + expect($select.value).eql("0"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + Simulate.change(node.querySelector("input#root_idCode"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect($select.value).eql("1"); + }); + + it("should not change the selected option when entering values and the subschema uses `allOf`", () => { + const schema = { + type: "object", + anyOf: [ + { + title: "First method of identification", + properties: { + firstName: { + type: "string", + }, + lastName: { + type: "string", + }, + }, + }, + { + title: "Second method of identification", + properties: { + idCode: { + type: "string", + }, + }, + allOf: [ + { + properties: { + foo: { + type: "string", + }, + }, + }, + { + properties: { + bar: { + type: "string", + }, + }, + }, + ], + }, + ], + }; + + const { node } = createFormComponent({ + schema, + }); + + const $select = node.querySelector("select"); + + expect($select.value).eql("0"); + + Simulate.change($select, { + target: { value: $select.options[1].value }, + }); + + expect($select.value).eql("1"); + + Simulate.change(node.querySelector("input#root_idCode"), { + target: { value: "Lorem ipsum dolor sit amet" }, + }); + + expect($select.value).eql("1"); + }); + + it("should not mutate a schema that contains nested anyOf and allOf", () => { + const schema = { + type: "object", + anyOf: [ + { + properties: { + foo: { type: "string" }, + }, + allOf: [ + { + properties: { + baz: { type: "string" }, + }, + }, + ], + anyOf: [ + { + properties: { + buzz: { type: "string" }, + }, + }, + ], + }, + ], + }; + + createFormComponent({ + schema, + }); + + expect(schema).to.eql({ + type: "object", + anyOf: [ + { + properties: { + foo: { type: "string" }, + }, + allOf: [ + { + properties: { + baz: { type: "string" }, + }, + }, + ], + anyOf: [ + { + properties: { + buzz: { type: "string" }, + }, + }, + ], + }, + ], + }); + }); + describe("Arrays", () => { it("should correctly render form inputs for anyOf inside array items", () => { const schema = {