Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
7 changes: 6 additions & 1 deletion 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`.
Comment thread
LucianBuzzo marked this conversation as 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
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,
};
172 changes: 172 additions & 0 deletions src/components/fields/AnyOfField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React, { Component } from "react";
import PropTypes from "prop-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: PropTypes.shape({
widgets: PropTypes.objectOf(
PropTypes.oneOfType([PropTypes.func, PropTypes.object])
).isRequired,
fields: PropTypes.objectOf(PropTypes.func).isRequired,
definitions: PropTypes.object.isRequired,
ArrayFieldTemplate: PropTypes.func,
ObjectFieldTemplate: PropTypes.func,
Comment thread
LucianBuzzo marked this conversation as resolved.
Outdated
FieldTemplate: PropTypes.func,
formContext: PropTypes.object.isRequired,
}),
};
}

export default AnyOfField;
2 changes: 1 addition & 1 deletion src/components/fields/ObjectField.js
Original file line number Diff line number Diff line change
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
40 changes: 38 additions & 2 deletions src/components/fields/SchemaField.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from "prop-types";

import {
isMultiSelect,
isSelect,
retrieveSchema,
toIdSchema,
getDefaultRegistry,
Expand Down Expand Up @@ -35,6 +36,13 @@ function getFieldComponent(schema, uiSchema, idSchema, fields) {
}

const componentName = COMPONENT_TYPES[getSchemaType(schema)];

// If the type is not defined and the schema uses 'anyOf', don't render
// a field and let the AnyOfField component handle the form display
if (!componentName && schema.anyOf) {
return () => null;
}

return componentName in fields
? fields[componentName]
: () => {
Expand Down Expand Up @@ -123,7 +131,7 @@ function DefaultTemplate(props) {
onKeyChange,
} = props;
if (hidden) {
return children;
return <div className="hidden">{children}</div>;
}
const additional = props.schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG);
const keyLabel = `${label} Key`;
Expand Down Expand Up @@ -296,7 +304,35 @@ function SchemaFieldRender(props) {
uiSchema,
};

return <FieldTemplate {...fieldProps}>{field}</FieldTemplate>;
const _AnyOfField = registry.fields.AnyOfField;

return (
<FieldTemplate {...fieldProps}>
{field}

{/*
If the schema `anyOf` can be rendered as a select control, don't
render the `anyOf` selection and let `StringField` component handle
rendering
*/}
{schema.anyOf && !isSelect(schema) && (
Comment thread
glasserc marked this conversation as resolved.
<_AnyOfField
disabled={disabled}
errorSchema={errorSchema}
formData={formData}
idPrefix={idPrefix}
idSchema={idSchema}
onBlur={props.onBlur}
onChange={props.onChange}
onFocus={props.onFocus}
schema={schema}
registry={registry}
safeRenderCompletion={props.safeRenderCompletion}
uiSchema={uiSchema}
/>
)}
</FieldTemplate>
);
}

class SchemaField extends React.Component {
Expand Down
2 changes: 2 additions & 0 deletions src/components/fields/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AnyOfField from "./AnyOfField";
import ArrayField from "./ArrayField";
import BooleanField from "./BooleanField";
import DescriptionField from "./DescriptionField";
Expand All @@ -9,6 +10,7 @@ import TitleField from "./TitleField";
import UnsupportedField from "./UnsupportedField";

export default {
AnyOfField,
ArrayField,
BooleanField,
DescriptionField,
Expand Down
13 changes: 13 additions & 0 deletions src/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,16 @@ export default function validateFormData(

return { errors: newErrors, errorSchema: newErrorSchema };
}

/**
* Validates data against a schema, returning true if the data is valid, or
* false otherwise. If the schema is invalid, then this function will return
* false.
*/
export function isValid(schema, data) {
try {
return ajv.validate(schema, data);
} catch (e) {
return false;
}
}
Loading