Skip to content

Commit 6728977

Browse files
epicfaaceedi9999
authored andcommitted
Fix dependency defaults for uncontrolled components (rjsf-team#1371)
* 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
1 parent 45ec5f6 commit 6728977

File tree

5 files changed

+125
-12
lines changed

5 files changed

+125
-12
lines changed

src/components/Form.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getDefaultRegistry,
1414
deepEquals,
1515
toPathSchema,
16+
isObject,
1617
} from "../utils";
1718
import validateFormData, { toErrorList } from "../validate";
1819

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

3132
constructor(props) {
3233
super(props);
33-
this.state = this.getStateFromProps(props);
34+
this.state = this.getStateFromProps(props, props.formData);
3435
if (
3536
this.props.onChange &&
3637
!deepEquals(this.state.formData, this.props.formData)
@@ -41,7 +42,7 @@ export default class Form extends Component {
4142
}
4243

4344
componentWillReceiveProps(nextProps) {
44-
const nextState = this.getStateFromProps(nextProps);
45+
const nextState = this.getStateFromProps(nextProps, nextProps.formData);
4546
if (
4647
!deepEquals(nextState.formData, nextProps.formData) &&
4748
!deepEquals(nextState.formData, this.state.formData) &&
@@ -52,7 +53,7 @@ export default class Form extends Component {
5253
this.setState(nextState);
5354
}
5455

55-
getStateFromProps(props, inputFormData = props.formData) {
56+
getStateFromProps(props, inputFormData) {
5657
const state = this.state || {};
5758
const schema = "schema" in props ? props.schema : this.props.schema;
5859
const uiSchema = "uiSchema" in props ? props.uiSchema : this.props.uiSchema;
@@ -171,6 +172,10 @@ export default class Form extends Component {
171172
};
172173

173174
onChange = (formData, newErrorSchema) => {
175+
if (isObject(formData) || Array.isArray(formData)) {
176+
const newState = this.getStateFromProps(this.props, formData);
177+
formData = newState.formData;
178+
}
174179
const mustValidate = !this.props.noValidate && this.props.liveValidate;
175180
let state = { formData };
176181
let newFormData = formData;

src/utils.js

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ function computeDefaults(
148148
schema,
149149
parentDefaults,
150150
definitions,
151-
rawFormData = {}
151+
rawFormData = {},
152+
includeUndefinedValues = false
152153
) {
153154
const formData = isObject(rawFormData) ? rawFormData : {};
154155
// Compute the defaults recursively: give highest priority to deepest nodes.
@@ -163,13 +164,31 @@ function computeDefaults(
163164
} else if ("$ref" in schema) {
164165
// Use referenced schema defaults for this node.
165166
const refSchema = findSchemaDefinition(schema.$ref, definitions);
166-
return computeDefaults(refSchema, defaults, definitions, formData);
167+
return computeDefaults(
168+
refSchema,
169+
defaults,
170+
definitions,
171+
formData,
172+
includeUndefinedValues
173+
);
167174
} else if ("dependencies" in schema) {
168175
const resolvedSchema = resolveDependencies(schema, definitions, formData);
169-
return computeDefaults(resolvedSchema, defaults, definitions, formData);
176+
return computeDefaults(
177+
resolvedSchema,
178+
defaults,
179+
definitions,
180+
formData,
181+
includeUndefinedValues
182+
);
170183
} else if (isFixedItems(schema)) {
171184
defaults = schema.items.map(itemSchema =>
172-
computeDefaults(itemSchema, undefined, definitions, formData)
185+
computeDefaults(
186+
itemSchema,
187+
undefined,
188+
definitions,
189+
formData,
190+
includeUndefinedValues
191+
)
173192
);
174193
} else if ("oneOf" in schema) {
175194
schema =
@@ -190,12 +209,16 @@ function computeDefaults(
190209
return Object.keys(schema.properties || {}).reduce((acc, key) => {
191210
// Compute the defaults for this node, with the parent defaults we might
192211
// have from a previous run: defaults[key].
193-
acc[key] = computeDefaults(
212+
let computedDefault = computeDefaults(
194213
schema.properties[key],
195214
(defaults || {})[key],
196215
definitions,
197-
(formData || {})[key]
216+
(formData || {})[key],
217+
includeUndefinedValues
198218
);
219+
if (includeUndefinedValues || computedDefault !== undefined) {
220+
acc[key] = computedDefault;
221+
}
199222
return acc;
200223
}, {});
201224

@@ -225,7 +248,12 @@ function computeDefaults(
225248
return defaults;
226249
}
227250

228-
export function getDefaultFormState(_schema, formData, definitions = {}) {
251+
export function getDefaultFormState(
252+
_schema,
253+
formData,
254+
definitions = {},
255+
includeUndefinedValues = false
256+
) {
229257
if (!isObject(_schema)) {
230258
throw new Error("Invalid schema: " + _schema);
231259
}
@@ -234,7 +262,8 @@ export function getDefaultFormState(_schema, formData, definitions = {}) {
234262
schema,
235263
_schema.default,
236264
definitions,
237-
formData
265+
formData,
266+
includeUndefinedValues
238267
);
239268
if (typeof formData === "undefined") {
240269
// No form data? Use schema defaults.
@@ -244,6 +273,9 @@ export function getDefaultFormState(_schema, formData, definitions = {}) {
244273
// Override schema defaults with form data.
245274
return mergeObjects(defaults, formData);
246275
}
276+
if (formData === 0) {
277+
return formData;
278+
}
247279
return formData || defaults;
248280
}
249281

src/validate.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import toPath from "lodash/toPath";
22
import Ajv from "ajv";
33
let ajv = createAjvInstance();
4-
import { deepEquals } from "./utils";
4+
import { deepEquals, getDefaultFormState } from "./utils";
55

66
let formerCustomFormats = null;
77
let formerMetaSchema = null;
@@ -172,6 +172,10 @@ export default function validateFormData(
172172
additionalMetaSchemas = [],
173173
customFormats = {}
174174
) {
175+
// Include form data with undefined values, which is required for validation.
176+
const { definitions } = schema;
177+
formData = getDefaultFormState(schema, formData, definitions, true);
178+
175179
const newMetaSchemas = !deepEquals(formerMetaSchema, additionalMetaSchemas);
176180
const newFormats = !deepEquals(formerCustomFormats, customFormats);
177181

test/Form_test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2469,4 +2469,28 @@ describe("Form", () => {
24692469
sinon.assert.notCalled(outerOnSubmit);
24702470
});
24712471
});
2472+
2473+
describe("Dependency defaults", () => {
2474+
it("should show dependency defaults for uncontrolled components", () => {
2475+
const schema = {
2476+
type: "object",
2477+
properties: {
2478+
firstName: { type: "string" },
2479+
},
2480+
dependencies: {
2481+
firstName: {
2482+
properties: {
2483+
lastName: { type: "string", default: "Norris" },
2484+
},
2485+
},
2486+
},
2487+
};
2488+
const { node } = createFormComponent({ schema });
2489+
2490+
Simulate.change(node.querySelector("#root_firstName"), {
2491+
target: { value: "Chuck" },
2492+
});
2493+
expect(node.querySelector("#root_lastName").value).eql("Norris");
2494+
});
2495+
});
24722496
});

test/utils_test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,33 @@ describe("utils", () => {
3535
})
3636
).to.eql("foo");
3737
});
38+
39+
it("should keep existing form data that is equal to 0", () => {
40+
expect(
41+
getDefaultFormState(
42+
{
43+
type: "number",
44+
default: 1,
45+
},
46+
0
47+
)
48+
).to.eql(0);
49+
});
50+
51+
const noneValues = [null, undefined, NaN];
52+
noneValues.forEach(noneValue => {
53+
it("should overwrite existing form data that is equal to a none value", () => {
54+
expect(
55+
getDefaultFormState(
56+
{
57+
type: "number",
58+
default: 1,
59+
},
60+
noneValue
61+
)
62+
).to.eql(1);
63+
});
64+
});
3865
});
3966

4067
describe("nested default", () => {
@@ -723,6 +750,27 @@ describe("utils", () => {
723750
});
724751
});
725752
});
753+
754+
describe("with schema keys not defined in the formData", () => {
755+
it("shouldn't add in undefined keys to formData", () => {
756+
const schema = {
757+
type: "object",
758+
properties: {
759+
foo: { type: "string" },
760+
bar: { type: "string" },
761+
},
762+
};
763+
const formData = {
764+
foo: "foo",
765+
baz: "baz",
766+
};
767+
const result = {
768+
foo: "foo",
769+
baz: "baz",
770+
};
771+
expect(getDefaultFormState(schema, formData)).to.eql(result);
772+
});
773+
});
726774
});
727775

728776
describe("asNumber()", () => {

0 commit comments

Comments
 (0)