diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js index 6c59dc5ed9..901dfa43b0 100644 --- a/src/components/fields/ArrayField.js +++ b/src/components/fields/ArrayField.js @@ -12,6 +12,7 @@ import { retrieveSchema, toIdSchema, getDefaultRegistry, + cleanUpNonRequiredArray, } from "../../utils"; function ArrayFieldTitle({ TitleField, idSchema, title, required }) { @@ -202,14 +203,19 @@ class ArrayField extends Component { onAddClick = event => { event.preventDefault(); - const { schema, registry, formData } = this.props; + const { schema, registry, required } = this.props; const { definitions } = registry; + let formData = this.props.formData; let itemSchema = schema.items; if (isFixedItems(schema) && allowAdditionalItems(schema)) { itemSchema = schema.additionalItems; } + this.props.onChange( - [...formData, getDefaultFormState(itemSchema, undefined, definitions)], + [ + ...formData, + getDefaultFormState(itemSchema, undefined, definitions, required), + ], { validate: false } ); }; @@ -219,9 +225,18 @@ class ArrayField extends Component { if (event) { event.preventDefault(); } - const { formData, onChange } = this.props; + const { onChange, required } = this.props; + let formData = this.props.formData; + + // if field isn't required and doesn't contain any entries + // remove the whole property from formData to guarantee correct validation + if (!required) { + formData = cleanUpNonRequiredArray(formData); + } // refs #195: revalidate to ensure properly reindexing errors - onChange(formData.filter((_, i) => i !== index), { validate: true }); + + formData = formData ? formData.filter((_, i) => i !== index) : formData; + onChange(formData, { validate: true }); }; }; @@ -250,12 +265,13 @@ class ArrayField extends Component { onChangeForIndex = index => { return value => { const { formData, onChange } = this.props; - const newFormData = formData.map((item, i) => { + let newFormData = 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; }); + onChange(newFormData, { validate: false }); }; }; diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index ff7e2ed172..e23d850289 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -4,6 +4,7 @@ import { orderProperties, retrieveSchema, getDefaultRegistry, + cleanUpNonRequiredObject, } from "../../utils"; class ObjectField extends Component { @@ -26,7 +27,14 @@ class ObjectField extends Component { onPropertyChange = name => { return (value, options) => { - const newFormData = { ...this.props.formData, [name]: value }; + const { required } = this.props; + + let newFormData = { ...this.props.formData, [name]: value }; + + if (!required) { + newFormData = cleanUpNonRequiredObject(newFormData); + } + this.props.onChange(newFormData, options); }; }; diff --git a/src/utils.js b/src/utils.js index 6d2135e636..89c32d120d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -103,7 +103,12 @@ export function getWidget(schema, widget, registeredWidgets = {}) { throw new Error(`No widget "${widget}" for type "${type}"`); } -function computeDefaults(schema, parentDefaults, definitions = {}) { +function computeDefaults( + schema, + parentDefaults, + definitions = {}, + required = false +) { // Compute the defaults recursively: give highest priority to deepest nodes. let defaults = parentDefaults; if (isObject(defaults) && isObject(schema.default)) { @@ -116,10 +121,10 @@ function computeDefaults(schema, parentDefaults, definitions = {}) { } else if ("$ref" in schema) { // Use referenced schema defaults for this node. const refSchema = findSchemaDefinition(schema.$ref, definitions); - return computeDefaults(refSchema, defaults, definitions); + return computeDefaults(refSchema, defaults, definitions, required); } else if (isFixedItems(schema)) { defaults = schema.items.map(itemSchema => - computeDefaults(itemSchema, undefined, definitions)); + computeDefaults(itemSchema, undefined, definitions, required)); } // Not defaults defined for this node, fallback to generic typed ones. if (typeof defaults === "undefined") { @@ -133,32 +138,76 @@ function computeDefaults(schema, parentDefaults, definitions = {}) { (acc, key) => { // Compute the defaults for this node, with the parent defaults we might // have from a previous run: defaults[key]. - acc[key] = computeDefaults( - schema.properties[key], - (defaults || {})[key], - definitions - ); + if (acc) { + acc[key] = computeDefaults( + schema.properties[key], + (defaults || {})[key], + definitions, + schema.required && schema.required.indexOf(key) > -1 + ); + + if (!required) { + acc = cleanUpNonRequiredObject(acc); + } + } return acc; }, {} ); case "array": - if (schema.minItems) { + if (required && schema.minItems) { return new Array(schema.minItems).fill( - computeDefaults(schema.items, defaults, definitions) + computeDefaults(schema.items, defaults, definitions, required) ); } } return defaults; } -export function getDefaultFormState(_schema, formData, definitions = {}) { +export function cleanUpNonRequiredObject(values) { + const cleanValues = []; + + for (let key in values) { + if (values.hasOwnProperty(key) && typeof values[key] !== "undefined") { + cleanValues.push(values[key]); + } + } + + if (!cleanValues.length) { + values = undefined; + } + + return values; +} + +export function cleanUpNonRequiredArray(values) { + values = values.filter(item => { + return item !== null && item !== undefined; + }); + + values = values.length ? values : undefined; + + return values; +} + +export function getDefaultFormState( + _schema, + formData, + definitions = {}, + required = true +) { if (!isObject(_schema)) { throw new Error("Invalid schema: " + _schema); } + const schema = retrieveSchema(_schema, definitions); - const defaults = computeDefaults(schema, _schema.default, definitions); + const defaults = computeDefaults( + schema, + _schema.default, + definitions, + required + ); if (typeof formData === "undefined") { // No form data? Use schema defaults. return defaults; @@ -197,6 +246,8 @@ export function isObject(thing) { export function mergeObjects(obj1, obj2, concatArrays = false) { // Recursively merge deeply nested objects. + obj1 = obj1 || {}; + obj2 = obj2 || {}; var acc = Object.assign({}, obj1); // Prevent mutation of source object. return Object.keys(obj2).reduce( (acc, key) => { diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js index d2af33113a..2476a01400 100644 --- a/test/ArrayField_test.js +++ b/test/ArrayField_test.js @@ -330,7 +330,7 @@ describe("ArrayField", () => { expect(inputs[3].id).eql("root_foo_1_baz"); }); - it("should render enough inputs with proper defaults to match minItems in schema when no formData is set", () => { + it("should render enough inputs with proper defaults when required to match minItems in schema when no formData is set", () => { const complexSchema = { type: "object", definitions: { @@ -353,13 +353,45 @@ describe("ArrayField", () => { }, }, }, + required: ["foo"], }; let form = createFormComponent({ schema: complexSchema, formData: {} }); let inputs = form.node.querySelectorAll("input[type=text]"); + console.log(0, inputs[0].value); + console.log(1, inputs[1].value); expect(inputs[0].value).eql("Default name"); expect(inputs[1].value).eql("Default name"); }); + it("should not render inputs with defaults when not required to match minItems in schema when no formData is set", () => { + const complexSchema = { + type: "object", + definitions: { + Thing: { + type: "object", + properties: { + name: { + type: "string", + default: "Default name", + }, + }, + }, + }, + properties: { + foo: { + type: "array", + minItems: 2, + items: { + $ref: "#/definitions/Thing", + }, + }, + }, + }; + let form = createFormComponent({ schema: complexSchema, formData: {} }); + let inputs = form.node.querySelectorAll("input[type=text]"); + expect(inputs.length).eql(0); + }); + it("should honor given formData, even when it does not meet ths minItems-requirement", () => { const complexSchema = { type: "object", diff --git a/test/utils_test.js b/test/utils_test.js index 586f4594a3..33c52374b5 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -43,7 +43,7 @@ describe("utils", () => { ).to.eql({ string: "foo" }); }); - it("should recursively map schema object default to form state", () => { + it("should recursively map schema object default to form state if required", () => { expect( getDefaultFormState({ type: "object", @@ -58,6 +58,7 @@ describe("utils", () => { }, }, }, + required: ["object"], }) ).to.eql({ object: { string: "foo" } }); }); @@ -79,6 +80,40 @@ describe("utils", () => { ).to.eql({ array: ["foo", "bar"] }); }); + it("should map schema array to undefined when no data and no defaults provided", () => { + expect( + getDefaultFormState({ + type: "object", + properties: { + array: { + type: "array", + items: { + type: "string", + }, + }, + }, + }) + ).to.eql({ array: undefined }); + }); + + it("should map schema to undefined when not required and every property is undefined data and no defaults provided", () => { + expect( + getDefaultFormState({ + type: "object", + properties: { + object: { + type: "object", + properties: { + string: { + type: "string", + }, + }, + }, + }, + }) + ).to.eql({ object: undefined }); + }); + it("should recursively map schema array default to form state", () => { expect( getDefaultFormState({ @@ -161,7 +196,7 @@ describe("utils", () => { }); }); - it("should use parent defaults for ArrayFields", () => { + it("should use parent defaults for ArrayFields if required", () => { const schema = { type: "object", properties: {