Skip to content

Commit

Permalink
Add support for anyOf
Browse files Browse the repository at this point in the history
  • Loading branch information
Stathis committed Nov 23, 2016
1 parent a3f3e9d commit 3688b94
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 29 deletions.
50 changes: 50 additions & 0 deletions playground/samples/anyOf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module.exports = {
schema: {
"title": "Any of",
"type": "object",
"properties": {
"List of widgets": {
"type": "array",
"items": {
"anyOf": [
{
"title": "string",
"type": "string"
},
{
"title": "integer",
"type": "integer"
},
{
"title": "array",
"type": "array",
"items": {
"anyOf": [
{
"title": "string",
"type": "string"
},
{
"title": "integer",
"type": "integer"
}
]
}
}
]
}
}
}
},
uiSchema: {},
formData: {
"List of widgets": [
27,
"Batman",
[
"Bruce",
"Wayne"
]
]
}
};
4 changes: 3 additions & 1 deletion playground/samples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import date from "./date";
import validation from "./validation";
import files from "./files";
import single from "./single";
import anyOf from "./anyOf";

export const samples = {
Simple: simple,
Expand All @@ -27,5 +28,6 @@ export const samples = {
"Date & time": date,
Validation: validation,
Files: files,
Single: single
Single: single,
"Any of": anyOf
};
159 changes: 134 additions & 25 deletions src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,19 @@ class ArrayField extends Component {

constructor(props) {
super(props);
this.state = this.getStateFromProps(props);
const formData = this.getStateFromProps(props);
let anyOfItems = [];
if (this.getAnyOfItemsSchema()) {
// We need to contruct the initial anyOfItems state, by searching for the props anyOf items
// in the available anyOf schema items
anyOfItems = this.getAnyOfItemsFromProps(formData.items, props.schema.items.anyOf);
}
this.state = {formData: formData, anyOfItems: anyOfItems};
}

componentWillReceiveProps(nextProps) {
this.setState(this.getStateFromProps(nextProps));
const newState = Object.assign({}, this.state, {formData: this.getStateFromProps(nextProps)});
this.setState(newState);
}

getStateFromProps(props) {
Expand All @@ -75,6 +83,29 @@ class ArrayField extends Component {
return shouldRender(this, nextProps, nextState);
}

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

// If this schema is an array, we need to recursively add its contents
if (schema.type === "array") {
this.getAnyOfItemsFromProps(item, schema.items.anyOf);
}

return schema;
});
}

getAnyOfItemSchema(anyOfSchema, type) {
return anyOfSchema.find((schemaElement) => {
const schemaElementType = schemaElement.type === "integer" ? "number" : schemaElement.type;
return schemaElementType === type;
});
}

