Skip to content

Commit bdd16f2

Browse files
committed
Merge pull request #7 from mozilla-services/fix-defaults-propagation
Fixed defaults initial propagation.
2 parents 19b8281 + 4ade2d8 commit bdd16f2

File tree

10 files changed

+272
-36
lines changed

10 files changed

+272
-36
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module.exports = {
2+
schema: {
3+
type: "object",
4+
title: "lvl 1 obj",
5+
properties: {
6+
object: {
7+
type: "object",
8+
title: "lvl 2 obj",
9+
properties: {
10+
array: {
11+
type: "array",
12+
items: {
13+
type: "object",
14+
title: "lvl 3 obj",
15+
properties: {
16+
bool: {
17+
type: "boolean",
18+
default: true
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}
27+
};

fixtures/Form/nested-default.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ module.exports = {
2828
done: {
2929
type: "boolean",
3030
title: "Done?",
31-
description: "Is that task done already?"
31+
description: "Is that task done already?",
32+
default: true,
3233
},
3334
title: {
3435
type: "string",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
module.exports = {
2+
schema: {
3+
title: "Todo Tasks",
4+
description: "Tasks collection.",
5+
type: "object",
6+
additionalProperties: false,
7+
required: [
8+
"title", "tasks"
9+
],
10+
properties: {
11+
title: {
12+
type: "string",
13+
title: "Tasks list title",
14+
},
15+
tasks: {
16+
type: "array",
17+
title: "Tasks list",
18+
items: {
19+
type: "object",
20+
properties: {
21+
type: {
22+
type: "string",
23+
title: "Category",
24+
enum: ["coding", "sleeping"]
25+
},
26+
done: {
27+
type: "boolean",
28+
title: "Done?",
29+
description: "Is that task done already?",
30+
default: false
31+
},
32+
title: {
33+
type: "string",
34+
title: "Title",
35+
description: "The task title.",
36+
minLength: 1
37+
}
38+
}
39+
}
40+
}
41+
}
42+
},
43+
uiSchema: {
44+
title: {
45+
widget: "textarea",
46+
},
47+
tasks: {
48+
items: {
49+
type: {
50+
widget: "radio"
51+
},
52+
done: {
53+
widget: "select",
54+
},
55+
title: {
56+
widget: "textarea"
57+
}
58+
}
59+
}
60+
},
61+
onSubmit: console.log.bind(console, "submit"),
62+
onError: console.log.bind(console, "errors")
63+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"eslint-plugin-react": "^3.6.3",
3636
"express": "^4.13.3",
3737
"gh-pages": "^0.4.0",
38+
"html": "0.0.10",
3839
"jsdom": "^7.2.1",
3940
"mocha": "^2.3.0",
4041
"react-addons-test-utils": "^0.14.3",

src/components/Form.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { Component, PropTypes } from "react";
22
import { Validator } from "jsonschema";
33

4+
import { getDefaultFormState } from "../utils";
45
import SchemaField from "./fields/SchemaField";
56
import ErrorList from "./ErrorList";
67

@@ -13,7 +14,7 @@ export default class Form extends Component {
1314
constructor(props) {
1415
super(props);
1516
const edit = !!props.formData;
16-
const formData = props.formData || props.schema.default || null;
17+
const formData = props.formData || getDefaultFormState(props.schema) || null;
1718
this.state = {
1819
status: "initial",
1920
formData,
@@ -28,18 +29,18 @@ export default class Form extends Component {
2829
}
2930

3031
renderErrors() {
31-
const {edit, status, errors} = this.state;
32-
if (edit && status !== "editing" && errors.length) {
32+
const {status, errors} = this.state;
33+
if (status !== "editing" && errors.length) {
3334
return <ErrorList errors={errors} />;
3435
}
3536
return null;
3637
}
3738

38-
onChange(formData) {
39+
onChange(formData, options={validate: true}) {
3940
this.setState({
4041
status: "editing",
4142
formData,
42-
errors: this.validate(formData)
43+
errors: options.validate ? this.validate(formData) : this.state.errors
4344
}, _ => {
4445
if (this.props.onChange) {
4546
this.props.onChange(this.state);

src/components/fields/ArrayField.js

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { Component, PropTypes } from "react";
22

3-
import { defaultTypeValue } from "../../utils";
3+
import { getDefaultFormState } from "../../utils";
44
import SchemaField from "./SchemaField";
55

66

@@ -12,50 +12,45 @@ class ArrayField extends Component {
1212
constructor(props) {
1313
super(props);
1414
const formData = Array.isArray(props.formData) ? props.formData : null;
15-
this.state = {items: formData || props.schema.default || []};
15+
this.state = {items: formData || getDefaultFormState(props.schema) || []};
1616
}
1717

1818
get itemTitle() {
1919
const {schema} = this.props;
2020
return schema.items.title || schema.items.description || "Item";
2121
}
2222

23-
defaultItem(itemsSchema) {
24-
if (itemsSchema.default) {
25-
return itemsSchema.default;
26-
}
27-
return defaultTypeValue(itemsSchema.type);
28-
}
29-
3023
isItemRequired(itemsSchema) {
3124
return itemsSchema.type === "string" && itemsSchema.minLength > 0;
3225
}
3326

34-
asyncSetState(state) {
27+
asyncSetState(state, options) {
3528
// ensure state is propagated to parent component when it's actually set
36-
this.setState(state, _ => this.props.onChange(this.state.items));
29+
this.setState(state, _ => this.props.onChange(this.state.items, options));
3730
}
3831

3932
onAddClick(event) {
4033
event.preventDefault();
41-
this.setState({
42-
items: this.state.items.concat(this.defaultItem(this.props.schema.items))
43-
});
34+
const {items} = this.state;
35+
const {schema} = this.props;
36+
this.asyncSetState({
37+
items: items.concat(getDefaultFormState(schema.items))
38+
}, {validate: false});
4439
}
4540

4641
onDropClick(index, event) {
4742
event.preventDefault();
48-
this.setState({
43+
this.asyncSetState({
4944
items: this.state.items.filter((_, i) => i !== index)
50-
});
45+
}, {validate: false});
5146
}
5247

5348
onChange(index, value) {
5449
this.asyncSetState({
5550
items: this.state.items.map((item, i) => {
5651
return index === i ? value : item;
5752
})
58-
});
53+
}, {validate: false});
5954
}
6055

6156
render() {
@@ -68,17 +63,19 @@ class ArrayField extends Component {
6863
{schema.description ? <div>{schema.description}</div> : null}
6964
<div className="array-item-list">{
7065
items.map((item, index) => {
71-
return <div key={index}>
72-
<SchemaField
73-
schema={schema.items}
74-
uiSchema={uiSchema.items}
75-
formData={items[index]}
76-
required={this.isItemRequired(schema.items)}
77-
onChange={this.onChange.bind(this, index)} />
78-
<p className="array-item-remove">
79-
<button type="button"
80-
onClick={this.onDropClick.bind(this, index)}>-</button></p>
81-
</div>;
66+
return (
67+
<div key={index}>
68+
<SchemaField
69+
schema={schema.items}
70+
uiSchema={uiSchema.items}
71+
formData={items[index]}
72+
required={this.isItemRequired(schema.items)}
73+
onChange={this.onChange.bind(this, index)} />
74+
<p className="array-item-remove">
75+
<button type="button"
76+
onClick={this.onDropClick.bind(this, index)}>-</button></p>
77+
</div>
78+
);
8279
})
8380
}</div>
8481
<p className="array-item-add">

src/components/fields/ObjectField.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { Component, PropTypes } from "react";
22

3+
import { getDefaultFormState } from "../../utils";
34
import SchemaField from "./SchemaField";
45

56

@@ -10,7 +11,7 @@ class ObjectField extends Component {
1011

1112
constructor(props) {
1213
super(props);
13-
this.state = props.formData || props.schema.default || {};
14+
this.state = props.formData || getDefaultFormState(props.schema) || {};
1415
}
1516

1617
isRequired(name) {

src/utils.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import RadioWidget from "./components/widgets/RadioWidget";
22
import SelectWidget from "./components/widgets/SelectWidget";
33
import TextareaWidget from "./components/widgets/TextareaWidget";
44

5+
56
export function defaultTypeValue(type) {
67
switch (type) {
78
case "array": return [];
@@ -26,3 +27,19 @@ export function getAlternativeWidget(name) {
2627
default: throw new Error(`No alternative widget for "${name}"`);
2728
}
2829
}
30+
31+
export function getDefaultFormState(schema) {
32+
if (typeof schema !== "object") {
33+
throw new Error("Invalid schema: " + schema);
34+
}
35+
if ("default" in schema) {
36+
return schema.default;
37+
}
38+
if (schema.type === "object") {
39+
return Object.keys(schema.properties).reduce((acc, key) => {
40+
acc[key] = getDefaultFormState(schema.properties[key]);
41+
return acc;
42+
}, {});
43+
}
44+
return defaultTypeValue(schema.type);
45+
}

test/index_test.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function createComponent(props) {
1515
}
1616

1717
function d(node) {
18-
console.log(node.outerHTML);
18+
console.log(require("html").prettyPrint(node.outerHTML, {indent_size: 2}));
1919
}
2020

2121
describe("Form", () => {
@@ -600,6 +600,51 @@ describe("Form", () => {
600600
});
601601
});
602602

603+
describe("Defaults array items default propagation", () => {
604+
const schema = {
605+
type: "object",
606+
title: "lvl 1 obj",
607+
properties: {
608+
object: {
609+
type: "object",
610+
title: "lvl 2 obj",
611+
properties: {
612+
array: {
613+
type: "array",
614+
items: {
615+
type: "object",
616+
title: "lvl 3 obj",
617+
properties: {
618+
bool: {
619+
type: "boolean",
620+
default: true
621+
}
622+
}
623+
}
624+
}
625+
}
626+
}
627+
}
628+
};
629+
630+
it("should propagate deeply nested defaults to form state", () => {
631+
const {comp, node} = createComponent({schema});
632+
633+
Simulate.click(node.querySelector(".array-item-add button"));
634+
Simulate.submit(node);
635+
636+
expect(comp.state.formData).eql({
637+
object: {
638+
array: [
639+
{
640+
bool: true
641+
}
642+
]
643+
}
644+
});
645+
});
646+
});
647+
603648
describe("Validation", () => {
604649
describe("Required fields", () => {
605650
const schema = {

0 commit comments

Comments
 (0)