Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add preliminary support for the anyOf keyword #1118

Merged
merged 6 commits into from
Jan 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1803,7 +1803,12 @@ This component follows [JSON Schema](http://json-schema.org/documentation.html)
* `additionalItems` keyword for arrays
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.
The `anyOf` keyword is supported but has the following caveats:
- The `anyOf` keyword is not supported when used inside the `items` keyword
for arrays.
- Properties declared inside the `anyOf` should not overlap with properties
"outside" of the `anyOf`.
LucianBuzzo marked this conversation as resolved.
Show resolved Hide resolved

You can use `oneOf` with [schema dependencies](#schema-dependencies) 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
Expand Down Expand Up @@ -1876,7 +1881,12 @@ $ git push --tags origin

### Q: Does rjsf support `oneOf`, `anyOf`, multiple types in an array, etc.?

A: Not yet (except for a special case where you can use `oneOf` in [schema dependencies](#schema-dependencies)), 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: The `anyOf` keyword is supported but has the following caveats:
- The `anyOf` keyword is not supported when used inside the `items` keyword
for arrays.
- Properties declared inside the `anyOf` should not overlap with properties
"outside" of the `anyOf`.
There is also special cased where you can use `oneOf` in [schema dependencies](#schema-dependencies), If you'd like to help improve support for these keywords, see the following issues for inspiration [#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]?

Expand Down
37 changes: 37 additions & 0 deletions playground/samples/anyOf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module.exports = {
schema: {
type: "object",
properties: {
age: {
type: "integer",
title: "Age",
},
},
anyOf: [
{
title: "First method of identification",
properties: {
firstName: {
type: "string",
title: "First name",
default: "Chuck",
},
lastName: {
type: "string",
title: "Last name",
},
},
},
{
title: "Second method of identification",
properties: {
idCode: {
type: "string",
title: "ID code",
},
},
},
],
},
formData: {},
};
2 changes: 2 additions & 0 deletions playground/samples/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import arrays from "./arrays";
import anyOf from "./anyOf";
import nested from "./nested";
import numbers from "./numbers";
import simple from "./simple";
Expand Down Expand Up @@ -40,4 +41,5 @@ export const samples = {
"Property dependencies": propertyDependencies,
"Schema dependencies": schemaDependencies,
"Additional Properties": additionalProperties,
"Optional Forms": anyOf,
};
163 changes: 163 additions & 0 deletions src/components/fields/AnyOfField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import * as types from "../../types";
import { guessType } from "../../utils";
import { isValid } from "../../validate";

class AnyOfField extends Component {
constructor(props) {
super(props);

const { formData, schema } = this.props;

this.state = {
selectedOption: this.getMatchingOption(formData, schema.anyOf),
};
}

componentWillReceiveProps(nextProps) {
const matchingOption = this.getMatchingOption(
nextProps.formData,
nextProps.schema.anyOf
);

if (matchingOption === this.state.selectedOption) {
return;
}

this.setState({ selectedOption: matchingOption });
}

getMatchingOption(formData, options) {
for (let i = 0; i < options.length; i++) {
if (isValid(options[i], formData)) {
return i;
}
}

// If the form data matches none of the options, use the first option
return 0;
}

onOptionChange = event => {
const selectedOption = parseInt(event.target.value, 10);
const { formData, onChange, schema } = this.props;
const options = schema.anyOf;

if (guessType(formData) === "object") {
const newFormData = Object.assign({}, formData);

const optionsToDiscard = options.slice();
optionsToDiscard.splice(selectedOption, 1);

// Discard any data added using other options
for (const option of optionsToDiscard) {
if (option.properties) {
for (const key in option.properties) {
if (newFormData.hasOwnProperty(key)) {
delete newFormData[key];
}
}
}
}

onChange(newFormData);
} else {
onChange(undefined);
}

this.setState({
selectedOption: parseInt(event.target.value, 10),
});
};

render() {
const {
disabled,
errorSchema,
formData,
idPrefix,
idSchema,
onBlur,
onChange,
onFocus,
schema,
registry,
safeRenderCompletion,
uiSchema,
} = this.props;

const _SchemaField = registry.fields.SchemaField;
const { selectedOption } = this.state;

const baseType = schema.type;
const options = schema.anyOf || [];
const option = options[selectedOption] || null;
let optionSchema;

if (option) {
// If the subschema doesn't declare a type, infer the type from the
// parent schema
optionSchema = option.type
? option
: Object.assign({}, option, { type: baseType });
}

return (
<div className="panel panel-default panel-body">
<div className="form-group">
<select
className="form-control"
onChange={this.onOptionChange}
value={selectedOption}
id={`${idSchema.$id}_anyof_select`}>
{options.map((option, index) => {
return (
<option key={index} value={index}>
{option.title || `Option ${index + 1}`}
</option>
);
})}
</select>
</div>

{option !== null && (
<_SchemaField
schema={optionSchema}
uiSchema={uiSchema}
errorSchema={errorSchema}
idSchema={idSchema}
idPrefix={idPrefix}
formData={formData}
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
registry={registry}
safeRenderCompletion={safeRenderCompletion}
disabled={disabled}
/>
)}
</div>
);
}
}

AnyOfField.defaultProps = {
disabled: false,
errorSchema: {},
idSchema: {},
uiSchema: {},
};

if (process.env.NODE_ENV !== "production") {
AnyOfField.propTypes = {
schema: PropTypes.object.isRequired,
uiSchema: PropTypes.object,
idSchema: PropTypes.object,
formData: PropTypes.any,
errorSchema: PropTypes.object,
registry: types.registry.isRequired,
};
}

export default AnyOfField;
31 changes: 2 additions & 29 deletions src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import AddButton from "../AddButton";
import IconButton from "../IconButton";
import React, { Component } from "react";
import PropTypes from "prop-types";
import includes from "core-js/library/fn/array/includes";
import * as types from "../../types";

import UnsupportedField from "./UnsupportedField";
import {
Expand Down Expand Up @@ -661,34 +661,7 @@ class ArrayField extends Component {
}

if (process.env.NODE_ENV !== "production") {
ArrayField.propTypes = {
schema: PropTypes.object.isRequired,
uiSchema: PropTypes.shape({
"ui:options": PropTypes.shape({
addable: PropTypes.bool,
orderable: PropTypes.bool,
removable: PropTypes.bool,
}),
}),
idSchema: PropTypes.object,
errorSchema: PropTypes.object,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func,
onFocus: PropTypes.func,
formData: PropTypes.array,
required: PropTypes.bool,
disabled: PropTypes.bool,
readonly: PropTypes.bool,
autofocus: PropTypes.bool,
registry: PropTypes.shape({
widgets: PropTypes.objectOf(
PropTypes.oneOfType([PropTypes.func, PropTypes.object])
).isRequired,
fields: PropTypes.objectOf(PropTypes.func).isRequired,
definitions: PropTypes.object.isRequired,
formContext: PropTypes.object.isRequired,
}),
};
ArrayField.propTypes = types.fieldProps;
}

export default ArrayField;
25 changes: 2 additions & 23 deletions src/components/fields/BooleanField.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import PropTypes from "prop-types";
import * as types from "../../types";

import {
getWidget,
Expand Down Expand Up @@ -55,28 +55,7 @@ function BooleanField(props) {
}

if (process.env.NODE_ENV !== "production") {
BooleanField.propTypes = {
schema: PropTypes.object.isRequired,
uiSchema: PropTypes.object,
idSchema: PropTypes.object,
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
formData: PropTypes.bool,
required: PropTypes.bool,
disabled: PropTypes.bool,
readonly: PropTypes.bool,
autofocus: PropTypes.bool,
registry: PropTypes.shape({
widgets: PropTypes.objectOf(
PropTypes.oneOfType([PropTypes.func, PropTypes.object])
).isRequired,
fields: PropTypes.objectOf(PropTypes.func).isRequired,
definitions: PropTypes.object.isRequired,
formContext: PropTypes.object.isRequired,
}),
rawErrors: PropTypes.arrayOf(PropTypes.string),
};
BooleanField.propTypes = types.fieldProps;
}

BooleanField.defaultProps = {
Expand Down
12 changes: 2 additions & 10 deletions src/components/fields/NumberField.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import PropTypes from "prop-types";

import * as types from "../../types";
import { asNumber } from "../../utils";

function NumberField(props) {
Expand All @@ -14,15 +14,7 @@ function NumberField(props) {
}

if (process.env.NODE_ENV !== "production") {
NumberField.propTypes = {
schema: PropTypes.object.isRequired,
uiSchema: PropTypes.object,
idSchema: PropTypes.object,
onChange: PropTypes.func.isRequired,
formData: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
required: PropTypes.bool,
formContext: PropTypes.object.isRequired,
};
NumberField.propTypes = types.fieldProps;
}

NumberField.defaultProps = {
Expand Down
24 changes: 3 additions & 21 deletions src/components/fields/ObjectField.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import AddButton from "../AddButton";
import React, { Component } from "react";
import PropTypes from "prop-types";
import * as types from "../../types";

import {
orderProperties,
Expand Down Expand Up @@ -178,7 +178,7 @@ class ObjectField extends Component {
let orderedProperties;

try {
const properties = Object.keys(schema.properties);
const properties = Object.keys(schema.properties || {});
orderedProperties = orderProperties(properties, uiSchema["ui:order"]);
} catch (err) {
return (
Expand Down Expand Up @@ -239,25 +239,7 @@ class ObjectField extends Component {
}

if (process.env.NODE_ENV !== "production") {
ObjectField.propTypes = {
schema: PropTypes.object.isRequired,
uiSchema: PropTypes.object,
errorSchema: PropTypes.object,
idSchema: PropTypes.object,
onChange: PropTypes.func.isRequired,
formData: PropTypes.object,
required: PropTypes.bool,
disabled: PropTypes.bool,
readonly: PropTypes.bool,
registry: PropTypes.shape({
widgets: PropTypes.objectOf(
PropTypes.oneOfType([PropTypes.func, PropTypes.object])
).isRequired,
fields: PropTypes.objectOf(PropTypes.func).isRequired,
definitions: PropTypes.object.isRequired,
formContext: PropTypes.object.isRequired,
}),
};
ObjectField.propTypes = types.fieldProps;
}

export default ObjectField;
Loading