get itemTitle() {
const {schema} = this.props;
return schema.items.title || schema.items.description || "Item";
Expand All @@ -86,68 +117,127 @@ class ArrayField extends Component {

asyncSetState(state, options={validate: false}) {
setState(this, state, () => {
this.props.onChange(this.state.items, options);
this.props.onChange(this.state.formData.items, options);
});
}

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

onAddClick = (event) => {
event.preventDefault();
const {items} = this.state;
const {items} = this.state.formData;
const {schema, registry} = this.props;
const {definitions} = registry;
let itemSchema = schema.items;
const anyOfItems = this.getAnyOfItemsSchema();
if (isFixedItems(schema) && allowAdditionalItems(schema)) {
itemSchema = schema.additionalItems;
}
this.asyncSetState({

let newAnyOfItems = [];
if (anyOfItems) {
// We pick the first anyOf item by default
itemSchema = anyOfItems[0];

newAnyOfItems = [
...this.state.anyOfItems,
itemSchema
];
}

const newItems = {
items: items.concat([
getDefaultFormState(itemSchema, undefined, definitions)
])
});
};
const newState = Object.assign({}, this.state, {formData: newItems, anyOfItems: newAnyOfItems});
this.asyncSetState(newState);
};

onDropIndexClick = (index) => {
return (event) => {
event.preventDefault();
this.asyncSetState({
items: this.state.items.filter((_, i) => i !== index)
}, {validate: true}); // refs #195
const {formData: {items}, anyOfItems} = this.state;
const newItems = {
items: items.filter((_, i) => i !== index)
};
const newAnyOfItems = anyOfItems.filter((_, i) => i !== index);
const newState = Object.assign({}, this.state,
{formData: newItems, anyOfItems: newAnyOfItems});
this.asyncSetState(newState, {validate: true}); // refs #195
};
};

onReorderClick = (index, newIndex) => {
return (event) => {
event.preventDefault();
event.target.blur();
const {items} = this.state;
this.asyncSetState({
items: items.map((item, i) => {
const {formData: {items}, anyOfItems} = this.state;

const reorder = (items, newIndex) =>
items.map((item, i) => {
if (i === newIndex) {
return items[index];
} else if (i === index) {
return items[newIndex];
} else {
return item;
}
})
}, {validate: true});
});

const newItems = {
items: reorder(items, newIndex)
};
const newAnyOfItems = reorder(anyOfItems, newIndex);

const newState = Object.assign({}, this.state,
{formData: newItems}, {anyOfItems: newAnyOfItems});
this.asyncSetState(newState, {validate: true});
};
};

onChangeForIndex = (index) => {
return (value) => {
this.asyncSetState({
items: this.state.items.map((item, i) => {
const items = {
items: this.state.formData.items.map((item, i) => {
return index === i ? value : item;
})
});
};
const newState = Object.assign({}, this.state, {formData: items});
this.asyncSetState(newState);
};
};

onSelectChange = (value) => {
this.asyncSetState({items: value});
const newState = Object.assign({}, this.state, {formData: {items: value}});
this.asyncSetState(newState);
};

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

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

const newAnyOfItems = [...this.state.anyOfItems];
newAnyOfItems[index] = foundItem;

const newState = Object.assign({}, this.state,
{formData: {items: newItems}, anyOfItems: newAnyOfItems});

this.asyncSetState(newState);
}

render() {
const {schema, uiSchema} = this.props;
if (isFilesArray(schema, uiSchema)) {
Expand Down Expand Up @@ -175,11 +265,12 @@ class ArrayField extends Component {
autofocus,
} = this.props;
const title = (schema.title === undefined) ? name : schema.title;
const {items} = this.state;
const {formData: {items}, anyOfItems} = this.state;
const {definitions, fields} = this.props.registry;
const {TitleField, DescriptionField} = fields;
const itemsSchema = retrieveSchema(schema.items, definitions);
let itemsSchema = retrieveSchema(schema.items, definitions);
const {addable=true} = getUiOptions(uiSchema);
const anyOfItemsSchema = this.getAnyOfItemsSchema();

return (
<fieldset
Expand All @@ -196,6 +287,9 @@ class ArrayField extends Component {
description={schema.description}/> : null}
<div className="row array-item-list">{
items.map((item, index) => {
if (anyOfItemsSchema) {
itemsSchema = anyOfItems[index];
}
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
const itemIdPrefix = idSchema.$id + "_" + index;
const itemIdSchema = toIdSchema(itemsSchema, itemIdPrefix, definitions);
Expand All @@ -208,7 +302,9 @@ class ArrayField extends Component {
itemErrorSchema,
itemData: items[index],
itemUiSchema: uiSchema.items,
autofocus: autofocus && index === 0
autofocus: autofocus && index === 0,
anyOfItemsSchema: anyOfItemsSchema,
selectWidgetValue: anyOfItems.length > 0 ? anyOfItems[index].type : ""
});
})
}</div>
Expand All @@ -221,7 +317,7 @@ class ArrayField extends Component {

renderMultiSelect() {
const {schema, idSchema, uiSchema, disabled, readonly, autofocus} = this.props;
const {items} = this.state;
const {items} = this.state.formData;
const {widgets, definitions} = this.props.registry;
const itemsSchema = retrieveSchema(schema.items, definitions);
const enumOptions = optionsList(itemsSchema);
Expand All @@ -244,7 +340,7 @@ class ArrayField extends Component {
renderFiles() {
const {schema, uiSchema, idSchema, name, disabled, readonly, autofocus} = this.props;
const title = schema.title || name;
const {items} = this.state;
const {items} = this.state.formData;
const {widgets} = this.props.registry;
const {widget="files", ...options} = getUiOptions(uiSchema);
const Widget = getWidget(schema, widget, widgets);
Expand Down Expand Up @@ -276,7 +372,7 @@ class ArrayField extends Component {
autofocus,
} = this.props;
const title = schema.title || name;
let {items} = this.state;
let {items} = this.state.formData;
const {definitions, fields} = this.props.registry;
const {TitleField} = fields;
const itemSchemas = schema.items.map(item =>
Expand Down Expand Up @@ -347,7 +443,9 @@ class ArrayField extends Component {
itemUiSchema,
itemIdSchema,
itemErrorSchema,
autofocus
autofocus,
anyOfItemsSchema,
selectWidgetValue
}) {
const {SchemaField} = this.props.registry.fields;
const {disabled, readonly, uiSchema} = this.props;
Expand All @@ -363,10 +461,21 @@ class ArrayField extends Component {
};
has.toolbar = Object.keys(has).some(key => has[key]);
const btnStyle = {flex: 1, paddingLeft: 6, paddingRight: 6, fontWeight: "bold"};
const {SelectWidget} = this.props.registry.widgets;

return (
<div key={index} className="array-item">
<div className={has.toolbar ? "col-xs-9" : "col-xs-12"}>
{anyOfItemsSchema ? (
<div className="form-group" style={{width: 120}}>
<SelectWidget
schema={{type: "integer"}}
id="test"
options={{enumOptions: this.anyOfOptions(anyOfItemsSchema)}}
value={selectWidgetValue}
onChange={(value) => this.setWidgetType(index, value)}/>
</div>
) : null}
<SchemaField
schema={itemSchema}
uiSchema={itemUiSchema}
Expand Down
10 changes: 7 additions & 3 deletions src/components/fields/SchemaField.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ const COMPONENT_TYPES = {
string: "StringField",
};

function getFieldComponent(schema, uiSchema, fields) {
function getFieldComponent(schema, name, uiSchema, fields) {
const field = uiSchema["ui:field"];
if (typeof field === "function") {
return field;
}
if (typeof field === "string" && field in fields) {
return fields[field];
}
const componentName = COMPONENT_TYPES[schema.type];


// anyOf logic is handled inside the ArrayField component
const type = name === "anyOf" ? "array" : schema.type;
const componentName = COMPONENT_TYPES[type];
return componentName in fields ? fields[componentName] : UnsupportedField;
}

Expand Down Expand Up @@ -132,7 +136,7 @@ function SchemaField(props) {
const {uiSchema, errorSchema, idSchema, name, required, registry} = props;
const {definitions, fields, formContext, FieldTemplate = DefaultTemplate} = registry;
const schema = retrieveSchema(props.schema, definitions);
const FieldComponent = getFieldComponent(schema, uiSchema, fields);
const FieldComponent = getFieldComponent(schema, name, uiSchema, fields);
const {DescriptionField} = fields;
const disabled = Boolean(props.disabled || uiSchema["ui:disabled"]);
const readonly = Boolean(props.readonly || uiSchema["ui:readonly"]);
Expand Down
Loading

0 comments on commit 3688b94

Please sign in to comment.