diff --git a/playground/app.js b/playground/app.js index e3b25acab1..770599e924 100644 --- a/playground/app.js +++ b/playground/app.js @@ -362,6 +362,7 @@ class App extends Component { uiSchema={uiSchema} formData={formData} onChange={this.onFormDataChange} + onSubmit={({formData}) => console.log("submitted formData", formData)} fields={{geo: GeoPosition}} validate={validate} onBlur={(id, value) => console.log(`Touched ${id} with value ${value}`)} diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js index 800612dba1..5344b2f80c 100644 --- a/src/components/fields/ArrayField.js +++ b/src/components/fields/ArrayField.js @@ -168,8 +168,14 @@ class ArrayField extends Component { return schema.items.title || schema.items.description || "Item"; } - isItemRequired(itemsSchema) { - return itemsSchema.type === "string" && itemsSchema.minLength > 0; + isItemRequired(itemSchema) { + if (Array.isArray(itemSchema.type)) { + // While we don't yet support composite/nullable jsonschema types, it's + // future-proof to check for requirement against these. + return !itemSchema.type.includes("null"); + } + // All non-null array item types are inherently required by design + return itemSchema.type !== "null"; } onAddClick = (event) => { @@ -191,10 +197,9 @@ class ArrayField extends Component { if (event) { event.preventDefault(); } - this.props.onChange( - this.props.formData.filter((_, i) => i !== index), - {validate: true} // refs #195 - ); + const {formData, onChange} = this.props; + // refs #195: revalidate to ensure properly reindexing errors + onChange(formData.filter((_, i) => i !== index), {validate: true}); }; }; @@ -204,30 +209,28 @@ class ArrayField extends Component { event.preventDefault(); event.target.blur(); } - const items = this.props.formData; - this.props.onChange( - items.map((item, i) => { - if (i === newIndex) { - return items[index]; - } else if (i === index) { - return items[newIndex]; - } else { - return item; - } - }), - {validate: true} - ); + const {formData, onChange} = this.props; + onChange(formData.map((item, i) => { + if (i === newIndex) { + return formData[index]; + } else if (i === index) { + return formData[newIndex]; + } else { + return item; + } + }), {validate: true}); }; }; onChangeForIndex = (index) => { return (value) => { - this.props.onChange( - this.props.formData.map((item, i) => { - return index === i ? value : item; - }), - {validate: false} - ); + const {formData, onChange} = this.props; + onChange(formData.map((item, i) => { + // We need to treat undefined items as nulls to have validation. + // See https://github.com/tdegrunt/jsonschema/issues/206 + const jsonValue = typeof value === "undefined" ? null : value; + return index === i ? jsonValue : item; + }), {validate: false}); }; }; diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js index 57ee2e82f5..8a6c24328a 100644 --- a/src/components/fields/BooleanField.js +++ b/src/components/fields/BooleanField.js @@ -1,7 +1,6 @@ import React, {PropTypes} from "react"; import { - defaultFieldValue, getWidget, getUiOptions, optionsList, @@ -36,7 +35,7 @@ function BooleanField(props) { id={idSchema && idSchema.$id} onChange={onChange} label={title === undefined ? name : title} - value={defaultFieldValue(formData, schema)} + value={formData} required={required} disabled={disabled} readonly={readonly} diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js index 98df908c92..ab62482617 100644 --- a/src/components/fields/StringField.js +++ b/src/components/fields/StringField.js @@ -1,7 +1,6 @@ import React, {PropTypes} from "react"; import { - defaultFieldValue, getWidget, getUiOptions, optionsList, @@ -36,7 +35,7 @@ function StringField(props) { schema={schema} id={idSchema && idSchema.$id} label={title === undefined ? name : title} - value={defaultFieldValue(formData, schema)} + value={formData} onChange={onChange} onBlur={onBlur} required={required} diff --git a/src/components/widgets/BaseInput.js b/src/components/widgets/BaseInput.js index d07ba50217..5a09ea907c 100644 --- a/src/components/widgets/BaseInput.js +++ b/src/components/widgets/BaseInput.js @@ -24,7 +24,7 @@ function BaseInput(props) { className="form-control" readOnly={readonly} autoFocus={autofocus} - value={typeof value === "undefined" ? "" : value} + value={value == null ? "" : value} onChange={_onChange} onBlur={onBlur && (event => onBlur(inputProps.id, event.target.value))}/> ); diff --git a/src/utils.js b/src/utils.js index 4656c99013..74797b7c81 100644 --- a/src/utils.js +++ b/src/utils.js @@ -64,10 +64,6 @@ export function getDefaultRegistry() { return defaultRegistry; } -export function defaultFieldValue(formData, schema) { - return typeof formData === "undefined" ? schema.default : formData; -} - export function getWidget(schema, widget, registeredWidgets={}) { const {type} = schema; diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js index c71b495474..5d67e6029b 100644 --- a/test/ArrayField_test.js +++ b/test/ArrayField_test.js @@ -111,6 +111,15 @@ describe("ArrayField", () => { .to.have.length.of(1); }); + it("should mark a non-null array item widget as required", () => { + const {node} = createFormComponent({schema}); + + Simulate.click(node.querySelector(".array-item-add button")); + + expect(node.querySelector(".field-string input[type=text]").required) + .eql(true); + }); + it("should fill an array field with data", () => { const {node} = createFormComponent({schema, formData: ["foo", "bar"]}); const inputs = node.querySelectorAll(".field-string input[type=text]"); @@ -229,6 +238,26 @@ describe("ArrayField", () => { .to.have.length.of(0); }); + it("should handle cleared field values in the array", () => { + const schema = { + type: "array", + items: {type: "integer"}, + }; + const formData = [1, 2, 3]; + const {comp, node} = createFormComponent({ + liveValidate: true, + schema, + formData, + }); + + Simulate.change(node.querySelector("#root_1"), { + target: {value: ""} + }); + + expect(comp.state.formData).eql([1, null, 3]); + expect(comp.state.errors).to.have.length.of(1); + }); + it("should render the input widgets with the expected ids", () => { const {node} = createFormComponent({schema, formData: ["foo", "bar"]}); @@ -590,6 +619,16 @@ describe("ArrayField", () => { expect(numInput.id).eql("root_1"); }); + it("should mark non-null item widgets as required", () => { + const {node} = createFormComponent({schema}); + const strInput = + node.querySelector("fieldset .field-string input[type=text]"); + const numInput = + node.querySelector("fieldset .field-number input[type=text]"); + expect(strInput.required).eql(true); + expect(numInput.required).eql(true); + }); + it("should fill fields with data", () => { const {node} = createFormComponent({schema, formData: ["foo", 42]}); const strInput = diff --git a/test/Form_test.js b/test/Form_test.js index c55f8a0c15..d3f8d35e83 100644 --- a/test/Form_test.js +++ b/test/Form_test.js @@ -415,6 +415,23 @@ describe("Form", () => { }); }); + describe("Default value handling on clear", () => { + const schema = { + type: "string", + default: "foo", + }; + + it("should not set default when a text field is cleared", () => { + const {node} = createFormComponent({schema, formData: "bar"}); + + Simulate.change(node.querySelector("input"), { + target: {value: ""} + }); + + expect(node.querySelector("input").value).eql(""); + }); + }); + describe("Defaults array items default propagation", () => { const schema = { type: "object",