-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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 support for anyOf #417
Changes from all commits
84aca60
4d11758
cc2b5d6
140c457
9b8249d
b2295d9
fbc25e7
2c0dc9f
00a96ac
7d90cc6
3460ec6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
] | ||
] | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,7 @@ function DefaultArrayItem(props) { | |
<div key={props.index} className={props.className}> | ||
|
||
<div className={props.hasToolbar ? "col-xs-9" : "col-xs-12"}> | ||
{props.selectWidget} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems kind of strange for |
||
{props.children} | ||
</div> | ||
|
||
|
@@ -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,34 +222,59 @@ 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) => { | ||
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,35 +284,69 @@ 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) { | ||
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})); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should it be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good spot, thanks! I think it should, yes. Maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good to me! |
||
} | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this work with things like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've tried it & it doesn't support $ref |
||
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 ? ( | ||
<div className="form-group" style={{width: 120}}> | ||
<SelectWidget | ||
schema={{type: "integer", default: selectWidgetValue}} | ||
id="select-widget-id" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same ID shouldn't be used more than once in a DOM. |
||
options={{enumOptions: this.anyOfOptions(anyOfItemsSchema)}} | ||
value={selectWidgetValue} | ||
onChange={(value) => this.setWidgetType(index, value)}/> | ||
</div> | ||
) : null; | ||
return { | ||
children: ( | ||
<SchemaField | ||
|
@@ -497,6 +606,7 @@ class ArrayField extends Component { | |
readonly={this.props.readonly} | ||
autofocus={autofocus}/> | ||
), | ||
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 | ||
}; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first this bothered me, but I see our capitalization isn't consistent --
Custom Array
vs.Date & time
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In any case (😉), leaving "of" as lowercase is correct, even when you're using title case. http://grammar.yourdictionary.com/capitalization/rules-for-capitalization-in-titles.html