Repository: mozilla-services/react-jsonschema-form (js)

Issue:
Form not setting default schema values when using onChange during initialisation
### Prerequisites

- [x] I have read the [documentation](https://github.com/mozilla-services/react-jsonschema-form/blob/master/README.md#readme);
- [ ] In the case of a bug report, I understand that providing a [SSCCE](http://sscce.org/) example is tremendously useful to the maintainers.

### Description

When there are defaults set in the schema such as 
```
  schema: {
    title: "A registration form",
    type: "object",
    properties: {
      firstName: {
        type: "string",
        title: "First name",
        default: "Chuck",
      }
   }
}
```

the default is correctly displayed in the form (and stored internally), but if you are using the `onChange` prop on the form and using that state for say an external submission button you never receive the defaults.

An example of this is can be seen in the playground by changing `/playground/samples/simple.js` by removing `formData.firstName` and chainging the definition of the schema property to:
```
firstName: {
  type: "string",
  title: "First name",
  default: "Chuck",
},
```

The displayed form data on the page doesn't include a firstName field as the playground is using the `onChange` prop to display that value.

I think on the form component mount it should call `onChange` once mounted to ensure any consumers of the form data have the correct values.

Pull request description:
call props.onChange on Form component mount to set any defaults in sc…
### Reasons for making this change

This fixes #1033 where schema defaults aren't being set on any model that is being updated using the form `onChange` prop.

### Checklist

* [ ] **I'm updating documentation**
  - [ ] I've checked the rendering of the Markdown text I've added
  - [ ] If I'm adding a new section, I've updated the Table of Content
* [x] **I'm adding or updating code**
  - [x] I've added and/or updated tests
  - [ ] I've updated docs if needed
  - [x] I've run `npm run cs-format` on my branch to conform my code to [prettier](https://github.com/prettier/prettier) coding style
* [ ] **I'm adding a new feature**
  - [x] I've updated the playground with an example use of the feature


Patch:
diff --git a/playground/samples/simple.js b/playground/samples/simple.js
index f30e10430..223d93a06 100644
--- a/playground/samples/simple.js
+++ b/playground/samples/simple.js
@@ -8,6 +8,7 @@ module.exports = {
       firstName: {
         type: "string",
         title: "First name",
+        default: "Chuck",
       },
       lastName: {
         type: "string",
@@ -60,7 +61,6 @@ module.exports = {
     },
   },
   formData: {
-    firstName: "Chuck",
     lastName: "Norris",
     age: 75,
     bio: "Roundhouse kicking asses since 1940",
diff --git a/src/components/Form.js b/src/components/Form.js
index 88d92f096..b233621b1 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -9,6 +9,7 @@ import {
   toIdSchema,
   setState,
   getDefaultRegistry,
+  deepEquals,
 } from "../utils";
 import validateFormData, { toErrorList } from "../validate";
 
@@ -25,10 +26,23 @@ export default class Form extends Component {
   constructor(props) {
     super(props);
     this.state = this.getStateFromProps(props);
+    if (
+      this.props.onChange &&
+      !deepEquals(this.state.formData, this.props.formData)
+    ) {
+      this.props.onChange(this.state);
+    }
   }
 
   componentWillReceiveProps(nextProps) {
-    this.setState(this.getStateFromProps(nextProps));
+    const nextState = this.getStateFromProps(nextProps);
+    this.setState(nextState);
+    if (
+      !deepEquals(nextState.formData, nextProps.formData) &&
+      this.props.onChange
+    ) {
+      this.props.onChange(nextState);
+    }
   }
 
   getStateFromProps(props) {


Test patch:
diff --git a/test/Form_test.js b/test/Form_test.js
index 08a24e67c..d90de7aed 100644
--- a/test/Form_test.js
+++ b/test/Form_test.js
@@ -44,6 +44,64 @@ describe("Form", () => {
     });
   });
 
+  describe("on component creation", () => {
+    let comp;
+    let onChangeProp;
+    let formData;
+    let schema;
+
+    function createComponent() {
+      comp = renderIntoDocument(
+        <Form schema={schema} onChange={onChangeProp} formData={formData}>
+          <button type="submit">Submit</button>
+          <button type="submit">Another submit</button>
+        </Form>
+      );
+    }
+
+    beforeEach(() => {
+      onChangeProp = sinon.spy();
+      schema = {
+        type: "object",
+        title: "root object",
+        required: ["count"],
+        properties: {
+          count: {
+            type: "number",
+            default: 789,
+          },
+        },
+      };
+    });
+
+    describe("when props.formData does not equal the default values", () => {
+      beforeEach(() => {
+        formData = {
+          foo: 123,
+        };
+        createComponent();
+      });
+
+      it("should call props.onChange with current state", () => {
+        sinon.assert.calledOnce(onChangeProp);
+        sinon.assert.calledWith(onChangeProp, comp.state);
+      });
+    });
+
+    describe("when props.formData equals the default values", () => {
+      beforeEach(() => {
+        formData = {
+          count: 789,
+        };
+        createComponent();
+      });
+
+      it("should not call props.onChange", () => {
+        sinon.assert.notCalled(onChangeProp);
+      });
+    });
+  });
+
   describe("Option idPrefix", function() {
     it("should change the rendered ids", function() {
       const schema = {
@@ -767,6 +825,88 @@ describe("Form", () => {
     });
   });
 
+  describe("Schema and external formData updates", () => {
+    let comp;
+    let onChangeProp;
+
+    beforeEach(() => {
+      onChangeProp = sinon.spy();
+      const formProps = {
+        schema: {
+          type: "string",
+          default: "foobar",
+        },
+        formData: "some value",
+        onChange: onChangeProp,
+      };
+      comp = createFormComponent(formProps).comp;
+    });
+
+    describe("when the form data is set to null", () => {
+      beforeEach(() => comp.componentWillReceiveProps({ formData: null }));
+
+      it("should call onChange", () => {
+        sinon.assert.calledOnce(onChangeProp);
+        sinon.assert.calledWith(onChangeProp, comp.state);
+        expect(comp.state.formData).eql("foobar");
+      });
+    });
+
+    describe("when the schema default is changed but formData is not changed", () => {
+      const newSchema = {
+        type: "string",
+        default: "the new default",
+      };
+
+      beforeEach(() =>
+        comp.componentWillReceiveProps({
+          schema: newSchema,
+          formData: "some value",
+        }));
+
+      it("should not call onChange", () => {
+        sinon.assert.notCalled(onChangeProp);
+        expect(comp.state.formData).eql("some value");
+        expect(comp.state.schema).deep.eql(newSchema);
+      });
+    });
+
+    describe("when the schema default is changed and formData is changed", () => {
+      const newSchema = {
+        type: "string",
+        default: "the new default",
+      };
+
+      beforeEach(() =>
+        comp.componentWillReceiveProps({
+          schema: newSchema,
+          formData: "something else",
+        }));
+
+      it("should not call onChange", () => {
+        sinon.assert.notCalled(onChangeProp);
+        expect(comp.state.formData).eql("something else");
+        expect(comp.state.schema).deep.eql(newSchema);
+      });
+    });
+
+    describe("when the schema default is changed and formData is nulled", () => {
+      const newSchema = {
+        type: "string",
+        default: "the new default",
+      };
+
+      beforeEach(() =>
+        comp.componentWillReceiveProps({ schema: newSchema, formData: null }));
+
+      it("should call onChange", () => {
+        sinon.assert.calledOnce(onChangeProp);
+        sinon.assert.calledWith(onChangeProp, comp.state);
+        expect(comp.state.formData).eql("the new default");
+      });
+    });
+  });
+
   describe("External formData updates", () => {
     describe("root level", () => {
       const formProps = {