Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
retrieveSchema,
toIdSchema,
getDefaultRegistry,
cleanUpNonRequiredArray,
} from "../../utils";

function ArrayFieldTitle({ TitleField, idSchema, title, required }) {
Expand Down Expand Up @@ -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 }
);
};
Expand All @@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we be doing this filter before the if (!required) check?

onChange(formData, { validate: true });
};
};

Expand Down Expand Up @@ -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) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Does this still need to be let?

// 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 });
};
};
Expand Down
10 changes: 9 additions & 1 deletion src/components/fields/ObjectField.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
orderProperties,
retrieveSchema,
getDefaultRegistry,
cleanUpNonRequiredObject,
} from "../../utils";

class ObjectField extends Component {
Expand All @@ -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);
};
};
Expand Down
75 changes: 63 additions & 12 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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") {
Expand All @@ -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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should schema.required be relevant here?

);
}
}
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to filter out undefined values here?

});

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;
Expand Down Expand Up @@ -197,6 +246,8 @@ export function isObject(thing) {

export function mergeObjects(obj1, obj2, concatArrays = false) {
// Recursively merge deeply nested objects.
obj1 = obj1 || {};
obj2 = obj2 || {};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this change about?

var acc = Object.assign({}, obj1); // Prevent mutation of source object.
return Object.keys(obj2).reduce(
(acc, key) => {
Expand Down
34 changes: 33 additions & 1 deletion test/ArrayField_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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",
Expand Down
39 changes: 37 additions & 2 deletions test/utils_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -58,6 +58,7 @@ describe("utils", () => {
},
},
},
required: ["object"],
})
).to.eql({ object: { string: "foo" } });
});
Expand All @@ -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({
Expand Down Expand Up @@ -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: {
Expand Down