Repository: mozilla-services/react-jsonschema-form
Pull request #1028 (2018-09-14 12:38:10)

Added ui:enumDisabled to RadioWidget.js
### Reasons for making this change

I noticed that `ui:enumDisabled` didn't work for the `RadioWidget` so I added it the same way it is implemented in the `SelectWidget`

### 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**
  - [ ] 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) {
diff --git a/src/components/widgets/RadioWidget.js b/src/components/widgets/RadioWidget.js
index a78235319..fbbb34334 100644
--- a/src/components/widgets/RadioWidget.js
+++ b/src/components/widgets/RadioWidget.js
@@ -13,14 +13,17 @@ function RadioWidget(props) {
   } = props;
   // Generating a unique field name to identify this set of radio buttons
   const name = Math.random().toString();
-  const { enumOptions, inline } = options;
+  const { enumOptions, enumDisabled, inline } = options;
   // checked={checked} has been moved above name={name}, As mentioned in #349;
   // this is a temporary fix for radio button rendering bug in React, facebook/react#7630.
   return (
     <div className="field-radio-group">
       {enumOptions.map((option, i) => {
         const checked = option.value === value;
-        const disabledCls = disabled || readonly ? "disabled" : "";
+        const itemDisabled =
+          enumDisabled && enumDisabled.indexOf(option.value) != -1;
+        const disabledCls =
+          disabled || itemDisabled || readonly ? "disabled" : "";
         const radio = (
           <span>
             <input
@@ -29,7 +32,7 @@ function RadioWidget(props) {
               name={name}
               required={required}
               value={option.value}
-              disabled={disabled || readonly}
+              disabled={disabled || itemDisabled || readonly}
               autoFocus={autofocus && i === 0}
               onChange={_ => onChange(option.value)}
             />


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 = {
diff --git a/test/uiSchema_test.js b/test/uiSchema_test.js
index 01848d32c..5db743131 100644
--- a/test/uiSchema_test.js
+++ b/test/uiSchema_test.js
@@ -2,6 +2,7 @@ import { expect } from "chai";
 import React from "react";
 import { Simulate } from "react-addons-test-utils";
 import SelectWidget from "../src/components/widgets/SelectWidget";
+import RadioWidget from "../src/components/widgets/RadioWidget";
 import { createFormComponent, createSandbox } from "./test_utils";
 
 describe("uiSchema", () => {
@@ -439,6 +440,38 @@ describe("uiSchema", () => {
         );
       });
     });
+
+    describe("enum fields disabled radio options", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          field: {
+            type: "string",
+            enum: ["foo", "bar"],
+          },
+        },
+      };
+      const uiSchema = {
+        field: {
+          "ui:widget": RadioWidget,
+          "ui:options": {
+            className: "custom",
+          },
+          "ui:enumDisabled": ["foo"],
+        },
+      };
+      it("should have atleast one radio option disabled", () => {
+        const { node } = createFormComponent({ schema, uiSchema });
+        const disabledOptionsLen = uiSchema.field["ui:enumDisabled"].length;
+        expect(node.querySelectorAll("input:disabled")).to.have.length.of(
+          disabledOptionsLen
+        );
+        expect(node.querySelectorAll("input:enabled")).to.have.length.of(
+          // Two options, one disabled, plus the placeholder
+          2 - disabledOptionsLen
+        );
+      });
+    });
   });
 
   describe("ui:help", () => {