diff --git a/.gitignore b/.gitignore index 7e671b889b..02aeee3be5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ build dist lib +/*.iml +.idea diff --git a/package.json b/package.json index de778a2702..4b989e18e1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dependencies": { "jsonschema": "^1.0.2", "lodash.topath": "^4.5.2", + "lodash.merge": "^4.5.2", "setimmediate": "^1.0.5" }, "devDependencies": { diff --git a/playground/samples/additionalProperties.js b/playground/samples/additionalProperties.js new file mode 100644 index 0000000000..9a9348bc8e --- /dev/null +++ b/playground/samples/additionalProperties.js @@ -0,0 +1,27 @@ +module.exports = { + schema: { + "title": "A registration form", + "description": "A simple form example.", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "devMap": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + } + }, + uiSchema: {}, + formData: { + "name": "test", + "devMap": { + "key": 2, + "value": 12 + } + } +}; diff --git a/playground/samples/index.js b/playground/samples/index.js index 1527696cc1..08cb748499 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 additionalProperties from "./additionalProperties"; export const samples = { Simple: simple, @@ -30,4 +31,5 @@ export const samples = { Files: files, Single: single, "Custom Array": customArray, + AdditionalProperties: additionalProperties }; diff --git a/src/components/Form.js b/src/components/Form.js index 7f8f2f31ea..078c6bbd07 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -18,7 +18,7 @@ export default class Form extends Component { liveValidate: false, safeRenderCompletion: false, noHtml5Validate: false - } + }; constructor(props) { super(props); @@ -93,7 +93,7 @@ export default class Form extends Component { if (this.props.onBlur) { this.props.onBlur(...args); } - } + }; onSubmit = (event) => { event.preventDefault(); diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index 1512aa1dbc..011efbeeda 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -21,10 +21,7 @@ function objectKeysHaveChanged(formData, state) { return true; } // deep check on sorted keys - if (!deepEquals(newKeys.sort(), oldKeys.sort())) { - return true; - } - return false; + return !deepEquals(newKeys.sort(), oldKeys.sort()); } class ObjectField extends Component { @@ -36,7 +33,7 @@ class ObjectField extends Component { required: false, disabled: false, readonly: false, - } + }; constructor(props) { super(props); @@ -84,67 +81,124 @@ class ObjectField extends Component { }; render() { - const { - uiSchema, - errorSchema, - idSchema, - name, - required, - disabled, - readonly, - onBlur - } = this.props; - const {definitions, fields, formContext} = this.props.registry; - const {SchemaField, TitleField, DescriptionField} = fields; + const {name} = this.props; + const {definitions} = this.props.registry; const schema = retrieveSchema(this.props.schema, definitions); const title = (schema.title === undefined) ? name : schema.title; + if ("additionalProperties" in schema){ + return this.renderAdditionalProperties(schema, title); + } + + return this.renderProperties(schema, title); + } + + renderProperties(schema, title){ + const { + uiSchema, + errorSchema, + idSchema, + name, + required, + disabled, + readonly, + onBlur + } = this.props; + const {fields, formContext} = this.props.registry; + const {SchemaField, TitleField, DescriptionField} = fields; + let orderedProperties; try { const properties = Object.keys(schema.properties); orderedProperties = orderProperties(properties, uiSchema["ui:order"]); } catch (err) { return ( -
-

- Invalid {name || "root"} object field configuration: - {err.message}. -

-
{JSON.stringify(schema)}
-
- ); +
+

+ Invalid {name || "root"} object field configuration: + {err.message}. +

+
{JSON.stringify(schema)}
+
+ ); } return ( -
- {title ? : null} - {schema.description ? - : null} - { - orderedProperties.map((name, index) => { - return ( - - ); - }) - }
- ); +
+ {title ? : null} + {schema.description ? + : null} + {orderedProperties.map((name, index) => { + return ( + + ); + })} +
+ ); + } + + renderAdditionalProperties(schema, title){ + const { + uiSchema, + errorSchema, + idSchema, + required, + disabled, + readonly, + onBlur + } = this.props; + const {fields, formContext} = this.props.registry; + const {SchemaField, TitleField, DescriptionField} = fields; + + return ( +
+ {title ? : null} + {schema.description ? + : null} + {Object.keys(this.state).map((name, index) => { + const childIdSchema = {"$id": `${idSchema.$id}__${name}`}; + return ( + + ); + })} +
+ ); } } diff --git a/src/utils.js b/src/utils.js index 4656c99013..b93750e4d8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,7 +1,7 @@ +import merge from "lodash.merge"; import React from "react"; import "setimmediate"; - const widgetMap = { boolean: { checkbox: "CheckboxWidget", @@ -112,7 +112,7 @@ function computeDefaults(schema, parentDefaults, definitions={}) { if (isObject(defaults) && isObject(schema.default)) { // For object defaults, only override parent defaults that are defined in // schema.default. - defaults = mergeObjects(defaults, schema.default); + defaults = merge(defaults, schema.default); } else if ("default" in schema) { // Use schema defaults for this node. defaults = schema.default; @@ -129,6 +129,10 @@ function computeDefaults(schema, parentDefaults, definitions={}) { } // We need to recur for object schema inner default values. if (schema.type === "object") { + if (!schema.properties){ + return defaults; + } + return Object.keys(schema.properties).reduce((acc, key) => { // Compute the defaults for this node, with the parent defaults we might // have from a previous run: defaults[key]. @@ -150,7 +154,7 @@ export function getDefaultFormState(_schema, formData, definitions={}) { return defaults; } if (isObject(formData)) { // Override schema defaults with form data. - return mergeObjects(defaults, formData); + return merge(defaults, formData); } return formData || defaults; } diff --git a/test/utils_test.js b/test/utils_test.js index 97fbfc07cc..a8380ce420 100644 --- a/test/utils_test.js +++ b/test/utils_test.js @@ -6,7 +6,6 @@ import { deepEquals, getDefaultFormState, isMultiSelect, - mergeObjects, pad, parseDateString, retrieveSchema, @@ -264,77 +263,6 @@ describe("utils", () => { }); }); - describe("mergeObjects()", () => { - it("should't mutate the provided objects", () => { - const obj1 = {a: 1}; - mergeObjects(obj1, {b: 2}); - expect(obj1).eql({a: 1}); - }); - - it("should merge two one-level deep objects", () => { - expect(mergeObjects({a: 1}, {b: 2})).eql({a: 1, b: 2}); - }); - - it("should override the first object with the values from the second", () => { - expect(mergeObjects({a: 1}, {a: 2})).eql({a: 2}); - }); - - it("should recursively merge deeply nested objects", () => { - const obj1 = { - a: 1, - b: { - c: 3, - d: [1, 2, 3], - e: {f: {g: 1}} - }, - c: 2 - }; - const obj2 = { - a: 1, - b: { - d: [3, 2, 1], - e: {f: {h: 2}}, - g: 1 - }, - c: 3 - }; - const expected = { - a: 1, - b: { - c: 3, - d: [3, 2, 1], - e: {f: {g: 1, h: 2}}, - g: 1 - }, - c: 3 - }; - expect(mergeObjects(obj1, obj2)).eql(expected); - }); - - describe("concatArrays option", () => { - it("should not concat arrays by default", () => { - const obj1 = {a: [1]}; - const obj2 = {a: [2]}; - - expect(mergeObjects(obj1, obj2)).eql({a: [2]}); - }); - - it("should concat arrays when concatArrays is true", () => { - const obj1 = {a: [1]}; - const obj2 = {a: [2]}; - - expect(mergeObjects(obj1, obj2, true)).eql({a: [1, 2]}); - }); - - it("should concat nested arrays when concatArrays is true", () => { - const obj1 = {a: {b: [1]}}; - const obj2 = {a: {b: [2]}}; - - expect(mergeObjects(obj1, obj2, true)).eql({a: {b: [1, 2]}}); - }); - }); - }); - describe("retrieveSchema()", () => { it("should 'resolve' a schema which contains definitions", () => { const schema = {$ref: "#/definitions/address"};