diff --git a/playground/samples/anyOf.js b/playground/samples/anyOf.js
new file mode 100644
index 0000000000..47bd662eb9
--- /dev/null
+++ b/playground/samples/anyOf.js
@@ -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"
+ ]
+ ]
+ }
+};
diff --git a/playground/samples/index.js b/playground/samples/index.js
index 1527696cc1..6414ae3ba5 100644
--- a/playground/samples/index.js
+++ b/playground/samples/index.js
@@ -13,6 +13,7 @@ import validation from "./validation";
import files from "./files";
import single from "./single";
import customArray from "./customArray";
+import anyOf from "./anyOf";
export const samples = {
Simple: simple,
@@ -30,4 +31,5 @@ export const samples = {
Files: files,
Single: single,
"Custom Array": customArray,
+ "Any of": anyOf
};
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index b9d4e54b14..28ef75f709 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -50,6 +50,7 @@ function DefaultArrayItem(props) {
+ {props.selectWidget}
{props.children}
@@ -161,11 +162,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 construct 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) {
@@ -180,6 +189,28 @@ 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;
+ 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";
@@ -191,24 +222,44 @@ 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) => {
@@ -216,9 +267,14 @@ class ArrayField extends Component {
if (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
};
};
@@ -228,9 +284,10 @@ class ArrayField extends Component {
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) {
@@ -238,25 +295,58 @@ class ArrayField extends Component {
} 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)) {
@@ -287,17 +377,21 @@ class ArrayField extends Component {
onBlur
} = this.props;
const title = (schema.title === undefined) ? name : schema.title;
- const {items = []} = this.state;
+ const {formData: {items = []}, anyOfItems} = this.state;
const {ArrayFieldTemplate, definitions, fields} = 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();
const arrayProps = {
canAdd: addable,
items: items.map((item, index) => {
const itemErrorSchema = errorSchema ? errorSchema[index] : undefined;
const itemIdPrefix = idSchema.$id + "_" + index;
+ if (anyOfItemsSchema) {
+ itemsSchema = anyOfItems[index];
+ }
const itemIdSchema = toIdSchema(itemsSchema, itemIdPrefix, definitions);
return this.renderArrayFieldItem({
index,
@@ -309,7 +403,9 @@ class ArrayField extends Component {
itemData: items[index],
itemUiSchema: uiSchema.items,
autofocus: autofocus && index === 0,
- onBlur
+ onBlur,
+ anyOfItemsSchema: anyOfItemsSchema,
+ selectWidgetValue: anyOfItems.length > 0 ? anyOfItems[index].type : ""
});
}),
className: `field field-array field-array-of-${itemsSchema.type}`,
@@ -332,7 +428,7 @@ class ArrayField extends Component {
renderMultiSelect() {
const {schema, idSchema, uiSchema, disabled, readonly, autofocus, onBlur} = this.props;
- const {items} = this.state;
+ const {items} = this.state.formData;
const {widgets, definitions, formContext} = this.props.registry;
const itemsSchema = retrieveSchema(schema.items, definitions);
const enumOptions = optionsList(itemsSchema);
@@ -357,7 +453,7 @@ class ArrayField extends Component {
renderFiles() {
const {schema, uiSchema, idSchema, name, disabled, readonly, autofocus, onBlur} = this.props;
const title = schema.title || name;
- const {items} = this.state;
+ const {items} = this.state.formData;
const {widgets, formContext} = this.props.registry;
const {widget="files", ...options} = getUiOptions(uiSchema);
const Widget = getWidget(schema, widget, widgets);
@@ -393,7 +489,7 @@ class ArrayField extends Component {
onBlur
} = this.props;
const title = schema.title || name;
- let {items} = this.state;
+ let {items} = this.state.formData;
const {ArrayFieldTemplate, definitions, fields} = registry;
const {TitleField} = fields;
const itemSchemas = schema.items.map(item =>
@@ -465,9 +561,12 @@ class ArrayField extends Component {
itemIdSchema,
itemErrorSchema,
autofocus,
- onBlur
+ onBlur,
+ anyOfItemsSchema,
+ selectWidgetValue
}) {
const {SchemaField} = this.props.registry.fields;
+ const {SelectWidget} = this.props.registry.widgets;
const {disabled, readonly, uiSchema} = this.props;
const {orderable, removable} = {
orderable: true,
@@ -481,6 +580,16 @@ class ArrayField extends Component {
};
has.toolbar = Object.keys(has).some(key => has[key]);
+ const selectWidget = anyOfItemsSchema ? (
+
+ this.setWidgetType(index, value)}/>
+
+ ) : null;
return {
children: (
),
+ selectWidget: selectWidget,
className: "array-item",
disabled,
hasToolbar: has.toolbar,
@@ -506,7 +616,9 @@ class ArrayField extends Component {
index,
onDropIndexClick: this.onDropIndexClick,
onReorderClick: this.onReorderClick,
- readonly
+ readonly,
+ anyOfItemsSchema,
+ selectWidgetValue
};
}
}
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 14aab87add..a56b3e5203 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -18,7 +18,7 @@ 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;
@@ -26,7 +26,11 @@ function getFieldComponent(schema, uiSchema, fields) {
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;
}
@@ -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"]);
@@ -158,7 +162,6 @@ function SchemaField(props) {
}
const {__errors, ...fieldErrorSchema} = errorSchema;
-
const field = (
{
expect(node.querySelector("#title-")).to.be.null;
});
});
+ describe("Any of", () => {
+ const schema = {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ }
+ };
+
+ let node, comp;
+
+ beforeEach(() => {
+ const form = createFormComponent({schema});
+ node = form.node;
+ comp = form.comp;
+ Simulate.click(node.querySelector(".array-item-add button"));
+ });
+
+ it("should render an add button", () => {
+ expect(node.querySelector(".array-item-add button"))
+ .not.eql(null);
+ });
+
+ it("should render a select along with the input field", () => {
+ expect(node.querySelectorAll(".array-item"))
+ .to.have.length.of(1);
+ });
+
+ it("should render two select and input fields", () => {
+ Simulate.click(node.querySelector(".array-item-add button"));
+ expect(node.querySelectorAll(".array-item")).to.have.length.of(2);
+ });
+
+ it("should change the type of the widget", () => {
+ Simulate.change(node.querySelector(".array-item select"), {
+ target: {value: "number"}
+ });
+ expect(node.querySelector(".array-item select").value).eql("number");
+ expect(node.querySelectorAll(".array-item .field-number")).to.have.length.of(1);
+ });
+
+ it("should be created with correct type", () => {
+ Simulate.change(node.querySelector("fieldset .field-string input[type=text]"), {target: {value: "asd"}});
+ expect(node.querySelector("fieldset .field-string input[type=text]").value).eql("asd");
+ expect(comp.state.formData).eql(["asd"]);
+ });
+
+ it("should clear the value after type update", () => {
+ Simulate.change(node.querySelector("fieldset .field-string input[type=text]"), {target: {value: "bar"}});
+ Simulate.change(node.querySelector(".array-item select"), {
+ target: {value: "number"}
+ });
+
+ expect(node.querySelector("fieldset .field-number input[type=text]").value).eql("");
+ });
+
+ it("should update types accordingly", () => {
+ Simulate.click(node.querySelector(".array-item-add button"));
+ const selects = node.querySelectorAll(".array-item select");
+
+ Simulate.change(selects[0], {
+ target: {value: "number"}
+ });
+
+ Simulate.change(node.querySelector("fieldset .field-number input[type=text]"), {target: {value: "123"}});
+ Simulate.change(node.querySelector("fieldset .field-string input[type=text]"), {target: {value: "123"}});
+ expect(comp.state.formData).eql([123, "123"]);
+ });
+
+ it("should delete the correct widget", () => {
+ Simulate.click(node.querySelector(".array-item-add button"));
+
+ const selects = node.querySelectorAll(".array-item select");
+
+ Simulate.change(selects[0], {
+ target: {value: "number"}
+ });
+
+ const inputs = node.querySelectorAll("input[type=text]");
+ Simulate.change(inputs[0], {target: {value: 123}});
+ Simulate.change(inputs[1], {target: {value: "abc"}});
+
+ const dropBtns = node.querySelectorAll(".array-item-remove");
+
+ Simulate.click(dropBtns[0]);
+ expect(node.querySelectorAll(".array-item .field-number")).to.have.length.of(0);
+ expect(comp.state.formData).eql(["abc"]);
+ });
+
+ it("should reorder widgets correctly", () => {
+ Simulate.click(node.querySelector(".array-item-add button"));
+
+ const selects = node.querySelectorAll(".array-item select");
+
+ Simulate.change(selects[0], {
+ target: {value: "number"}
+ });
+
+ const inputs = node.querySelectorAll("input[type=text]");
+ Simulate.change(inputs[0], {target: {value: 123}});
+ Simulate.change(inputs[1], {target: {value: "abc"}});
+
+ const moveDownBtns = node.querySelectorAll(".array-item-move-down");
+ Simulate.click(moveDownBtns[0]);
+
+ expect(node.querySelectorAll(".array-item select")[0].value).eql("string");
+ expect(comp.state.formData).eql(["abc", 123]);
+ });
+ });
});