Skip to content

Commit

Permalink
feat: supporting anyof
Browse files Browse the repository at this point in the history
  • Loading branch information
picodoth committed Jun 1, 2018
1 parent 66ab17a commit 8872e55
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 5 deletions.
55 changes: 55 additions & 0 deletions playground/samples/anyOf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module.exports = {
schema: {
"title": "Any of",
"type": "object",
"properties": {
"List of widgets": {
"type": "array",
"items": {
"anyOf": [
{
"title": "string",
"type": "string",
"default": ""
},
{
"title": "integer",
"type": "integer",
"default": 0
},
{
"title": "array",
"type": "array",
"items": {
"anyOf": [
{
"title": "string",
"type": "string",
"default": ""
},
{
"title": "integer",
"type": "integer",
"default": 0
}
]
},
"default": [""],
}
]
}
}
}
},
uiSchema: {},
formData: {
"List of widgets": [
27,
"Batman",
[
"Bruce",
"Wayne"
]
]
}
};
2 changes: 2 additions & 0 deletions playground/samples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import customObject from "./customObject";
import alternatives from "./alternatives";
import propertyDependencies from "./propertyDependencies";
import schemaDependencies from "./schemaDependencies";
import anyOf from "./anyOf";

export const samples = {
Simple: simple,
Expand All @@ -38,4 +39,5 @@ export const samples = {
Alternatives: alternatives,
"Property dependencies": propertyDependencies,
"Schema dependencies": schemaDependencies,
"Any of": anyOf,
};
112 changes: 109 additions & 3 deletions src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { validate as jsonValidate } from "jsonschema";

import UnsupportedField from "./UnsupportedField";
import {
Expand All @@ -13,9 +14,37 @@ import {
optionsList,
retrieveSchema,
toIdSchema,
getSchemaType,
getDefaultRegistry,
} from "../../utils";

function resolveSchemaRecursively(schema, definitions) {
let unwrappedSchema = schema;
if (schema.$ref) {
unwrappedSchema = retrieveSchema(schema, definitions);
}

if (unwrappedSchema.properties) {
Object.keys(unwrappedSchema.properties).forEach(p => {
const prop = unwrappedSchema.properties[p];
if (prop.type === "array") {
if (prop.items.anyOf) {
prop.items.anyOf.forEach((ele, index) => {
prop.items.anyOf[index] = resolveSchemaRecursively(prop.items[index], definitions);
});
} else if (prop.items.$ref) {
prop.items = resolveSchemaRecursively(prop.items, definitions);
}
// else: do nothing since there is no other type pointer
} else {
unwrappedSchema.properties[p] = resolveSchemaRecursively(prop, definitions);
}
});
}

return unwrappedSchema;
}

function ArrayFieldTitle({ TitleField, idSchema, title, required }) {
if (!title) {
// See #312: Ensure compatibility with old versions of React.
Expand Down Expand Up @@ -57,6 +86,7 @@ function DefaultArrayItem(props) {
return (
<div key={props.index} className={props.className}>
<div className={props.hasToolbar ? "col-xs-9" : "col-xs-12"}>
{props.selectWidget}
{props.children}
</div>

Expand Down Expand Up @@ -199,6 +229,40 @@ class ArrayField extends Component {
return schema.items.title || schema.items.description || "Item";
}

getAnyOfItemsFromProps() {
const anyOfSchema = this.props.schema.items.anyOf;
const formDataItems = this.props.formData;

if (this.getAnyOfItemsSchema() && formDataItems.map) {
return formDataItems.map((item) => {
const type = typeof item;
const itemType = (type === "object" && Array.isArray(item)) ? "array" : type;
const schema = this.getAnyOfItemSchema(anyOfSchema, itemType, item);

return schema;
});
}

return [];
}

getAnyOfItemSchema(anyOfSchema, type, item) {
return anyOfSchema.find((schemaElement) => {
if ("$ref" in schemaElement) {
const refSchema = resolveSchemaRecursively(schemaElement, this.props.registry.definitions);
const { errors } = jsonValidate(item, refSchema);
return errors.length === 0;
}
const schemaElementType = schemaElement.type === "integer" ? "number" : schemaElement.type;
return schemaElementType === type;
});
}

getAnyOfItemsSchema() {
const { schema } = this.props;
return schema.items.anyOf;
}

isItemRequired(itemSchema) {
if (Array.isArray(itemSchema.type)) {
// While we don't yet support composite/nullable jsonschema types, it's
Expand Down Expand Up @@ -232,9 +296,14 @@ class ArrayField extends Component {
if (isFixedItems(schema) && allowAdditionalItems(schema)) {
itemSchema = schema.additionalItems;
}
if (itemSchema.anyOf) {
// use the first type by default if it's an anyOf
itemSchema = itemSchema.anyOf[0];
}
const newItem = getDefaultFormState(itemSchema, undefined, definitions);
this.props.onChange([
...formData,
getDefaultFormState(itemSchema, undefined, definitions),
newItem,
]);
};

Expand Down Expand Up @@ -324,6 +393,19 @@ class ArrayField extends Component {
this.props.onChange(value);
};

anyOfOptions(anyOfItems) {
return anyOfItems.map(item => ({ value: getSchemaType(item), label: item.title }));
}

setWidgetType(index, value) {
const { formData, registry: { definitions } } = this.props;
const anyOfItemsSchema = this.getAnyOfItemsSchema();
const newItems = formData.slice();
const foundItem = anyOfItemsSchema.find((element) => getSchemaType(element) === value);
newItems[index] = getDefaultFormState(foundItem, undefined, definitions);
this.props.onChange(newItems);
}

render() {
const {
schema,
Expand Down Expand Up @@ -374,13 +456,18 @@ class ArrayField extends Component {
const title = schema.title === undefined ? name : schema.title;
const { ArrayFieldTemplate, definitions, fields, formContext } = registry;
const { TitleField, DescriptionField } = fields;
const itemsSchema = retrieveSchema(schema.items, definitions);
const anyOfItems = this.getAnyOfItemsFromProps();
const itemsSchema = retrieveSchema(schema.items || schema.items.anyOf, definitions);
const anyOfItemsSchema = this.getAnyOfItemsSchema();
const arrayProps = {
canAdd: this.canAddItem(formData),
items: formData.map((item, index) => {
const itemSchema = retrieveSchema(schema.items, definitions, item);
let itemSchema = retrieveSchema(schema.items, definitions, item);
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
const itemIdPrefix = idSchema.$id + "_" + index;
if (anyOfItemsSchema) {
itemSchema = anyOfItems[index];
}
const itemIdSchema = toIdSchema(
itemSchema,
itemIdPrefix,
Expand All @@ -400,6 +487,8 @@ class ArrayField extends Component {
autofocus: autofocus && index === 0,
onBlur,
onFocus,
anyOfItemsSchema: anyOfItemsSchema,
selectWidgetValue: anyOfItems.length > 0 ? getSchemaType(itemSchema) : "",
});
}),
className: `field field-array field-array-of-${itemsSchema.type}`,
Expand Down Expand Up @@ -612,6 +701,8 @@ class ArrayField extends Component {
onBlur,
onFocus,
rawErrors,
anyOfItemsSchema,
selectWidgetValue
} = props;
const {
disabled,
Expand All @@ -621,6 +712,7 @@ class ArrayField extends Component {
} = this.props;
const {
fields: { SchemaField },
widgets: { SelectWidget },
} = registry;
const { orderable, removable } = {
orderable: true,
Expand All @@ -634,6 +726,17 @@ class ArrayField extends Component {
};
has.toolbar = Object.keys(has).some(key => has[key]);

const selectWidget = anyOfItemsSchema ? (
<div className="form-group" style={{ width: 120 }}>
<SelectWidget
schema={{type: "integer", default: selectWidgetValue }}
id="select-widget-id"
options={{enumOptions: this.anyOfOptions(anyOfItemsSchema)}}
value={selectWidgetValue}
onChange={(value) => this.setWidgetType(index, value)}/>
</div>
) : null;

return {
children: (
<SchemaField
Expand All @@ -653,6 +756,7 @@ class ArrayField extends Component {
rawErrors={rawErrors}
/>
),
selectWidget: selectWidget,
className: "array-item",
disabled,
hasToolbar: has.toolbar,
Expand All @@ -663,6 +767,8 @@ class ArrayField extends Component {
onDropIndexClick: this.onDropIndexClick,
onReorderClick: this.onReorderClick,
readonly,
anyOfItemsSchema,
selectWidgetValue,
};
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/fields/SchemaField.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const COMPONENT_TYPES = {
string: "StringField",
};

function getFieldComponent(schema, uiSchema, idSchema, fields) {
function getFieldComponent(schema, name, uiSchema, idSchema, fields) {
const field = uiSchema["ui:field"];
if (typeof field === "function") {
return field;
Expand Down Expand Up @@ -172,7 +172,7 @@ function SchemaFieldRender(props) {
toIdSchema(schema, null, definitions, formData, idPrefix),
idSchema
);
const FieldComponent = getFieldComponent(schema, uiSchema, idSchema, fields);
const FieldComponent = getFieldComponent(schema, name, uiSchema, idSchema, fields);
const { DescriptionField } = fields;
const disabled = Boolean(props.disabled || uiSchema["ui:disabled"]);
const readonly = Boolean(props.readonly || uiSchema["ui:readonly"]);
Expand Down
2 changes: 2 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export function getSchemaType(schema) {
let { type } = schema;
if (!type && schema.enum) {
type = "string";
} else if (!type && schema.$ref) {
type = schema.$ref.split('/').pop();
}
return type;
}
Expand Down

0 comments on commit 8872e55

Please sign in to comment.