diff --git a/README.md b/README.md
index b3350e3dc0..9cfc0356ce 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,12 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
- [The case of empty strings](#the-case-of-empty-strings)
- [Styling your forms](#styling-your-forms)
- [Schema definitions and references](#schema-definitions-and-references)
+ - [Property dependencies](#property-dependencies)
+ - [Unidirectional](#unidirectional)
+ - [Bidirectional](#bidirectional)
+ - [Schema dependencies](#schema-depdencies)
+ - [Conditional](#conditional)
+ - [Dynamic](#dynamic)
- [JSON Schema supporting status](#json-schema-supporting-status)
- [Tips and tricks](#tips-and-tricks)
- [Contributing](#contributing)
@@ -1497,6 +1503,166 @@ This library partially supports [inline schema definition dereferencing]( http:/
Note that it only supports local definition referencing, we do not plan on fetching foreign schemas over HTTP anytime soon. Basically, you can only reference a definition from the very schema object defining it.
+## Property dependencies
+
+This library supports conditionally making fields required based on the presence of other fields.
+
+### Unidirectional
+
+In the following example the `billing_address` field will be required if `credit_card` is defined.
+
+```json
+{
+ "type": "object",
+
+ "properties": {
+ "name": { "type": "string" },
+ "credit_card": { "type": "number" },
+ "billing_address": { "type": "string" }
+ },
+
+ "required": ["name"],
+
+ "dependencies": {
+ "credit_card": ["billing_address"]
+ }
+}
+```
+
+### Bidirectional
+
+In the following example the `billing_address` field will be required if `credit_card` is defined and the `credit_card`
+field will be required if `billing_address` is defined making them both required if either is defined.
+
+```json
+{
+ "type": "object",
+
+ "properties": {
+ "name": { "type": "string" },
+ "credit_card": { "type": "number" },
+ "billing_address": { "type": "string" }
+ },
+
+ "required": ["name"],
+
+ "dependencies": {
+ "credit_card": ["billing_address"],
+ "billing_address": ["credit_card"]
+ }
+}
+```
+
+*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#property-dependencies))*
+
+## Schema dependencies
+
+This library also supports modifying portions of a schema based on form data.
+
+### Conditional
+
+```json
+{
+ "type": "object",
+
+ "properties": {
+ "name": { "type": "string" },
+ "credit_card": { "type": "number" }
+ },
+
+ "required": ["name"],
+
+ "dependencies": {
+ "credit_card": {
+ "properties": {
+ "billing_address": { "type": "string" }
+ },
+ "required": ["billing_address"]
+ }
+ }
+}
+```
+
+In this example the `billing_address` field will be displayed in the form if `credit_card` is defined.
+
+*(Sample schemas courtesy of the [Space Telescope Science Institute](https://spacetelescope.github.io/understanding-json-schema/reference/object.html#schema-dependencies))*
+
+### Dynamic
+
+The JSON Schema standard says that the dependency is triggered if the property is present. However, sometimes it's useful to have more sophisticated rules guiding the application of the dependency. For example, maybe you have three possible values for a field, and each one should lead to adding a different question. For this, we support a very restricted use of the `oneOf` keyword.
+
+```json
+{
+ "title": "Person",
+ "type": "object",
+ "properties": {
+ "Do you have any pets?": {
+ "type": "string",
+ "enum": [
+ "No",
+ "Yes: One",
+ "Yes: More than one"
+ ],
+ "default": "No"
+ }
+ },
+ "required": [
+ "Do you have any pets?"
+ ],
+ "dependencies": {
+ "Do you have any pets?": {
+ "oneOf": [
+ {
+ "properties": {
+ "Do you have any pets?": {
+ "enum": [
+ "No"
+ ]
+ }
+ }
+ },
+ {
+ "properties": {
+ "Do you have any pets?": {
+ "enum": [
+ "Yes: One"
+ ]
+ },
+ "How old is your pet?": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "How old is your pet?"
+ ]
+ },
+ {
+ "properties": {
+ "Do you have any pets?": {
+ "enum": [
+ "Yes: More than one"
+ ]
+ },
+ "Do you want to get rid of any?": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "Do you want to get rid of any?"
+ ]
+ }
+ ]
+ }
+ }
+}
+```
+
+In this example the user is prompted with different follow-up questions dynamically based on their answer to the first question.
+
+Note that this is quite far from complete `oneOf` support!
+
+In these examples, the "Do you have any pets?" question is validated against the corresponding property in each schema in the `oneOf` array. If exactly one matches, the rest of that schema is merged with the existing schema.
+
## JSON Schema supporting status
This component follows [JSON Schema](http://json-schema.org/documentation.html) specs. Due to the limitation of form widgets, there are some exceptions as follows:
@@ -1505,6 +1671,7 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html)
This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported.
* `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]`
Nobody yet has come up with a PR that adds this feature with a simple and easy-to-understand UX.
+ You can use `oneOf` with [schema dependencies](#schema-depdencies) to dynamically add schema properties based on input data but this feature does not bring general support for `oneOf` elsewhere in a schema.
## Tips and tricks
@@ -1576,7 +1743,7 @@ $ git push --tags origin
### Q: Does rjsf support `oneOf`, `anyOf`, multiple types in an array, etc.?
-A: Not yet, but perhaps you will be the person whose PR will finally add the feature in a way that gets merged. For inspiration, see [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648).
+A: Not yet (except for a special case where you can use `oneOf` in [schema dependencies](#schema-depdencies)), but perhaps you will be the person whose PR will finally add the feature in a way that gets merged. For inspiration, see [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648).
### Q: Will react-jsonschema-form support Material, Ant-Design, Foundation, or [some other specific widget library or frontend style]?
diff --git a/playground/samples/index.js b/playground/samples/index.js
index ddc7b18c0c..9d8806f8dc 100644
--- a/playground/samples/index.js
+++ b/playground/samples/index.js
@@ -15,6 +15,8 @@ import single from "./single";
import customArray from "./customArray";
import customObject from "./customObject";
import alternatives from "./alternatives";
+import propertyDependencies from "./propertyDependencies";
+import schemaDependencies from "./schemaDependencies";
export const samples = {
Simple: simple,
@@ -34,4 +36,6 @@ export const samples = {
"Custom Array": customArray,
"Custom Object": customObject,
Alternatives: alternatives,
+ "Property dependencies": propertyDependencies,
+ "Schema dependencies": schemaDependencies,
};
diff --git a/playground/samples/propertyDependencies.js b/playground/samples/propertyDependencies.js
new file mode 100644
index 0000000000..133f708ea0
--- /dev/null
+++ b/playground/samples/propertyDependencies.js
@@ -0,0 +1,98 @@
+import React from "react";
+
+module.exports = {
+ schema: {
+ title: "Property dependencies",
+ description: "These samples are best viewed without live validation.",
+ type: "object",
+ properties: {
+ unidirectional: {
+ title: "Unidirectional",
+ src:
+ "https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies",
+ type: "object",
+ properties: {
+ name: {
+ type: "string",
+ },
+ credit_card: {
+ type: "number",
+ },
+ billing_address: {
+ type: "string",
+ },
+ },
+ required: ["name"],
+ dependencies: {
+ credit_card: ["billing_address"],
+ },
+ },
+ bidirectional: {
+ title: "Bidirectional",
+ src:
+ "https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies",
+ description:
+ "Dependencies are not bidirectional, you can, of course, define the bidirectional dependencies explicitly.",
+ type: "object",
+ properties: {
+ name: {
+ type: "string",
+ },
+ credit_card: {
+ type: "number",
+ },
+ billing_address: {
+ type: "string",
+ },
+ },
+ required: ["name"],
+ dependencies: {
+ credit_card: ["billing_address"],
+ billing_address: ["credit_card"],
+ },
+ },
+ },
+ },
+ uiSchema: {
+ unidirectional: {
+ credit_card: {
+ "ui:help": (
+
+ If you enter anything here then billing_address will
+ become required.
+
+ ),
+ },
+ billing_address: {
+ "ui:help":
+ "It’s okay to have a billing address without a credit card number.",
+ },
+ },
+ bidirectional: {
+ credit_card: {
+ "ui:help": (
+
+ "If you enter anything here then billing_address will
+ become required.
+
+ ),
+ },
+ billing_address: {
+ "ui:help": (
+
+ If you enter anything here then credit_card will become
+ required.
+
+ ),
+ },
+ },
+ },
+ formData: {
+ unidirectional: {
+ name: "Tim",
+ },
+ bidirectional: {
+ name: "Jill",
+ },
+ },
+};
diff --git a/playground/samples/schemaDependencies.js b/playground/samples/schemaDependencies.js
new file mode 100644
index 0000000000..610d6dc635
--- /dev/null
+++ b/playground/samples/schemaDependencies.js
@@ -0,0 +1,177 @@
+import React from "react";
+
+module.exports = {
+ schema: {
+ title: "Schema dependencies",
+ description: "These samples are best viewed without live validation.",
+ type: "object",
+ properties: {
+ simple: {
+ src:
+ "https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies",
+ title: "Simple",
+ type: "object",
+ properties: {
+ name: {
+ type: "string",
+ },
+ credit_card: {
+ type: "number",
+ },
+ },
+ required: ["name"],
+ dependencies: {
+ credit_card: {
+ properties: {
+ billing_address: {
+ type: "string",
+ },
+ },
+ required: ["billing_address"],
+ },
+ },
+ },
+ conditional: {
+ title: "Conditional",
+ $ref: "#/definitions/person",
+ },
+ arrayOfConditionals: {
+ title: "Array of conditionals",
+ type: "array",
+ items: {
+ $ref: "#/definitions/person",
+ },
+ },
+ fixedArrayOfConditionals: {
+ title: "Fixed array of conditionals",
+ type: "array",
+ items: [
+ {
+ title: "Primary person",
+ $ref: "#/definitions/person",
+ },
+ ],
+ additionalItems: {
+ title: "Additional person",
+ $ref: "#/definitions/person",
+ },
+ },
+ },
+ definitions: {
+ person: {
+ title: "Person",
+ type: "object",
+ properties: {
+ "Do you have any pets?": {
+ type: "string",
+ enum: ["No", "Yes: One", "Yes: More than one"],
+ default: "No",
+ },
+ },
+ required: ["Do you have any pets?"],
+ dependencies: {
+ "Do you have any pets?": {
+ oneOf: [
+ {
+ properties: {
+ "Do you have any pets?": {
+ enum: ["No"],
+ },
+ },
+ },
+ {
+ properties: {
+ "Do you have any pets?": {
+ enum: ["Yes: One"],
+ },
+ "How old is your pet?": {
+ type: "number",
+ },
+ },
+ required: ["How old is your pet?"],
+ },
+ {
+ properties: {
+ "Do you have any pets?": {
+ enum: ["Yes: More than one"],
+ },
+ "Do you want to get rid of any?": {
+ type: "boolean",
+ },
+ },
+ required: ["Do you want to get rid of any?"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ uiSchema: {
+ simple: {
+ credit_card: {
+ "ui:help": (
+
+ If you enter anything here then billing_address will be
+ dynamically added to the form.
+
+ ),
+ },
+ },
+ conditional: {
+ "Do you want to get rid of any?": {
+ "ui:widget": "radio",
+ },
+ },
+ arrayOfConditionals: {
+ items: {
+ "Do you want to get rid of any?": {
+ "ui:widget": "radio",
+ },
+ },
+ },
+ fixedArrayOfConditionals: {
+ items: {
+ "Do you want to get rid of any?": {
+ "ui:widget": "radio",
+ },
+ },
+ additionalItems: {
+ "Do you want to get rid of any?": {
+ "ui:widget": "radio",
+ },
+ },
+ },
+ },
+ formData: {
+ simple: {
+ name: "Randy",
+ },
+ conditional: {
+ "Do you have any pets?": "No",
+ },
+ arrayOfConditionals: [
+ {
+ "Do you have any pets?": "Yes: One",
+ "How old is your pet?": 6,
+ },
+ {
+ "Do you have any pets?": "Yes: More than one",
+ "Do you want to get rid of any?": false,
+ },
+ ],
+ fixedArrayOfConditionals: [
+ {
+ "Do you have any pets?": "No",
+ },
+ {
+ "Do you have any pets?": "Yes: One",
+ "How old is your pet?": 6,
+ },
+ {
+ "Do you have any pets?": "Yes: More than one",
+ "Do you want to get rid of any?": true,
+ },
+ ],
+ },
+};
diff --git a/src/components/Form.js b/src/components/Form.js
index 14c90c8aef..79215ffae8 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -48,7 +48,8 @@ export default class Form extends Component {
const idSchema = toIdSchema(
schema,
uiSchema["ui:rootFieldId"],
- definitions
+ definitions,
+ formData
);
return {
schema,
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index 22fe42cde0..67f433394d 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -338,17 +338,23 @@ class ArrayField extends Component {
const arrayProps = {
canAdd: this.canAddItem(formData),
items: formData.map((item, index) => {
+ const itemSchema = retrieveSchema(schema.items, definitions, item);
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
const itemIdPrefix = idSchema.$id + "_" + index;
- const itemIdSchema = toIdSchema(itemsSchema, itemIdPrefix, definitions);
+ const itemIdSchema = toIdSchema(
+ itemSchema,
+ itemIdPrefix,
+ definitions,
+ item
+ );
return this.renderArrayFieldItem({
index,
canMoveUp: index > 0,
canMoveDown: index < formData.length - 1,
- itemSchema: itemsSchema,
+ itemSchema: itemSchema,
itemIdSchema,
itemErrorSchema,
- itemData: formData[index],
+ itemData: item,
itemUiSchema: uiSchema.items,
autofocus: autofocus && index === 0,
onBlur,
@@ -380,6 +386,7 @@ class ArrayField extends Component {
schema,
idSchema,
uiSchema,
+ formData,
disabled,
readonly,
autofocus,
@@ -389,7 +396,7 @@ class ArrayField extends Component {
} = this.props;
const items = this.props.formData;
const { widgets, definitions, formContext } = registry;
- const itemsSchema = retrieveSchema(schema.items, definitions);
+ const itemsSchema = retrieveSchema(schema.items, definitions, formData);
const enumOptions = optionsList(itemsSchema);
const { widget = "select", ...options } = {
...getUiOptions(uiSchema),
@@ -455,6 +462,7 @@ class ArrayField extends Component {
const {
schema,
uiSchema,
+ formData,
errorSchema,
idSchema,
name,
@@ -470,11 +478,11 @@ class ArrayField extends Component {
let items = this.props.formData;
const { ArrayFieldTemplate, definitions, fields } = registry;
const { TitleField } = fields;
- const itemSchemas = schema.items.map(item =>
- retrieveSchema(item, definitions)
+ const itemSchemas = schema.items.map((item, index) =>
+ retrieveSchema(item, definitions, formData[index])
);
const additionalSchema = allowAdditionalItems(schema)
- ? retrieveSchema(schema.additionalItems, definitions)
+ ? retrieveSchema(schema.additionalItems, definitions, formData)
: null;
if (!items || items.length < itemSchemas.length) {
@@ -489,11 +497,19 @@ class ArrayField extends Component {
className: "field field-array field-array-fixed-items",
disabled,
idSchema,
+ formData,
items: items.map((item, index) => {
const additional = index >= itemSchemas.length;
- const itemSchema = additional ? additionalSchema : itemSchemas[index];
+ const itemSchema = additional
+ ? retrieveSchema(schema.additionalItems, definitions, item)
+ : itemSchemas[index];
const itemIdPrefix = idSchema.$id + "_" + index;
- const itemIdSchema = toIdSchema(itemSchema, itemIdPrefix, definitions);
+ const itemIdSchema = toIdSchema(
+ itemSchema,
+ itemIdPrefix,
+ definitions,
+ item
+ );
const itemUiSchema = additional
? uiSchema.additionalItems || {}
: Array.isArray(uiSchema.items)
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index bfbb034ad3..e10c22410c 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -72,7 +72,7 @@ class ObjectField extends Component {
} = this.props;
const { definitions, fields, formContext } = registry;
const { SchemaField, TitleField, DescriptionField } = fields;
- const schema = retrieveSchema(this.props.schema, definitions);
+ const schema = retrieveSchema(this.props.schema, definitions, formData);
const title = schema.title === undefined ? name : schema.title;
const description = uiSchema["ui:description"] || schema.description;
let orderedProperties;
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 57a87c9eda..6ab79bb664 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -148,6 +148,7 @@ DefaultTemplate.defaultProps = {
function SchemaFieldRender(props) {
const {
uiSchema,
+ formData,
errorSchema,
idSchema,
name,
@@ -160,7 +161,7 @@ function SchemaFieldRender(props) {
formContext,
FieldTemplate = DefaultTemplate,
} = registry;
- const schema = retrieveSchema(props.schema, definitions);
+ const schema = retrieveSchema(props.schema, definitions, formData);
const FieldComponent = getFieldComponent(schema, uiSchema, idSchema, fields);
const { DescriptionField } = fields;
const disabled = Boolean(props.disabled || uiSchema["ui:disabled"]);
diff --git a/src/utils.js b/src/utils.js
index e19362134f..6633fd4250 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,5 +1,6 @@
import React from "react";
import "setimmediate";
+import validateFormData from "./validate";
const widgetMap = {
boolean: {
@@ -167,7 +168,7 @@ export function getDefaultFormState(_schema, formData, definitions = {}) {
if (!isObject(_schema)) {
throw new Error("Invalid schema: " + _schema);
}
- const schema = retrieveSchema(_schema, definitions);
+ const schema = retrieveSchema(_schema, definitions, formData);
const defaults = computeDefaults(schema, _schema.default, definitions);
if (typeof formData === "undefined") {
// No form data? Use schema defaults.
@@ -394,17 +395,134 @@ function findSchemaDefinition($ref, definitions = {}) {
throw new Error(`Could not find a definition for ${$ref}.`);
}
-export function retrieveSchema(schema, definitions = {}) {
- // No $ref attribute found, returning the original schema.
- if (!schema.hasOwnProperty("$ref")) {
+export function retrieveSchema(schema, definitions = {}, formData = {}) {
+ if (schema.hasOwnProperty("$ref")) {
+ // Retrieve the referenced schema definition.
+ const $refSchema = findSchemaDefinition(schema.$ref, definitions);
+ // Drop the $ref property of the source schema.
+ const { $ref, ...localSchema } = schema;
+ // Update referenced schema definition with local schema properties.
+ return retrieveSchema(
+ { ...$refSchema, ...localSchema },
+ definitions,
+ formData
+ );
+ } else if (schema.hasOwnProperty("dependencies")) {
+ const resolvedSchema = resolveDependencies(schema, definitions, formData);
+ return retrieveSchema(resolvedSchema, definitions, formData);
+ } else {
+ // No $ref or dependencies attribute found, returning the original schema.
+ return schema;
+ }
+}
+
+function resolveDependencies(schema, definitions, formData) {
+ // Drop the dependencies from the source schema.
+ let { dependencies = {}, ...resolvedSchema } = schema;
+ // Process dependencies updating the local schema properties as appropriate.
+ for (const dependencyKey in dependencies) {
+ // Skip this dependency if its trigger property is not present.
+ if (!formData.hasOwnProperty(dependencyKey)) {
+ continue;
+ }
+ const dependencyValue = dependencies[dependencyKey];
+ if (Array.isArray(dependencyValue)) {
+ resolvedSchema = withDependentProperties(resolvedSchema, dependencyValue);
+ } else if (isObject(dependencyValue)) {
+ resolvedSchema = withDependentSchema(
+ resolvedSchema,
+ definitions,
+ formData,
+ dependencyKey,
+ dependencyValue
+ );
+ }
+ }
+ return resolvedSchema;
+}
+
+function withDependentProperties(schema, additionallyRequired) {
+ if (!additionallyRequired) {
+ return schema;
+ }
+ const required = Array.isArray(schema.required)
+ ? Array.from(new Set([...schema.required, ...additionallyRequired]))
+ : additionallyRequired;
+ return { ...schema, required: required };
+}
+
+function withDependentSchema(
+ schema,
+ definitions,
+ formData,
+ dependencyKey,
+ dependencyValue
+) {
+ let { oneOf, ...dependentSchema } = retrieveSchema(
+ dependencyValue,
+ definitions,
+ formData
+ );
+ schema = mergeSchemas(schema, dependentSchema);
+ return oneOf === undefined
+ ? schema
+ : withExactlyOneSubschema(
+ schema,
+ definitions,
+ formData,
+ dependencyKey,
+ oneOf
+ );
+}
+
+function withExactlyOneSubschema(
+ schema,
+ definitions,
+ formData,
+ dependencyKey,
+ oneOf
+) {
+ if (!Array.isArray(oneOf)) {
+ throw new Error(
+ `invalid oneOf: it is some ${typeof oneOf} instead of an array`
+ );
+ }
+ const validSubschemas = oneOf.filter(subschema => {
+ if (!subschema.properties) {
+ return false;
+ }
+ const { [dependencyKey]: conditionPropertySchema } = subschema.properties;
+ if (conditionPropertySchema) {
+ const conditionSchema = {
+ type: "object",
+ properties: {
+ [dependencyKey]: conditionPropertySchema,
+ },
+ };
+ const { errors } = validateFormData(formData, conditionSchema);
+ return errors.length === 0;
+ }
+ });
+ if (validSubschemas.length !== 1) {
+ console.warn(
+ "ignoring oneOf in dependencies because there isn't exactly one subschema that is valid"
+ );
return schema;
}
- // Retrieve the referenced schema definition.
- const $refSchema = findSchemaDefinition(schema.$ref, definitions);
- // Drop the $ref property of the source schema.
- const { $ref, ...localSchema } = schema;
- // Update referenced schema definition with local schema properties.
- return { ...$refSchema, ...localSchema };
+ const subschema = validSubschemas[0];
+ const {
+ [dependencyKey]: conditionPropertySchema,
+ ...dependentSubschema
+ } = subschema.properties;
+ const dependentSchema = { ...subschema, properties: dependentSubschema };
+ return mergeSchemas(
+ schema,
+ retrieveSchema(dependentSchema, definitions, formData)
+ );
+}
+
+function mergeSchemas(schema1, schema2) {
+ return mergeObjects(schema1, schema2, true);
}
function isArguments(object) {
@@ -493,16 +611,16 @@ export function shouldRender(comp, nextProps, nextState) {
return !deepEquals(props, nextProps) || !deepEquals(state, nextState);
}
-export function toIdSchema(schema, id, definitions) {
+export function toIdSchema(schema, id, definitions, formData = {}) {
const idSchema = {
$id: id || "root",
};
if ("$ref" in schema) {
- const _schema = retrieveSchema(schema, definitions);
- return toIdSchema(_schema, id, definitions);
+ const _schema = retrieveSchema(schema, definitions, formData);
+ return toIdSchema(_schema, id, definitions, formData);
}
if ("items" in schema && !schema.items.$ref) {
- return toIdSchema(schema.items, id, definitions);
+ return toIdSchema(schema.items, id, definitions, formData);
}
if (schema.type !== "object") {
return idSchema;
@@ -510,7 +628,7 @@ export function toIdSchema(schema, id, definitions) {
for (const name in schema.properties || {}) {
const field = schema.properties[name];
const fieldId = idSchema.$id + "_" + name;
- idSchema[name] = toIdSchema(field, fieldId, definitions);
+ idSchema[name] = toIdSchema(field, fieldId, definitions, formData[name]);
}
return idSchema;
}
diff --git a/test/utils_test.js b/test/utils_test.js
index 65e08a66b8..09b9d8dea0 100644
--- a/test/utils_test.js
+++ b/test/utils_test.js
@@ -496,6 +496,330 @@ describe("utils", () => {
title: "foo",
});
});
+
+ describe("property dependencies", () => {
+ describe("false condition", () => {
+ it("should not add required properties", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ b: { type: "integer" },
+ },
+ required: ["a"],
+ dependencies: {
+ a: ["b"],
+ },
+ };
+ const definitions = {};
+ const formData = {};
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ b: { type: "integer" },
+ },
+ required: ["a"],
+ });
+ });
+ });
+
+ describe("true condition", () => {
+ describe("when required is not defined", () => {
+ it("should define required properties", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ b: { type: "integer" },
+ },
+ dependencies: {
+ a: ["b"],
+ },
+ };
+ const definitions = {};
+ const formData = { a: "1" };
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ b: { type: "integer" },
+ },
+ required: ["b"],
+ });
+ });
+ });
+
+ describe("when required is defined", () => {
+ it("should concat required properties", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ b: { type: "integer" },
+ },
+ required: ["a"],
+ dependencies: {
+ a: ["b"],
+ },
+ };
+ const definitions = {};
+ const formData = { a: "1" };
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ b: { type: "integer" },
+ },
+ required: ["a", "b"],
+ });
+ });
+ });
+ });
+ });
+
+ describe("schema dependencies", () => {
+ describe("conditional", () => {
+ describe("false condition", () => {
+ it("should not modify properties", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ },
+ dependencies: {
+ a: {
+ properties: {
+ b: { type: "integer" },
+ },
+ },
+ },
+ };
+ const definitions = {};
+ const formData = {};
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ },
+ });
+ });
+ });
+
+ describe("true condition", () => {
+ it("should add properties", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ },
+ dependencies: {
+ a: {
+ properties: {
+ b: { type: "integer" },
+ },
+ },
+ },
+ };
+ const definitions = {};
+ const formData = { a: "1" };
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ b: { type: "integer" },
+ },
+ });
+ });
+ });
+
+ describe("with $ref in dependency", () => {
+ it("should retrieve referenced schema", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ },
+ dependencies: {
+ a: {
+ $ref: "#/definitions/needsB",
+ },
+ },
+ };
+ const definitions = {
+ needsB: {
+ properties: {
+ b: { type: "integer" },
+ },
+ },
+ };
+ const formData = { a: "1" };
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ b: { type: "integer" },
+ },
+ });
+ });
+ });
+ });
+
+ describe("dynamic", () => {
+ describe("false condition", () => {
+ it("should not modify properties", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ },
+ dependencies: {
+ a: {
+ oneOf: [
+ {
+ properties: {
+ a: { enum: ["int"] },
+ b: { type: "integer" },
+ },
+ },
+ {
+ properties: {
+ a: { enum: ["bool"] },
+ b: { type: "boolean" },
+ },
+ },
+ ],
+ },
+ },
+ };
+ const definitions = {};
+ const formData = {};
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string" },
+ },
+ });
+ });
+ });
+
+ describe("true condition", () => {
+ it("should add 'first' properties given 'first' data", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string", enum: ["int", "bool"] },
+ },
+ dependencies: {
+ a: {
+ oneOf: [
+ {
+ properties: {
+ a: { enum: ["int"] },
+ b: { type: "integer" },
+ },
+ },
+ {
+ properties: {
+ a: { enum: ["bool"] },
+ b: { type: "boolean" },
+ },
+ },
+ ],
+ },
+ },
+ };
+ const definitions = {};
+ const formData = { a: "int" };
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string", enum: ["int", "bool"] },
+ b: { type: "integer" },
+ },
+ });
+ });
+
+ it("should add 'second' properties given 'second' data", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string", enum: ["int", "bool"] },
+ },
+ dependencies: {
+ a: {
+ oneOf: [
+ {
+ properties: {
+ a: { enum: ["int"] },
+ b: { type: "integer" },
+ },
+ },
+ {
+ properties: {
+ a: { enum: ["bool"] },
+ b: { type: "boolean" },
+ },
+ },
+ ],
+ },
+ },
+ };
+ const definitions = {};
+ const formData = { a: "bool" };
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string", enum: ["int", "bool"] },
+ b: { type: "boolean" },
+ },
+ });
+ });
+ });
+
+ describe("with $ref in dependency", () => {
+ it("should retrieve the referenced schema", () => {
+ const schema = {
+ type: "object",
+ properties: {
+ a: { type: "string", enum: ["int", "bool"] },
+ },
+ dependencies: {
+ a: {
+ $ref: "#/definitions/typedInput",
+ },
+ },
+ };
+ const definitions = {
+ typedInput: {
+ oneOf: [
+ {
+ properties: {
+ a: { enum: ["int"] },
+ b: { type: "integer" },
+ },
+ },
+ {
+ properties: {
+ a: { enum: ["bool"] },
+ b: { type: "boolean" },
+ },
+ },
+ ],
+ },
+ };
+ const formData = { a: "bool" };
+ expect(retrieveSchema(schema, definitions, formData)).eql({
+ type: "object",
+ properties: {
+ a: { type: "string", enum: ["int", "bool"] },
+ b: { type: "boolean" },
+ },
+ });
+ });
+ });
+ });
+ });
});
describe("shouldRender", () => {