Skip to content

Commit

Permalink
Fix dependency defaults for uncontrolled components (#1371)
Browse files Browse the repository at this point in the history
* fix: don't return keys with undefined values in computeDefaults

* fix: make sure getDefaultFormState keeps default data equal to 0

* fix: make sure existing formData equal to null is overwritten by defaults

* fix: undo "don't return keys with undefined values"

* fix: fix overwriting of data with defaults for all noneValues

* fix: re-add fix of removing undefined values from computeDefaults

* fix: include form data with undefined values when validateFormData is called

* fix: render dependency defaults for uncontrolled components
  • Loading branch information
epicfaace authored and edi9999 committed Jul 30, 2019
1 parent 45ec5f6 commit 6728977
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 12 deletions.
11 changes: 8 additions & 3 deletions src/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getDefaultRegistry,
deepEquals,
toPathSchema,
isObject,
} from "../utils";
import validateFormData, { toErrorList } from "../validate";

Expand All @@ -30,7 +31,7 @@ export default class Form extends Component {

constructor(props) {
super(props);
this.state = this.getStateFromProps(props);
this.state = this.getStateFromProps(props, props.formData);
if (
this.props.onChange &&
!deepEquals(this.state.formData, this.props.formData)
Expand All @@ -41,7 +42,7 @@ export default class Form extends Component {
}

componentWillReceiveProps(nextProps) {
const nextState = this.getStateFromProps(nextProps);
const nextState = this.getStateFromProps(nextProps, nextProps.formData);
if (
!deepEquals(nextState.formData, nextProps.formData) &&
!deepEquals(nextState.formData, this.state.formData) &&
Expand All @@ -52,7 +53,7 @@ export default class Form extends Component {
this.setState(nextState);
}

getStateFromProps(props, inputFormData = props.formData) {
getStateFromProps(props, inputFormData) {
const state = this.state || {};
const schema = "schema" in props ? props.schema : this.props.schema;
const uiSchema = "uiSchema" in props ? props.uiSchema : this.props.uiSchema;
Expand Down Expand Up @@ -171,6 +172,10 @@ export default class Form extends Component {
};

onChange = (formData, newErrorSchema) => {
if (isObject(formData) || Array.isArray(formData)) {
const newState = this.getStateFromProps(this.props, formData);
formData = newState.formData;
}
const mustValidate = !this.props.noValidate && this.props.liveValidate;
let state = { formData };
let newFormData = formData;
Expand Down
48 changes: 40 additions & 8 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ function computeDefaults(
schema,
parentDefaults,
definitions,
rawFormData = {}
rawFormData = {},
includeUndefinedValues = false
) {
const formData = isObject(rawFormData) ? rawFormData : {};
// Compute the defaults recursively: give highest priority to deepest nodes.
Expand All @@ -163,13 +164,31 @@ function computeDefaults(
} else if ("$ref" in schema) {
// Use referenced schema defaults for this node.
const refSchema = findSchemaDefinition(schema.$ref, definitions);
return computeDefaults(refSchema, defaults, definitions, formData);
return computeDefaults(
refSchema,
defaults,
definitions,
formData,
includeUndefinedValues
);
} else if ("dependencies" in schema) {
const resolvedSchema = resolveDependencies(schema, definitions, formData);
return computeDefaults(resolvedSchema, defaults, definitions, formData);
return computeDefaults(
resolvedSchema,
defaults,
definitions,
formData,
includeUndefinedValues
);
} else if (isFixedItems(schema)) {
defaults = schema.items.map(itemSchema =>
computeDefaults(itemSchema, undefined, definitions, formData)
computeDefaults(
itemSchema,
undefined,
definitions,
formData,
includeUndefinedValues
)
);
} else if ("oneOf" in schema) {
schema =
Expand All @@ -190,12 +209,16 @@ function computeDefaults(
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].
acc[key] = computeDefaults(
let computedDefault = computeDefaults(
schema.properties[key],
(defaults || {})[key],
definitions,
(formData || {})[key]
(formData || {})[key],
includeUndefinedValues
);
if (includeUndefinedValues || computedDefault !== undefined) {
acc[key] = computedDefault;
}
return acc;
}, {});

Expand Down Expand Up @@ -225,7 +248,12 @@ function computeDefaults(
return defaults;
}

export function getDefaultFormState(_schema, formData, definitions = {}) {
export function getDefaultFormState(
_schema,
formData,
definitions = {},
includeUndefinedValues = false
) {
if (!isObject(_schema)) {
throw new Error("Invalid schema: " + _schema);
}
Expand All @@ -234,7 +262,8 @@ export function getDefaultFormState(_schema, formData, definitions = {}) {
schema,
_schema.default,
definitions,
formData
formData,
includeUndefinedValues
);
if (typeof formData === "undefined") {
// No form data? Use schema defaults.
Expand All @@ -244,6 +273,9 @@ export function getDefaultFormState(_schema, formData, definitions = {}) {
// Override schema defaults with form data.
return mergeObjects(defaults, formData);
}
if (formData === 0) {
return formData;
}
return formData || defaults;
}

Expand Down
6 changes: 5 additions & 1 deletion src/validate.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import toPath from "lodash/toPath";
import Ajv from "ajv";
let ajv = createAjvInstance();
import { deepEquals } from "./utils";
import { deepEquals, getDefaultFormState } from "./utils";

let formerCustomFormats = null;
let formerMetaSchema = null;
Expand Down Expand Up @@ -172,6 +172,10 @@ export default function validateFormData(
additionalMetaSchemas = [],
customFormats = {}
) {
// Include form data with undefined values, which is required for validation.
const { definitions } = schema;
formData = getDefaultFormState(schema, formData, definitions, true);

const newMetaSchemas = !deepEquals(formerMetaSchema, additionalMetaSchemas);
const newFormats = !deepEquals(formerCustomFormats, customFormats);

Expand Down
24 changes: 24 additions & 0 deletions test/Form_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2469,4 +2469,28 @@ describe("Form", () => {
sinon.assert.notCalled(outerOnSubmit);
});
});

describe("Dependency defaults", () => {
it("should show dependency defaults for uncontrolled components", () => {
const schema = {
type: "object",
properties: {
firstName: { type: "string" },
},
dependencies: {
firstName: {
properties: {
lastName: { type: "string", default: "Norris" },
},
},
},
};
const { node } = createFormComponent({ schema });

Simulate.change(node.querySelector("#root_firstName"), {
target: { value: "Chuck" },
});
expect(node.querySelector("#root_lastName").value).eql("Norris");
});
});
});
48 changes: 48 additions & 0 deletions test/utils_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,33 @@ describe("utils", () => {
})
).to.eql("foo");
});

it("should keep existing form data that is equal to 0", () => {
expect(
getDefaultFormState(
{
type: "number",
default: 1,
},
0
)
).to.eql(0);
});

const noneValues = [null, undefined, NaN];
noneValues.forEach(noneValue => {
it("should overwrite existing form data that is equal to a none value", () => {
expect(
getDefaultFormState(
{
type: "number",
default: 1,
},
noneValue
)
).to.eql(1);
});
});
});

describe("nested default", () => {
Expand Down Expand Up @@ -723,6 +750,27 @@ describe("utils", () => {
});
});
});

describe("with schema keys not defined in the formData", () => {
it("shouldn't add in undefined keys to formData", () => {
const schema = {
type: "object",
properties: {
foo: { type: "string" },
bar: { type: "string" },
},
};
const formData = {
foo: "foo",
baz: "baz",
};
const result = {
foo: "foo",
baz: "baz",
};
expect(getDefaultFormState(schema, formData)).to.eql(result);
});
});
});

describe("asNumber()", () => {
Expand Down

0 comments on commit 6728977

Please # to comment.